カスタム Code First 規約

Note

EF6 以降のみ - このページで説明する機能、API などは、Entity Framework 6 で導入されました。 以前のバージョンを使用している場合、一部またはすべての情報は適用されません。

Code First を使用すると、一連の規約を使用してクラスからモデルが計算されます。 既定の Code First 規約により、エンティティの主キーになるプロパティ、エンティティがマップされるテーブルの名前、および 10 進数列の有効桁数と小数点以下桁数の既定値が決定されます。

これらの既定の規約はモデルに最適ではない場合があり、データ注釈または Fluent API を使用して多数の個々のエンティティを構成することにより、回避する必要があります。 カスタム Code First 規約を使用すると、モデルの構成の既定値を提供する独自の規約を定義できます。 このチュートリアルでは、さまざまな種類のカスタム規約と、それぞれの作成方法について説明します。

モデルベースの規則

このページでは、カスタム規則のための DbModelBuilder API について説明します。 ほとんどのカスタム規約の作成には、この API で十分です。 ただし、作成後に最終的なモデルを操作する規約であるモデルベースの規約を作成して、高度なシナリオを処理することもできます。 詳しくは、「モデルベースの規約」をご覧ください。

 

モデル

まず、規約で使用できる単純なモデルを定義します。 次のクラスを、プロジェクトに追加します。

    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;

    public class ProductContext : DbContext
    {
        static ProductContext()
        {
            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
        }

        public DbSet<Product> Products { get; set; }
    }

    public class Product
    {
        public int Key { get; set; }
        public string Name { get; set; }
        public decimal? Price { get; set; }
        public DateTime? ReleaseDate { get; set; }
        public ProductCategory Category { get; set; }
    }

    public class ProductCategory
    {
        public int Key { get; set; }
        public string Name { get; set; }
        public List<Product> Products { get; set; }
    }

 

カスタム規約の導入

Key という名前のプロパティをエンティティ型の主キーとして構成する規約を書いてみましょう。

規約はモデル ビルダーで有効になっており、コンテキストで OnModelCreating をオーバーライドすることでアクセスできます。 ProductContext クラスを次のように更新します。

    public class ProductContext : DbContext
    {
        static ProductContext()
        {
            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
        }

        public DbSet<Product> Products { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Properties()
                        .Where(p => p.Name == "Key")
                        .Configure(p => p.IsKey());
        }
    }

これで、モデル内の Key という名前のプロパティが、その一部であるエンティティの主キーとして構成されます。

また、構成するプロパティの型でフィルター処理することで、規約をより限定的にすることもできます。

    modelBuilder.Properties<int>()
                .Where(p => p.Name == "Key")
                .Configure(p => p.IsKey());

これにより、Key という名前のプロパティは、整数の場合にのみ、エンティティの主キーとして構成されます。

IsKey メソッドの興味深い機能は、それが加法である点です。 つまり、複数のプロパティで IsKey を呼び出した場合、それらはすべて複合キーの一部になります。 これに関する 1 つの注意点として、1 つのキーに複数のプロパティを指定する場合は、それらのプロパティの順序も指定する必要があります。 これを行うには、次のように HasColumnOrder メソッドを呼び出します。

    modelBuilder.Properties<int>()
                .Where(x => x.Name == "Key")
                .Configure(x => x.IsKey().HasColumnOrder(1));

    modelBuilder.Properties()
                .Where(x => x.Name == "Name")
                .Configure(x => x.IsKey().HasColumnOrder(2));

このコードでは、モデルの型が、int の Key 列と string の Name 列で構成される複合キーを持つように構成されます。 デザイナーでモデルを表示すると、次のように表示されます。

composite Key

プロパティの規約のもう 1 つの例は、モデル内のすべての DateTime プロパティを、SQL Server で datetime ではなく datetime2 型にマップするように構成する場合です。 これは、次のようにして実現できます。

    modelBuilder.Properties<DateTime>()
                .Configure(c => c.HasColumnType("datetime2"));

 

規約クラス

規約を定義するもう 1 つの方法は、規約クラスを使用して規約をカプセル化する方法です。 規約クラスを使用する場合は、System.Data.Entity.ModelConfiguration.Conventions 名前空間の Convention クラスから継承する型を作成します。

次のようにして、前に示した datetime2 規約で規約クラスを作成できます。

    public class DateTime2Convention : Convention
    {
        public DateTime2Convention()
        {
            this.Properties<DateTime>()
                .Configure(c => c.HasColumnType("datetime2"));        
        }
    }

