頭ん中

しがないITエンジニアが、考えた事を書きます。

UWPでローカルDBのマイグレーションをする(EntityFramework Core / SQLite)

まえがき

ちょっと昔のAndroidアプリやWindowsアプリで、分岐とDDL文を散りばめたギリギリの運用を目にすることが結構ありました。

ローカルDBとテーブルをCodeFirstに生成し、とりあえずデータをCRUDする方法が紹介された記事は一杯ありますが、アプリをアップデートする際のマイグレーションまで解説されたドキュメントは、あまりないんですよね。

ということで、たぶん現在もっとも標準的と思われるWindowsアプリの構成(UWP / EntityFramework Core / SQLite)で、ローカルDBのマイグレーションを試していきたいと思います。

前提

動作環境

Entity Framework Coreの設定・実装

プロジェクト作成

手っ取り早くWindows Template Studioで新規アプリを作ります。以下の2つのプロジェクトができると思います。

  • MyApp
  • MyApp.Core

モデルクラスの実装

NugetでMicrosoft.EntityFrameworkCore.Sqliteを入れて、MyApp.CoreプロジェクトにDbContextとModelクラスを実装します。

using Microsoft.EntityFrameworkCore;

namespace MyApp.Core.Models
{
    public class BloggingContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder options)
            => options.UseSqlite("Data Source=blogging.db");
    }

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }

        public List<Post> Posts { get; } = new List<Post>();
    }

    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }
}

初回のマイグレーション

マイグレーションコード生成

パッケージマネージャコンソールから初回のマイグレーションを実行したいのですが、、どちらのプロジェクトで実行しても以下のエラーになってしまう。

PM> Add-Migration InitialCreate
Startup project 'MyApp' is a Universal Windows Platform app. This version of the Entity Framework Core Package Manager Console Tools doesn't support this type of project. For more information on using the EF Core Tools with UWP projects, see https://go.microsoft.com/fwlink/?linkid=858496
PM> Add-Migration InitialCreate
Build started...
Build succeeded.
Startup project 'MyApp.Core' targets framework '.NETStandard'. There is no runtime associated with this framework, and projects targeting it cannot be executed directly. To use the Entity Framework Core Package Manager Console Tools with this project, add an executable project targeting .NET Framework or .NET Core that references this project, and set it as the startup project; or, update this project to cross-target .NET Framework or .NET Core. For more information on using the EF Core Tools with .NET Standard projects, see https://go.microsoft.com/fwlink/?linkid=2034705

どうやらUWPプロジェクトや.NET StandardアプリではEFツールが使えない様なので、新しいプロジェクトからコンソールアプリDbMigrationを追加します。 f:id:siamcats:20200215040833p:plain

作成したDbMigrationの依存関係にMyApp.Coreへの参照を追加し、NugetでMicrosoft.EntityFrameworkCore.Designをインストールします。これでプロジェクトは以下の3つになりました。

  • MyApp
  • MyApp.Core
  • DbMigration ←NEW!

パッケージマネージャコンソールから以下のコマンドでマイグレーションを実行します。この時、スタートアッププロジェクトはDbMigrationを、パッケージマネージャコンソールの既定のプロジェクトをMyApp.Coreとするのを間違えないようにしてください。

Add-Migration {マイグレーション名}

今回はマイグレーションInitialCreateとして実行し、MyApp.Coreの以下の場所にマイグレーションファイルが作成されました。 f:id:siamcats:20200215043032p:plain

UWPアプリの初回動作

ここからはUWPアプリMyAppプロジェクトです。App.xaml.csコンストラクタでcontext.Database.Migration()を呼び出します。

        public App()
        {
            //中略
            using (var db = new BloggingContext())
            {
                db.Database.Migrate();
            }
        }

これでアプリを起動するとにローカルDBファイルが生成されるはずだったんですが、例外が発生します。

Microsoft.Data.Sqlite.SqliteException: 'SQLite Error 14: 'unable to open database file'.' localfolder

UWP problem with Microsoft.Data.Sqlite version 3.0.0 - Developer Community

EF.Coreのissueにもあげられてるようですが、3.0.0以降で発生する問題のようです。接続文字列で明示的にApplicationData.Current.LocalFolder.Pathを指定すればいいようですが、今回はContextクラスを用意したMyApp.Coreが.NET Standardなのですんなりできなさそう。

