C#
migration
EntityFramework
sqlite
CodeFirst

EntityFramework6 CodeFirst+SQLiteでMigrationもしちゃう方法

概要

C#でEF6 CodeFirst+SQLite+Migrationを実現する方法です。
Windows用の小規模をアプリを作るときにオススメです。
※注意:EF Coreではありません。

サンプルコード

以下に実際に動作するコードを置いてます。
https://github.com/minoru-nagasawa/SQLiteMigrationSample

初期状態のデータベースの作成

1. NuGetで必要なパッケージを取得

プロジェクトを右クリックし、「NuGetパッケージの管理」を選択します。
select_manage_nuget_package.png

「参照」タブからSystem.Data.SQLite.EF6.Migrationsを検索し、インストールします。
インストール後、「インストール済み」タブに以下の6つが表示されればOKです。
EntityFrameworkの6.2が欲しい場合は、個別に更新してください。
- EntityFramework
- System.Data.SQLite
- System.Data.SQLite.Core
- System.Data.SQLite.EF6
- System.Data.SQLite.EF6.Migrations
- System.Data.SQLite.Linq
installed_nuget_package.PNG

2. 必要なクラスを作成

作業後のイメージです。
「2. 必要なクラスを作成」が完了したら、以下の構成になります。
file_list_after_creation_1st_class.png


まず、以下の2つのフォルダを作ります。(任意です)
- DataAccess
- Models


データベースのテーブルに対応するクラスを作成します。
いわゆるPOCOなクラスです。
EntityFrameworkが使用するためデフォルトコンストラクタは必須です。

Models/SamplePoco.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace SQLiteMigrationSample.Models
{
    [Table("Sample")]
    public class SamplePoco
    {
        [Key]
        public long Id { get; set; }

        [Required]
        public string Name { get; set; }

        public SamplePoco()
        {
        }

        public SamplePoco(long id, string name)
        {
            Id = id;
            Name = name;
        }
    }
}

DbContextを継承したクラスを作成します。
このクラスを使ってデータベースにアクセスします。

今回はUpdate-Databaseの結果を保存するデータベースを、プロジェクトと同じパスにします。
そのため、そのパスをs_migrationSqlitePathにセットして、デフォルトコンストラクタで指定しています。
このデータベースはアプリケーションの実行時には使用しません。
あくまでUpdate-Databaseで使用するだけです。

デフォルトコンストラクタはEnable-Migrationsなどで使用されるため必須です。
また、baseの第2引数をfalseにしないとUpdate-Databaseで例外が発生します。

DataAccess/ApplicationDbContext.cs
using SQLiteMigrationSample.Models;
using System;
using System.Data.Common;
using System.Data.Entity;
using System.Data.SQLite;
using System.IO;

namespace SQLiteMigrationSample.DataAccess
{
    public class ApplicationDbContext : DbContext
    {
        static private string s_migrationSqlitePath;
        static ApplicationDbContext()
        {
            var exeDir     = AppDomain.CurrentDomain.BaseDirectory;
            var exeDirInfo = new DirectoryInfo(exeDir);
            var projectDir = exeDirInfo.Parent.Parent.FullName;
            s_migrationSqlitePath = $@"{projectDir}\MigrationDb.sqlite3";
        }

        public ApplicationDbContext() : base(new SQLiteConnection($"DATA Source={s_migrationSqlitePath}"), false)
        {
        }

        public ApplicationDbContext(DbConnection connection) : base(connection, true)
        {
        }

        public DbSet<SamplePoco> Samples { get; set; }
    }
}

コンフィグレーションクラスを作成します。
クラスを作らずに、同様の内容をApp.configに書く方法でもいいと思いますが、個人的な好みでコードに書きます。

DataAccess/SQLiteCOnfiguration.cs
using System.Data.Entity;
using System.Data.Entity.Core.Common;
using System.Data.SQLite;
using System.Data.SQLite.EF6;