EF にこの規約を使用するように指定するには、OnModelCreating で Conventions コレクションにそれを追加します。チュートリアルに従っている場合は、次のようになります。

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Properties<int>()
                    .Where(p => p.Name.EndsWith("Key"))
                    .Configure(p => p.IsKey());

        modelBuilder.Conventions.Add(new DateTime2Convention());
    }

ご覧のように、規約のインスタンスを規約コレクションに追加します。 Convention から継承するのは、規約をグループ化したり、チームまたはプロジェクト間で共有したりするのに便利な方法です。 たとえば、組織のすべてのプロジェクトで使用する共通の規約セットを含むクラス ライブラリを作成できます。

 

カスタム属性

規約のもう 1 つの便利な使い方は、モデルを構成するときに新しい属性を使用できるようにすることです。 これを説明するため、String プロパティを非 Unicode としてマークするために使用できる属性を作成しましょう。

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class NonUnicode : Attribute
    {
    }

次に、この属性をモデルに適用する規約を作成します。

    modelBuilder.Properties()
                .Where(x => x.GetCustomAttributes(false).OfType<NonUnicode>().Any())
                .Configure(c => c.IsUnicode(false));

この規約を使用すると、NonUnicode 属性を任意の文字列プロパティに追加できます。つまり、データベースの列は nvarchar ではなく varchar として格納されます。

この規約で注意する点の 1 つは、NonUnicode 属性を文字列プロパティ以外のものに設定した場合、例外がスローされることです。 このようになるのは、文字列以外の型には IsUnicode を構成できないためです。 これが発生した場合は、規約をより限定的にして、文字列ではないものをすべてフィルターで除外できます。

上の規約はカスタム属性の定義で機能しますが、属性クラスのプロパティを使用する場合に特にいっそう使いやすいもう 1 つの API があります。

この例では、属性を更新して、それを IsUnicode 属性に変更します。次のようになります。

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    internal class IsUnicode : Attribute
    {
        public bool Unicode { get; set; }

        public IsUnicode(bool isUnicode)
        {
            Unicode = isUnicode;
        }
    }

このようにしたら、属性に bool を設定して、プロパティを Unicode にする必要があるかどうかを規約に示すことができます。 次のように、構成クラスの ClrProperty にアクセスすると、既にある規約でこれを行うことができます。

    modelBuilder.Properties()
                .Where(x => x.GetCustomAttributes(false).OfType<IsUnicode>().Any())
                .Configure(c => c.IsUnicode(c.ClrPropertyInfo.GetCustomAttribute<IsUnicode>().Unicode));

これでも十分に簡単ですが、規約 API の Having メソッドを使用すると、さらに簡潔な方法でこれを実現できます。 Having メソッドには Func<PropertyInfo, T> 型のパラメーターがあり、Where メソッドと同じように PropertyInfo を受け取りますが、オブジェクトが返されます。 返されたオブジェクトが null の場合、プロパティは構成されません。つまり、Where と同様にプロパティをフィルターで除外できますが、返されたオブジェクトをキャプチャして Configure メソッドに渡すこともできる点が異なります。 これは次のように動作します。

    modelBuilder.Properties()
                .Having(x => x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault())
                .Configure((config, att) => config.IsUnicode(att.Unicode));

Having メソッドは、カスタム属性の場合にだけ使用するのではなく、型やプロパティを構成するときにフィルター処理する必要があるすべての場所で役に立ちます。

 

型の構成

これまでの規約はすべてプロパティに関するものでしたが、規約 API にはモデルでの型の構成に関するもう 1 つの部分があります。 エクスペリエンスはこれまで見てきた規約に似ていますが、configure 内のオプションはプロパティ レベルではなくエンティティになります。

型レベルの規約が非常に便利な点の 1 つは、EF の既定値とは異なる既存のスキーマにマップするか、または異なる名前付け規則を持つ新しいデータベースを作成するかという、テーブルの名前付け規則を変更する場合です。 これを行うには、まず、モデル内の型に対する TypeInfo を受け取り、その型に対して必要なテーブル名を返すメソッドが必要です。

    private string GetTableName(Type type)
    {
        var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);

        return result.ToLower();
    }

このメソッドは、型を受け取り、小文字と、キャメル ケースではなくアンダースコアを使用する文字列を返します。 このモデルでは、これは ProductCategory クラスが ProductCategories ではなく、product_category という名前のテーブルにマップされることを意味します。

そのメソッドを作成したら、次のような規約でそれを呼び出すことができます。

    modelBuilder.Types()
                .Configure(c => c.ToTable(GetTableName(c.ClrType)));

この規約では、モデル内のすべての型が、GetTableName メソッドから返されるテーブル名にマップするように構成されます。 この規約は、Fluent API を使ってモデル内のエンティティごとに ToTable メソッドを呼び出すことと同等です。

