次の方法で共有


カスタムコードファーストの規約

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

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

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

モデルベース慣例

このページでは、カスタム規則用の DbModelBuilder API について説明します。 この API は、ほとんどのカスタム規則を作成するのに十分である必要があります。 ただし、モデルベースの規則 (作成後に最終的なモデルを操作する規則) を作成して、高度なシナリオを処理する機能もあります。 詳細については、「Model-Based コンベンション」を参照してください。

 

私たちのモデル

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

    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 つの注意事項は、キーに複数のプロパティを指定する場合は、それらのプロパティの順序も指定する必要があるということです。 これを行うには、次のように 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 列と文字列 Name 列で構成される複合キーを持つモデルの型を構成します。 デザイナーでモデルを表示すると、次のようになります。

複合キー

プロパティ規則のもう 1 つの例は、モデル内のすべての DateTime プロパティを、datetime ではなく SQL Server の 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());
    }

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

 

カスタム属性

規則のもう 1 つの優れた用途は、モデルの構成時に新しい属性を使用できるようにすることです。 これを説明するために、文字列プロパティを 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 を構成できないためです。 これが発生した場合は、規則をより具体的にして、文字列ではないものを除外することができます。

上記の規則はカスタム属性の定義に適していますが、特に属性クラスのプロパティを使用する場合は、はるかに使いやすい別の API があります。

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

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

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

これを取得したら、属性にブール値を設定して、プロパティを 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 つは、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();
    }

このメソッドは型を受け取り、CamelCase の代わりにアンダースコアで小文字を使用する文字列を返します。 このモデルでは、これは 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();
    }

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 の例

これを回避し、いくつかの方法で既定の 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>();
    }