namespace SQLiteMigrationSample.DataAccess
{
    public class SQLiteConfiguration : DbConfiguration
    {
        public SQLiteConfiguration()
        {
            SetProviderFactory("System.Data.SQLite",     SQLiteFactory.Instance);
            SetProviderFactory("System.Data.SQLite.EF6", SQLiteProviderFactory.Instance);
            SetProviderServices("System.Data.SQLite",    (DbProviderServices)SQLiteProviderFactory.Instance.GetService(typeof(DbProviderServices)));
        }
    }
}

3. Migrationの実施

[表示]-[その他のウィンドウ]-[パッケージマネージャーコンソール]を選択します。
select_package_manager_console.png


パッケージマネージャコンソールで"Enable-Migrations"を実行します。
実行するとMigrations/Configuration.csが自動生成されます。
「既定のプロジェクト」が間違っているとエラーになるので注意してください。
enable_migrations.PNG


自動生成されたConfigurationクラスのコンストラクタで、AutomaticMigrationsEnabledをtrueにします。
また、SetSqlGeneratorを呼び出すようにします。

Migrations/COnfiguration.cs
namespace SQLiteMigrationSample.Migrations
{
    using System;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;

    internal sealed class Configuration : DbMigrationsConfiguration<SQLiteMigrationSample.DataAccess.ApplicationDbContext>
    {
        public Configuration()
        {
            // trueに変更
            AutomaticMigrationsEnabled = true;

            // 以下のコードを追加
            SetSqlGenerator("System.Data.SQLite", new SQLiteMigrationSqlGenerator());
        }

        protected override void Seed(SQLiteMigrationSample.DataAccess.ApplicationDbContext context)
        {
        }
    }
}

パッケージマネージャコンソールで"Add-Migration InitialMigration"を実行します。
パラメータの「InitialMigration」の部分は任意の文字列で、クラス名に使われます。
実行すると201805061344319_InitialMigration.csのようなコードが自動生成されます。
add_migration.PNG


パッケージマネージャコンソールで"Update-Database"を実行します。
実行するとプロジェクトと同じフォルダにMigrationDb.sqlite3が生成されます。
update_database.PNG

4. コードからのMigrationの実行

以下のコードが具体例です。
実行すると、exeと同じ場所にdb.sqlite3が生成されます。
また、マイグレーションも実施されます。
つまり、何もなければSampleテーブルを作り、存在していれば何もしません。

ポイントは以下です。

  • DbConnectionInfoの第2引数は、リフレクションを使って取得しています。
    直接"System.Data.SQLite"を渡してもいいです。
  • ConfigurationのTargetDatabaseにDbConnectionInfoを設定します。
    DbMigratorのTargetDatabaseに設定しても効果が無く、例えばmigrator.GetPendingMigrations()の結果が正しく取得できないなどの問題が起きます。
  • EntityFramework 6.2のバグ?のため、リフレクションを使って直接DbMigratorのprivateメンバを変更する必要があります。
Program.cs
using SQLiteMigrationSample.DataAccess;
using SQLiteMigrationSample.Migrations;
using SQLiteMigrationSample.Models;
using System;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Migrations;
using System.Data.SQLite;
using System.Linq;
using System.Reflection;