これについて注意することの 1 つは、ToTable EF を呼び出すときは、テーブル名の正確な文字列を指定することであり、テーブル名を決定するときに通常行う複数形化は使用できません。 ここで示す規約でテーブル名が product_categories ではなく product_category になっているのはこのためです。 規約で独自に複数形化サービスを呼び出すことで、それを解決できます。

次のコードでは、EF6 で追加された依存関係解決機能を使用して、EF で使用された複数形化サービスを取得し、テーブル名を複数形にします。

    private string GetTableName(Type type)
    {
        var pluralizationService = DbConfiguration.DependencyResolver.GetService<IPluralizationService>();

        var result = pluralizationService.Pluralize(type.Name);

        result = Regex.Replace(result, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);

        return result.ToLower();
    }

Note

汎用バージョンの GetService は System.Data.Entity.Infrastructure.DependencyResolution 名前空間の拡張メソッドであり、それを使用するには、コンテキストに using ステートメントを追加する必要があります。

ToTable と継承

ToTable のもう 1 つの重要な側面は、型を特定のテーブルに明示的にマップする場合、EF で使用されるマッピング戦略を変更できることです。 継承階層内のすべての型に対して ToTable を呼び出し、上記のようにテーブルの名前として型名を渡す場合は、既定の Table-Per-Hierarchy (TPH) マッピング戦略を Table-Per-Type (TPT) に変更します。 これを説明する最善の方法は、具体的な例です。

    public class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Manager : Employee
    {
        public string SectionManaged { get; set; }
    }

既定では、従業員とマネージャーの両方が、データベース内の同じテーブル (Employees) にマップされます。 テーブルには、従業員とマネージャーの両方と、各行に格納されているインスタンスの種類を示す判別列が含まれます。 階層に対してテーブルが 1 つあるので、これは TPH マッピングです。 一方、両方のクラスで ToTable を呼び出すと、代わりに各型が独自のテーブルにマップされます。各型に独自のテーブルがあるので TPT とも呼ばれます。

    modelBuilder.Types()
                .Configure(c=>c.ToTable(c.ClrType.Name));

上記のコードは、次のようなテーブル構造にマップされます。

tpt Example

これを回避し、いくつかの方法で既定の TPH マッピングを維持できます。

  1. 階層内の型ごとに、同じテーブル名で ToTable を呼び出します。
  2. 階層の基底クラス (この例では従業員) でのみ ToTable を呼び出します。

 

実行する順番

規約は、Fluent API と同じように、最後に該当したものが使用されます。 つまり、同じプロパティの同じオプションを構成する 2 つの規約を書いた場合、最後のものが実行されます。 たとえば、次のコードでは、すべての文字列の最大長は 500 に設定されますが、モデル内の Name という名前のすべてのプロパティについては最大長を 250 に構成します。

    modelBuilder.Properties<string>()
                .Configure(c => c.HasMaxLength(500));

    modelBuilder.Properties<string>()
                .Where(x => x.Name == "Name")
                .Configure(c => c.HasMaxLength(250));

最大長を 250 に設定する規約は、すべての文字列を 500 に設定する規約の後にあるため、モデル内の Name という名前のすべてのプロパティの MaxLength は 250 になりますが、説明などの他の文字列は 500 になります。 この方法で規約を使用すると、モデル内の型またはプロパティの一般的な規約を指定してから、異なるサブセットに対してそれらをオーバーライドできます。

また、Fluent API とデータ注釈を使用して、特定のケースで規約をオーバーライドすることもできます。 上の例では、Fluent API を使用してプロパティの最大長を設定した場合、それを規約の前または後に置くことができます。これは、より一般的な構成規約ではなく、より具体的な Fluent API が使用されるためです。

 

組み込みの規約

カスタム規約は既定の Code First 規約の影響を受ける可能性があるので、別の規約の前または後に実行する規約を追加すると便利です。 これを行うには、派生した DbContext で Conventions コレクションの AddBefore メソッドと AddAfter メソッドを使用できます。 次のコードでは、前に作成した規約クラスを、組み込みのキー検出規約の前に実行されるように追加しています。

    modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());

これは、組み込み規約の前または後に実行する必要がある規約を追加するときに最も使用されます。組み込み規約の一覧については、「System.Data.Entity.ModelConfiguration.Conventions 名前空間」をご覧ください。

また、モデルに適用したくない規約を削除することもできます。 規約を削除するには、Remove メソッドを使用します。 PluralizingTableNameConvention を削除する例を次に示します。

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
    }