仕方ないのでNugetでMicrosoft.EntityFrameworkCore.SqliteMicrosoft.EntityFrameworkCore.Designのバージョンを2.2.6に下げます。

これでアプリを起動すると無事{ユーザーディレクトリ}AppData\Local\Packages\{アプリディレクトリ}\LocalState}にローカルDBが生成されました。 f:id:siamcats:20200216123923p:plain

中を見ると、InitialCreateで定義した通りのテーブル定義と、マイグレーションの履歴データができていると思います。 f:id:siamcats:20200216104831p:plain

2回目以降のマイグレーションの流れ

今後モデルの定義を変更した場合は以下の流れで反映します。

  1. MyApp.Coreのモデル定義を変更する。
  2. DbMigrationマイグレーションコード生成する。
  3. これでMyAppのUWPアプリ起動時にローカルDBに自動反映される。

モデル定義変更

試しにMyApp.CoreのBlogクラスにTiteとSubtitleプロパティを追加してみます。

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
        public string Title{ get; set; }     //2020.04.01 Add
        public string Subtitle{ get; set; }  //2020.04.01 Add

        public List<Post> Posts { get; } = new List<Post>();
    }

マイグレーションコード生成

DbMigrationプロジェクトでマイグレーションを実行します。

Add-Migration AddBlogTitleColumns

MyApp.Coreプロジェクトに、カラム追加のマイグレーションファイルができました。 f:id:siamcats:20200216111304p:plain

アプリ起動

MyAppプロジェクトに戻ってUWPアプリを実行すると、ローカルDBにカラムが追加されています。 f:id:siamcats:20200216111750p:plain

3回目のマイグレーション

同じ手順の繰り返しになりますが、念のためバージョンが飛んだりユーザーのデータがある場合にどうなるかを試してみます。

今度はPostモデルを変更します。

    public class Post
    {
        public int PostId { get; set; }
        public DateTime PostDate { get; set; } //2020.06.01 Add
        public string Subject { get; set; }    //2020.06.01 Rename
        public string Content { get; set; }

        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }
Add-Migration ModifyPostTable

ユーザーデータが存在する場合の動作

ローカルDBにユーザーデータを作っておいて、 f:id:siamcats:20200216115650p:plain

アプリを実行すると、

無事、ユーザーデータが残ったまま、定義が更新されていますね。 f:id:siamcats:20200216115922p:plain

バージョン飛ばしてマイグレーションする場合の動作

初回マグレーションInitalCreate状態のローカルDBにしておいて、 f:id:siamcats:20200216120411p:plain f:id:siamcats:20200216120514p:plain

アプリを実行すると、

2回分のマイグレーションが反映され、しっかり最新の定義に追いついています。 f:id:siamcats:20200216120621p:plain f:id:siamcats:20200216120712p:plain

あとがき

DBマイグレーションというと、大規模なシステム更改における集中管理DBのデータ移行を思い浮かべる人が多いと思います。レプリケーションどうするとか、本番サービスは継続させるんだとか、トランザクションが重複したとか、いろんなドラマがありそうなネタです。

ネイティブアプリにとっても、バージョンアップデートに伴うローカルDBのマイグレーションは、地味ですが大事な問題です。本番製品運用するアプリは、数多の端末にローカルDBが存在していて、バージョンもばらばらで、そこにはユーザーの大切なデータが保管されています。それらをマイグレーションするのって、実は割と難易度が高くリスクも大きい処理だと思います。

場当たり的な対応じゃなくて、初期構築の時からマイグレーションに考慮した、綺麗で安全な設計・実装をしていきたいですね(自戒)

参考

*1:公式のチュートリアル通りだと、詰まりポイントが多かった…。

*2:今回試したカラム追加や名称変更は、Migration()で自動的にマイグレーション可能でしたが、カラム削除などは不可能です。アプリでのMigration()メソッド実行時にNotSupportedExceptionが発生します。マイグレーションコードは生成できるので、自力で何とか処理するしかない。

*3:ローカルDBのバックアップにもなるし、こういう方法でもいい気もする。みんなマイグレーションどうやってるんでしょうね?ベストプラクティスが知りたい。