MySQL で Entity Framework のマイグレーション

MySQL で Entity Framework コードファーストのマイグレーションを利用してみました。

利用した際のライブラリのバージョンは次のとおりです。

  • Entity Framework: 6.1.1
  • MySQL.ConnectorNET.Entity: 6.8.3.2

結論から書くと、設定をちゃんと書けば、使えるレベルになってきてるな、というところです。以下、ASP.NET MVC で簡単な本の情報を扱うアプリケーションの作成を例に書いていきます。

まずは、プロジェクトの作成から。プロジェクト名は MysqlCodeFirstWithNoAuth02 として、「認証なし」なプロジェクトとします。

最初に NuGet から Entity Framework と MySQL.ConnectorNET.Entity を導入します(MySQL.ConnectorNET.Entity は mysql で検索すると出てきます)。

次に、アプリケーションで利用する MySQL のユーザーの作成とアプリケーション用の DB への権限付与をしておきます(DB 名: efcfSample02, DB ユーザー名: efcf01, DB ユーザーのパスワード: ******** とします)。
MySQL Command Line Client の例: grant all on efcfSample02.* to 'efcf01'@'localhost' identified by '********';

次に、プロジェクトのルートにある Web.config です。

...
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=自動生成された値" requirePermission="false" />
  </configSections>
  <connectionStrings>
    <add name="BooksDb" connectionString="server=localhost;database=efcfSample02;uid=efcf01;password=********;charset=utf8" providerName="MySql.Data.MySqlClient" />
  </connectionStrings>
  <appSettings>
...
  </runtime>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
      <provider invariantName="MySql.Data.MySqlClient" type="MySql.Data.MySqlClient.MySqlProviderServices, MySql.Data.Entity.EF6" />
    </providers>
  </entityFramework>
<system.data>
    <DbProviderFactories>
      <remove invariant="MySql.Data.MySqlClient" />
      <add name="MySQL Data Provider" invariant="MySql.Data.MySqlClient" description=".Net Framework Data Provider for MySQL" type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=6.8.3.0, Culture=neutral, PublicKeyToken=自動生成された値" />
    </DbProviderFactories>
  </system.data>
</configuration>

次のことを行っています。

  • connectionStrings の設定
  • entityFramework の providers への MySql.Data.MySqlClient の追加(MySQL の Chapter 10 EF 6 Support を参照)
  • system.data の DbProviderFactories の remove にある name="MySQL Data Provider" の削除(自動追加されるんですが、文法違反になっているという。。。バグですね。。。そのうち修正されるでしょう)

次にモデルを作成します。Models フォルダに Book クラスを追加します。

using System.ComponentModel.DataAnnotations;

namespace MysqlCodeFirstWithNoAuth02.Models
{
    public class Book
    {
        public int Id { get; set; }
        [Required]
        [MaxLength(50, ErrorMessage = "タイトルの文字数は 50 文字までです。")]
        public string Title { get; set; }
    }
}

次に DbContext です。プロジェクトに DAL フォルダを作成し、DAL フォルダに BooksContext クラスを追加します。

using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Migrations.History;

using MySql.Data.Entity;
using MySql.Data.MySqlClient;

using MysqlCodeFirstWithNoAuth02.Models;

namespace MysqlCodeFirstWithNoAuth02.DAL
{
    [DbConfigurationType(typeof(MysqlConfiguration))]
    public class BooksContext : DbContext
    {
        public BooksContext() : base("BooksDb") { }

        public DbSet<Book> Books { get; set; }
    }

    public class MysqlConfiguration : DbConfiguration
    {
        public MysqlConfiguration()
        {
            AddDependencyResolver(new MySqlDependencyResolver());
            SetProviderFactory(MySqlProviderInvariantName.ProviderName, new MySqlClientFactory());
            SetDefaultConnectionFactory(new MySqlConnectionFactory());
            SetMigrationSqlGenerator(MySqlProviderInvariantName.ProviderName, () => new MySqlMigrationSqlGenerator());
            SetProviderServices(MySqlProviderInvariantName.ProviderName, new MySqlProviderServices());
            SetProviderFactoryResolver(new MySqlProviderFactoryResolver());
            SetManifestTokenResolver(new MySqlManifestTokenResolver());

            // __migrationHistory テーブルのデフォルト設定の変更
            SetHistoryContext("MySql.Data.MySqlClient", (connection, defaultSchema) => new MyHistoryContext(connection, defaultSchema));
        }
    }

    public class MyHistoryContext : HistoryContext
    {
        public MyHistoryContext(DbConnection dbConnection, string defaultSchema)
            : base(dbConnection, defaultSchema)
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // 複合キー(MigrationId, ContextKey)の長さがデフォルトでは大きすぎるので設定を変更する
            // 基底クラスの OnModelCreating(modelBuilder) でデフォルト設定を行っているので、base.OnModelCreating() の後に行うこと
            modelBuilder.Entity<HistoryRow>().Property(h => h.MigrationId).HasMaxLength(100).IsRequired();
            modelBuilder.Entity<HistoryRow>().Property(h => h.ContextKey).HasMaxLength(200).IsRequired();
        }
    }
}

ここで行っているのが、MySQL 用の設定です。コメントにも書いていますが、マイグレーション用のデータを保存するテーブルの複合キーのデフォルトの長さが MySQL の制限にひっかかってしまうので、制限に引っかからないように最大長を設定しています(参考: Entity Framework with MySql and Migrations failing because “max key length is 767 bytes”)。

これでマイグレーションの準備ができました。
パッケージマネージャ コンソールで次のコマンドを入力します。
Enable-Migrations
Add-Migration InitialCreate
Update-Database