namespace SQLiteMigrationSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var exeDir  = AppDomain.CurrentDomain.BaseDirectory;
            var dbPath  = $"{exeDir}db.sqlite3";
            var connStr = $"DATA Source={dbPath}";
            using (var connection = new SQLiteConnection(connStr))
            {
                using (var context = new ApplicationDbContext(connection))
                {
                    // providerNameをコードを使って取得する。
                    // コードを使わずに、直接"System.Data.SQLite"を使ってもいい
                    // https://stackoverflow.com/questions/36060478/dbmigrator-does-not-detect-pending-migrations-after-switching-database
                    var internalContext = context.GetType().GetProperty("InternalContext", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(context);
                    var providerName = (string)internalContext.GetType().GetProperty("ProviderName").GetValue(internalContext);

                    // Migratorが使うConfigurationを生成する。
                    // TargetDatabaseはDbMigratorの方ではなく、Configurationの方に設定しないと効果が無い。
                    var configuration = new Configuration()
                    {
                        TargetDatabase = new DbConnectionInfo(context.Database.Connection.ConnectionString, providerName)
                    };

                    // DbMigratorを生成する
                    var migrator = new DbMigrator(configuration);

                    // EF6.13では問題ないが、EF6.2の場合にUpdateのタイミングで以下の例外が吐かれないようにする対策
                    // System.ObjectDisposedException: '破棄されたオブジェクトにアクセスできません。
                    // オブジェクト名 'SQLiteConnection' です。'
                    // https://stackoverflow.com/questions/47329496/updating-to-ef-6-2-0-from-ef-6-1-3-causes-cannot-access-a-disposed-object-error/47518197
                    var _historyRepository = migrator.GetType().GetField("_historyRepository", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(migrator);
                    var _existingConnection = _historyRepository.GetType().BaseType.GetField("_existingConnection", BindingFlags.Instance | BindingFlags.NonPublic);
                    _existingConnection.SetValue(_historyRepository, null);

                    // Migrationを実行する。
                    migrator.Update();

                    // データベースにアクセスして保存する例
                    if (context.Samples.Count() == 0)
                    {
                        var dummyItem = new SamplePoco()
                        {
                            Id = 1,
                            Name = "Dummy"
                        };
                        context.Samples.Add(dummyItem);
                        context.SaveChanges();
                    }
                }
            }
        }
    }
}

データベースの更新

1. テーブル用クラスの作成or更新

例えば以下のような新規のテーブル用クラスを作成します。

Models/TestPoco.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace SQLiteMigrationSample.Models
{
    [Table("Test")]
    public class TestPoco
    {
        [Key]
        public long Id { get; set; }

        [Required]
        public string Name { get; set; }

        public TestPoco()
        {
        }

        public TestPoco(long id, string name)
        {
            Id = id;
            Name = name;
        }
    }
}

2. (クラスを追加したら)ApplicationDbContextにも追加

クラスを追加した場合はApplicationDbContextにメンバを追加します。
クラスの変更の場合は不要です。

DataAccess/ApplicationDbContext.cs
namespace SQLiteMigrationSample.DataAccess
{
    public class ApplicationDbContext : DbContext
    {
        :
        // 追加
        public DbSet<TestPoco> Tests { get; set; }
        :
    }
}

3. Migrationの実施

パッケージマネージャコンソールで"Add-Migration AddTestTableMigration"を実行します。
パラメータの「AddTestTableMigration」の部分は任意の文字列で、クラス名に使われます。
実行すると201805061529099_AddTestTableMigration.csのようなコードが自動生成されます。


パッケージマネージャコンソールで"Update-Database"を実行します。
実行するとプロジェクトと同じフォルダのMigrationDb.sqlite3が更新されます。

4. コードからのMigrationの実行

既存コードのmigrator.Update()のタイミングで、データベースが更新されます。
そのため、以下のようなコードで更新したデータベースにアクセスできます。

Program.cs
namespace SQLiteMigrationSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var exeDir  = AppDomain.CurrentDomain.BaseDirectory;
            var dbPath  = $"{exeDir}db.sqlite3";
            var connStr = $"DATA Source={dbPath}";
            using (var connection = new SQLiteConnection(connStr))
            {
                using (var context = new ApplicationDbContext(connection))
                {
                    :
                    migrator.Update();

                    // 以下のようにTestテーブルにアクセスできる
                    if (context.Tests.Count() == 0)
                    {
                        var dummyItem = new TestPoco()
                        {
                            Id = 1,
                            Name = "Dummy"
                        };
                        context.Tests.Add(dummyItem);
                        context.SaveChanges();
                    }
                }
            }
        }
    }
}

参考URL

参考:失敗時の情報

ApplicationDbContextのデフォルトコンストラクタの第2引数がtrueでUpdate-Databaseを実行したときの例外

