Entity Frameworkをチーム開発で使ってみました

ASP.NET Advent Calendar 2015 - Qiitaの7日目のエントリです。


最近、初めてEntity Frameworkを使ったチーム開発を行いましたので、得られた気づきを備忘録として整理しています。
Entity Frameworkの導入を考えておられる方々の一助になれば幸いです。

どんな開発だったのか

ASP.NET MVC 5、Silverlight 5 + WCF Data ServicesのWebアプリケーション開発でした。サーバのORマッパーとしてEntity Framework 5を使い、SQL Server 2012にアクセスするアーキテクチャです。
少々複雑な開発フローで、まずプロトタイプ開発と詳細設計を並行させ、詳細設計の内容をプロトタイプへ反映させ、最後に結合テストを通過させてリリース版を完成させる流れでした。
開発者は計10名(自身も含めて7名がC#未経験)で、ウォーターフォール型の開発にどっぷり浸かった面々でした。

Entity Framework

言わずと知れた.NET Framework(3.5 SP1以降)のORマッパーです。ASP.NETではデファクトスタンダードかと思います。

開発初期(プロトタイプ開発)

プロトタイプ開発ではモデルの追加・変更・削除が頻繁に発生するため、コードファーストを採用しました。

コードファースト

ウォーターフォール型開発にありがちな、設計でテーブル定義が固められ、実装ではDBにCREATE TABLEを流すところからスタートする方法とは、真逆のやり方です。
コードファーストではその名の通り、まず開発者がモデルクラス(POCO)を作成することで、そのクラスからDBとテーブルが自動生成されます。DBのデータに律速されないことで、高速な開発を支援する考え方です。
まずDBイニシャライザでDropCreateDatabaseIfModelChangesを継承させるようにします。これにより、モデルクラスが変更されるとDBが再作成されるようになります。
Seedメソッド内でテストデータを投入させることで、DBの再作成後もスムーズに動作確認できます。
この段階では、モデルクラスの改廃を行うたびにマイグレーションさせていたのでは手間なので、マイグレーションは使用していません。

// EmployeesとDivisionsを持つDBコンテキストを定義
public class SampleDbContext : DbContext
{
    public DbSet<Employee> Employees { get; set; }
    public DbSet<Division> Divisions { get; set; }
}
// イニシャライザでDropCreateDatabaseIfModelChangesを継承
public class SampleDbInitializer : DropCreateDatabaseIfModelChanges<SampleDbContext>
{
    protected override void Seed(SampleDbContext context)
    {
        base.Seed(context);

        var Employees = new List<Employee>
        {
            new Employee { Id = 1, Name="荒岩", DivisionId = 2 }, 
            new Employee { Id = 2, Name="田中", DivisionId = 2 }
        };
        employees.ForEach(e => context.Employees.Add(e));

        var Divisions = new List<Division>
        {
            new Division { Id = 1, Name="経理課" }, 
            new Division { Id = 2, Name="営業課" }
        };
        divisions.ForEach(d => context.Divisions.Add(d));

        context.SaveChanges();
    }
}

レイヤ単位ではなく機能単位(画面とAPI)で開発者の担当を決めたことで関心を持つモデルクラスが概ね分離されたこと、各開発者PCにSQL Server 2012 Expressをインストールしたことが幸いして、アドホックにモデルを改廃したりDBを作成・削除しても何ら問題とはなりませんでした。

開発中期(ブラッシュアップ)

プロトタイプ開発と並行して進めていた詳細設計がクローズしたので、プロトタイプのブラッシュアップに入りました。
リリースレベルに近づけるために、モデルにも下記のような要望が続々とやってきます。

  • カラムの長さが指定されてなくて、テーブルサイズが見積もれない。(フォームの入力制限も画面でいちいちかけるの?)
  • あれ?キーの設定ってどうするのだろう?(Entity Frameworkは"Id"が付くカラムは自動的にキーにされるので、"Id"と付かないカラムをキーにする時に初めて気づく)
  • カラムの順番が変なので直して。

モデルとDBの関連付けを詳細に設定するためには、モデルクラスに属性を付与する方法とFluent APIを使う方法があります。モデルクラスの修正だけで完結するわかりやすさと手っ取り早さを優先して、前者の属性付与を採用しました。

モデルクラスへの属性付与

テーブル名、主キー属性、必須項目、最小長・最大長、カラム順、外部キーを設定しています。

// Employeeモデルクラス
[Table("EmployeeTable")] 
public class Employee
{
    [Key]
    [Column(Order = 0)]
    public int Id { get; set; }

    [Required]
    [MinLength(1), MaxLength(30)] 
    [Column(Order = 1)]
    public String Name { get; set; }

    [Column(Order = 2)]
    public int? DivisionId { get; set; }
    [ForeignKey("DivisionId")]
    public virtual Division Division {get; set; }
}

開発後期(テスト)

ブラッシュアップも概ねクリアし、いよいよテスト段階へと移ってまいります。
これまで通り自動マイグレーションでこの段階を迎えてしまい、いろいろな問題が起こりました。

  • シナリオに沿った結合テスト中にデータが初期化されてしまっては、最初からやり直しになってしまう。
  • データに起因した不具合の調査中に、最新版のソースコードを取得・実行したらモデルの変更→データのロスによって、不具合が再現できなくなってしまった。

そこで、テストデータを保全する目的で、マイグレーションを有効化しました。

マイグレーション

まず、モデルクラスが改廃されても今のDBを削除されないようにします。
CreateDatabaseIfNotExistsを継承させることで、DBが存在しない場合にのみ作成されるようにします。

public class SampleDbInitializer : CreateDatabaseIfNotExists<SampleDbContext>
{
    // ・・・(省略)・・・
}

次に、マイグレーションを有効化して、マイグレーションファイルを生成します。
Visual Studioのパッケージマネージャコンソールを開いて、下記のコマンドを実行してマイグレーションを有効化します。

PM> Enable-Migrations -ProjectName SampleWebApplication

MigrationsフォルダとConfiguration.csが生成されます。Configurationにて自動マイグレーションを無効化されているか確認します。

internal sealed class Configuration : DbMigrationsConfiguration<>
{
    public Configuration()
    {
        AutomaticMigrationsEnabled = false; // falseになっていれば手動マイグレーション
        ContextKey = "SampleApplication.Data.SampleDbContext";
    }
}

さらに次のコマンドを実行することで初回のマイグレーションファイル(タイムスタンプ_InitialCreate.cs)が生成されます。

PM> Add-Migrations InitialCreate -ProjectName SampleWebApplication

モデルクラスが変更するたびに、同じようにAdd-Migrationを実行します。InitialCreateからの差分となるマイグレーションファイル(タイムスタンプ_AddSalaryToEmployee.cs)が生成されます。

public class Employee
{
    // ・・・(省略)・・・

    // Salaryを追加
    [Column(Order = 3)]
    public int Salary { get; set; }
}
PM> Add-Migration AddSalaryToEmployee -ProjectName SampleWebApplication
// 生成されたマイグレーションファイル
public partial class AddSalaryToEmployee : DbMigration
{
    public override void Up()
    {
        AddColumn("dbo.Employee", "Salary", c => c.Int(nullable: false));
    }
    public override void Down()
    {
        DropColumn("dbo.Employee", "Salary");
    }
}

自動でモデルクラスがDBへ反映されないため、Update-Databaseコマンドを実行することでDBが更新されます。

  • 最新化したい場合
PM> Update-Database -ProjectName SampleWebApplication
PM> Update-Database -TargetMigration:AddSalaryToEmployee -ProjectName SampleWebApplication

DBでは「__MigrationHistory」というテーブルが作成され、現時点までに適用されているマイグレーションの一覧が保持されています。
f:id:ohke:20151206211859p:plain
モデルクラスとDBに差異がある状態でアクセスすると、AutomaticMigrationsDisabledExceptionがthrowされます。
f:id:ohke:20151206210910p:plain
モデルクラスとDBの差異を自らコマンドで埋めたり、マイグレーションの名前を考えたりする必要が生じるため、開発者にとっての手間は少し増えてしまいます。またデータベースファーストの開発では存在しなかったマイグレーションの概念を意識しないといけないため、敷居も上がります。手順や命名規則を定めて共有するなど、混乱を招かないように努めました。

まとめ

「開発初期はモデルの追加・変更・削除のスピードを重視し、リリースに近づくに連れてデータの保全を重視する」という、概ね教科書通りの進め方でした。
開発者の技術ギャップなどで展開や運用に悩むこともありましたが、DBやテーブルの定義やデータがネックとなって、アプリケーションの修正が遅れるという事態は劇的に減ったかと思います。