これで MySQL のデータベースが作成されて、テーブルも作成されています。
MySQL Command Line Client でテーブルの情報を確認したものが次のものです。

InitialCreate を反映したデータベースのテーブル情報
InitialCreate を反映したデータベース

次にモデルの Book クラスに TitleYomigana を追加します(ついでに、このあとのアプリケーションの表示用に Display 属性も追加しています)。

using System.ComponentModel.DataAnnotations;

namespace MysqlCodeFirstWithNoAuth02.Models
{
    public class Book
    {
        public int Id { get; set; }
        [Required]
        [Display(Name="書名")]
        [MaxLength(50, ErrorMessage = "タイトルの文字数は 50 文字までです。")]
        public string Title { get; set; }
        [Required]
        [Display(Name="よみがな")]
        [MaxLength(100, ErrorMessage = "よみがなの文字数は 100 文字までです。")]
        public string TitleYomigana { get; set; }
    }
}

パッケージマネージャ コンソールで次のコマンドを入力します。
Add-Migration AddBook_TitleYomigana
Update-Database

これで MySQL のテーブルに反映されています。
MySQL Command Line Client でテーブルの情報を確認したものが次のものです。

AddBook_TitleYomigana を反映したデータベースのテーブル情報
AddBook_TitleYomigana を反映したデータベース

ということで、マイグレーションによる「DB 作成、テーブル作成、テーブルへの変更の反映」が問題なく行えています。なお、「Update-Database -TargetMigration: InitialCreate」コマンドを入力することで、データベースが InitialCreate の時点に戻ることも確認しました。

以降は、ついでということで、「一覧機能」「データ追加機能」まで実装してみます。

DAL フォルダに IBooksRepository インターフェイスを追加します。

using System;
using System.Linq;
using System.Threading.Tasks;

using MysqlCodeFirstWithNoAuth02.Models;

namespace MysqlCodeFirstWithNoAuth02.DAL
{
    public interface IBooksRepository : IDisposable
    {
        IQueryable<Book> FindBooks();
        Task<Book> GetBookAsync(int id);
        Book AddBook(Book book);
        void UpdateBook(Book book);
        void DeleteBook(int id);

        Task SaveAsync();
    }
}

DAL フォルダに BooksRepository クラスを追加します。

using System;
using System.Linq;
using System.Threading.Tasks;

using MysqlCodeFirstWithNoAuth02.Models;

namespace MysqlCodeFirstWithNoAuth02.DAL
{
    public class BooksRepository : IBooksRepository
    {
        private BooksContext _context;

        public BooksRepository()
        {
            _context = new BooksContext();
        }

        #region IBooksRepository メンバー

        public IQueryable<Book> FindBooks()
        {
            IQueryable<Book> query = _context.Books.AsNoTracking().OrderBy(k => k.Id);
            return query;
        }

        public Task<Book> GetBookAsync(int id)
        {
            throw new NotImplementedException();
        }

        public Book AddBook(Book book)
        {
            return _context.Books.Add(book);
        }

        public void UpdateBook(Book book)
        {
            throw new NotImplementedException();
        }

        public void DeleteBook(int id)
        {
            throw new NotImplementedException();
        }

        public async Task SaveAsync()
        {
            await _context.SaveChangesAsync();
        }

        #endregion

        #region IDisposable メンバー

        private bool _disposed = false;

        /// <summary>
        /// リソースの開放を行います。
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// リソースの開放を行います。
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (_disposed) return;

            _disposed = true;

            if (disposing)
            {
                // マネージ リソースの解放処理
            }
            // アンマネージ リソースの解放処理
            _context.Dispose();
        }

        #endregion

        /// <summary>
        /// デストラクタ
        /// </summary>
        ~BooksRepository()
        {
            Dispose(false);
        }
    }
}

DB への保存を 非同期メソッドで行うようにしています。

Controllers フォルダの HomeController を修正します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;

using MysqlCodeFirstWithNoAuth02.DAL;
using MysqlCodeFirstWithNoAuth02.Models;

namespace MysqlCodeFirstWithNoAuth02.Controllers
{
    public class HomeController : Controller
    {
        private readonly IBooksRepository _respository;

        public HomeController() : this(new BooksRepository()) { }
        public HomeController(BooksRepository repository)
        {
            _respository = repository;
        }

        public ActionResult Index()
        {
            return View(_respository.FindBooks());
        }

        public ActionResult Create()
        {
            return View();
        }

        [HttpPost]
        public async Task<ActionResult> Create(Book book)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    _respository.AddBook(book);
                    await _respository.SaveAsync();
                    return RedirectToAction("Index");
                }

                return View(book);
            }
            catch
            {
                return View(book);
            }
        }

        public ActionResult About()
        {
            ViewBag.Message = "Your application description page.";

            return View();
        }

        public ActionResult Contact()
        {
            ViewBag.Message = "Your contact page.";

            return View();
        }
    }
}

Index アクションを右クリックし、「ビューの追加」を選択して、テンプレートを List、モデルクラスを Book にして、「追加」ボタンをクリックし、ビューを置き換えます。

Create アクションを右クリックし、「ビューの追加」を選択して、テンプレートを Create、モデルクラスを Book にして、「追加」ボタンをクリックし、ビューを作成します。

次の図は、これを動かして、データを追加してみた画面です。

データ追加後の一覧表示画面
データ追加後の一覧表示

リポジトリで非同期メソッドの SaveChangesAsync を使いましたが、問題なく動いています 🙂


1 thought on “MySQL で Entity Framework のマイグレーション

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です