PM> Update-database
Specify the '-Verbose' flag to view the SQL statements being applied to the target database.
System.ObjectDisposedException: 破棄されたオブジェクトにアクセスできません。
オブジェクト名 'SQLiteConnection' です。
   場所 System.Data.SQLite.SQLiteConnection.CheckDisposed()
   場所 System.Data.SQLite.SQLiteConnection.get_State()
   場所 System.Data.Entity.Internal.RepositoryBase.CreateConnection()
   場所 System.Data.Entity.Migrations.History.HistoryRepository.QueryExists(String contextKey)
   場所 System.Data.Entity.Migrations.History.HistoryRepository.Exists(String contextKey)
   場所 System.Data.Entity.Migrations.History.HistoryRepository.<GetUpgradeOperations>d__16.MoveNext()
   場所 System.Linq.Enumerable.Any[TSource](IEnumerable`1 source)
   場所 System.Data.Entity.Migrations.DbMigrator.UpdateInternal(String targetMigration)
   場所 System.Data.Entity.Migrations.DbMigrator.<>c__DisplayClasse.<Update>b__d()
   場所 System.Data.Entity.Migrations.DbMigrator.EnsureDatabaseExists(Action mustSucceedToKeepDatabase)
   場所 System.Data.Entity.Migrations.Infrastructure.MigratorBase.EnsureDatabaseExists(Action mustSucceedToKeepDatabase)
   場所 System.Data.Entity.Migrations.DbMigrator.Update(String targetMigration)
   場所 System.Data.Entity.Migrations.Infrastructure.MigratorBase.Update(String targetMigration)
   場所 System.Data.Entity.Migrations.Design.ToolingFacade.UpdateRunner.RunCore()
   場所 System.Data.Entity.Migrations.Design.ToolingFacade.BaseRunner.Run()
破棄されたオブジェクトにアクセスできません。
オブジェクト名 'SQLiteConnection' です。

SQLiteConfigurationを作成しないでEnable-Migrationsを実行したときに発生する例外

PM> Enable-Migrations
Checking if the context targets an existing database...
System.InvalidOperationException: No Entity Framework provider found for the ADO.NET provider with invariant name 'System.Data.SQLite'. Make sure the provider is registered in the 'entityFramework' section of the application config file. See http://go.microsoft.com/fwlink/?LinkId=260882 for more information.
   場所 System.Data.Entity.Infrastructure.DependencyResolution.DefaultProviderServicesResolver.GetService(Type type, Object key)
   場所 System.Data.Entity.Infrastructure.DependencyResolution.CachingDependencyResolver.<>c__DisplayClass1.<GetService>b__0(Tuple`2 k)
   場所 System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   場所 System.Data.Entity.Infrastructure.DependencyResolution.CachingDependencyResolver.GetService(Type type, Object key)
   場所 System.Data.Entity.Infrastructure.DependencyResolution.ResolverChain.<>c__DisplayClass3.<GetService>b__0(IDbDependencyResolver r)
   場所 System.Linq.Enumerable.WhereSelectArrayIterator`2.MoveNext()
   場所 System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source, Func`2 predicate)
   場所 System.Data.Entity.Infrastructure.DependencyResolution.ResolverChain.GetService(Type type, Object key)
   場所 System.Data.Entity.Infrastructure.DependencyResolution.RootDependencyResolver.GetService(Type type, Object key)
   場所 System.Data.Entity.Infrastructure.DependencyResolution.ResolverChain.<>c__DisplayClass3.<GetService>b__0(IDbDependencyResolver r)
   場所 System.Linq.Enumerable.WhereSelectArrayIterator`2.MoveNext()
   場所 System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source, Func`2 predicate)
   場所 System.Data.Entity.Infrastructure.DependencyResolution.ResolverChain.GetService(Type type, Object key)
   場所 System.Data.Entity.Infrastructure.DependencyResolution.CompositeResolver`2.GetService(Type type, Object key)
   場所 System.Data.Entity.Infrastructure.DependencyResolution.DbDependencyResolverExtensions.GetService[T](IDbDependencyResolver resolver, Object key)
   場所 System.Data.Entity.Utilities.DbProviderFactoryExtensions.GetProviderServices(DbProviderFactory factory)
   場所 System.Data.Entity.Infrastructure.DefaultManifestTokenResolver.<>c__DisplayClass1.<ResolveManifestToken>b__0(Tuple`3 k)
   場所 System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   場所 System.Data.Entity.Infrastructure.DefaultManifestTokenResolver.ResolveManifestToken(DbConnection connection)
   場所 System.Data.Entity.Utilities.DbConnectionExtensions.GetProviderInfo(DbConnection connection, DbProviderManifest& providerManifest)
   場所 System.Data.Entity.DbModelBuilder.Build(DbConnection providerConnection)
   場所 System.Data.Entity.Internal.LazyInternalContext.CreateModel(LazyInternalContext internalContext)
   場所 System.Data.Entity.Internal.RetryLazy`2.GetValue(TInput input)
   場所 System.Data.Entity.Internal.LazyInternalContext.InitializeContext()
   場所 System.Data.Entity.Internal.LazyInternalContext.get_ModelBeingInitialized()
   場所 System.Data.Entity.Infrastructure.EdmxWriter.WriteEdmx(DbContext context, XmlWriter writer)
   場所 System.Data.Entity.Utilities.DbContextExtensions.<>c__DisplayClass1.<GetModel>b__0(XmlWriter w)
   場所 System.Data.Entity.Utilities.DbContextExtensions.GetModel(Action`1 writeXml)
   場所 System.Data.Entity.Utilities.DbContextExtensions.GetModel(DbContext context)
   場所 System.Data.Entity.Migrations.DbMigrator..ctor(DbMigrationsConfiguration configuration, DbContext usersContext, DatabaseExistenceState existenceState, Boolean calledByCreateDatabase)
   場所 System.Data.Entity.Migrations.DbMigrator..ctor(DbMigrationsConfiguration configuration)
   場所 System.Data.Entity.Migrations.Design.MigrationScaffolder..ctor(DbMigrationsConfiguration migrationsConfiguration)
   場所 System.Data.Entity.Migrations.Design.ToolingFacade.ScaffoldRunner.Run()
   場所 System.AppDomain.DoCallBack(CrossAppDomainDelegate callBackDelegate)
   場所 System.AppDomain.DoCallBack(CrossAppDomainDelegate callBackDelegate)
   場所 System.Data.Entity.Migrations.Design.ToolingFacade.Run(BaseRunner runner)
   場所 System.Data.Entity.Migrations.Design.ToolingFacade.ScaffoldInitialCreate(String language, String rootNamespace)
   場所 System.Data.Entity.Migrations.EnableMigrationsCommand.<>c__DisplayClass2.<.ctor>b__0()
   場所 System.Data.Entity.Migrations.MigrationsDomainCommand.Execute(Action command)
No Entity Framework provider found for the ADO.NET provider with invariant name 'System.Data.SQLite'. Make sure the provider is registered in the 'entityFramework' section of the application config file. See http://go.microsoft.com/fwlink/?LinkId=260882 for more information.

ConfigurationにSetSqlGeneratorを追加せずにAdd-Migrationをした場合のログ

No MigrationSqlGenerator found for provider 'System.Data.SQLite'. Use the SetSqlGenerator method in the target migrations configuration class to register additional SQL generators.

EF6.2を使っている場合に、_existingConnectionを更新する処理を行わない場合に発生する例外

System.ObjectDisposedException: '破棄されたオブジェクトにアクセスできません。
オブジェクト名 'SQLiteConnection' です。'

Configuration::AutomaticMigrationsEnabledをfalseのまま、migrator.Update()を実行したときに発生する例外

System.Data.Entity.Migrations.Infrastructure.AutomaticMigrationsDisabledException:
'Unable to update database to match the current model because there are pending changes and automatic migration is disabled.
Either write the pending model changes to a code-based migration or enable automatic migration.
Set DbMigrationsConfiguration.AutomaticMigrationsEnabled to true to enable automatic migration.'