fluent API - プロパティと型の構成とマッピング

Entity Framework Code First を使う場合、既定の動作は、EF に組み込まれた規約のセットを使って POCO クラスをテーブルにマップすることです。 ただし、そのような規約に従うことができない、または従いたくない場合には、規約で決められている以外のものにエンティティをマップする必要があります。

規約以外のものを使うように EF を構成する方法は主に 2 つあります。つまり、注釈または EF の fluent API です。 注釈は fluent API 機能の一部にのみ対応するので、注釈を使っても実現できないマッピング シナリオがあります。 この記事では、fluent API を使ってプロパティを構成する方法について説明します。

Code First fluent API にアクセスする最も一般的な例は、派生した DbContextOnModelCreating メソッドをオーバーライドする場合です。 次のサンプルは、fluent API を使ってさまざまなタスクを実行する方法を示すために設計されています。このコードをコピーして自分のモデルに合わせてカスタマイズすることができます。そのまま使用できるモデルを確認する場合は、この記事の最後に掲載するものを参照してください。

モデル全体の設定

既定のスキーマ (EF6 以降)

EF6 以降、DbModelBuilder で HasDefaultSchema メソッドを使い、すべてのテーブル、ストアド プロシージャなどに使うデータベース スキーマを指定できるようになりました。異なるスキーマを明示的に構成したオブジェクトがある場合に、この既定の設定は上書きされます。

modelBuilder.HasDefaultSchema("sales");

カスタム規約 (EF6 以降)

EF6 以降、独自の規約を作成し、Code First に含まれているものを補完できるようになりました。 詳細については、「カスタム Code First 規則」を参照してください。

プロパティ マッピング

Property メソッドは、エンティティや複合型に属する各プロパティの属性を構成するために使われます。 Property メソッドは、特定のプロパティの構成オブジェクトを取得するために使われます。 構成オブジェクトのオプションは、構成する型に固有のものです。たとえば、IsUnicode は文字列プロパティでのみ使用できます。

主キーを構成する

Entity Framework の主キーの規約は次のとおりです。

  1. クラスには、名前が "ID" または "Id" であるプロパティを定義する
  2. または、クラス名の後に "ID" または "Id" を付ける

主キーとなるプロパティを明示的に設定するには、HasKey メソッドを使うことができます。 次の例では、HasKey メソッドを使い、OfficeAssignment 型の InstructorID の主キーを構成しています。

modelBuilder.Entity<OfficeAssignment>().HasKey(t => t.InstructorID);

複合主キーを構成する

次の例では、DepartmentID と Name のプロパティを Department 型の複合主キーとして構成しています。

modelBuilder.Entity<Department>().HasKey(t => new { t.DepartmentID, t.Name });

数値主キーの ID をオフにする

次の例では、DepartmentID プロパティを System.ComponentModel.DataAnnotations.DatabaseGeneratedOption.None に設定し、値がデータベースによって生成されないことを示しています。

modelBuilder.Entity<Department>().Property(t => t.DepartmentID)
    .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

プロパティの最大長を指定する

次の例では、Name プロパティは 50 文字以下にする必要があります。 50 文字を超える値にすると、DbEntityValidationException 例外が発生します。 Code First を使ってこのモデルからデータベースを作成する場合、Name 列の最大長も 50 文字に設定されます。

modelBuilder.Entity<Department>().Property(t => t.Name).HasMaxLength(50);

Property を Required に構成する

次の例では、Name プロパティが必須です。 Name を指定しない場合、DbEntityValidationException 例外が発生します。 Code First を使ってこのモデルからデータベースを作成する場合、通常、このプロパティを格納するために使われる列は null 非許容です。

Note

ときには、プロパティが必要であっても、データベース内の列を null 非許容にすることができない場合があります。 たとえば、TPH 継承戦略の使用時には、複数の型のデータが 1 つのテーブルに格納されます。 派生型に必須プロパティが含まれる場合、階層内のすべての型がこのプロパティを持つわけではないため、列を null 非許容にすることができません。

modelBuilder.Entity<Department>().Property(t => t.Name).IsRequired();

1 つまたは複数のプロパティに対して Index を構成する

Note

EF6.1 以降のみ - Index 属性は、Entity Framework 6.1 で導入されました。 以前のバージョンを使おうとしている場合、このセクションの情報は当てはまりません。

インデックスの作成は fluent API のネイティブではサポートされていませんが、fluent API を介して IndexAttribute のサポートを使うことができます。 Index 属性を処理するには、モデルにモデル注釈を含めます。これは後の工程のパイプラインでデータベース内のインデックスに変換されます。 fluent API を使って、これらと同じ注釈を手動で追加することができます。

この最も簡単な方法は、新しいインデックスのすべての設定を含む IndexAttribute のインスタンスを作成することです。 次に IndexAnnotation のインスタンスを作成します。これは EF 固有の型であり、IndexAttribute の設定が EF モデルに格納できるモデル注釈に変換されます。 これらを fluent API の HasColumnAnnotation メソッドに渡すには、注釈に Index という名前を指定します。

modelBuilder
    .Entity<Department>()
    .Property(t => t.Name)
    .HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute()));

IndexAttribute で使用できる設定の詳細な一覧については、「Code First のデータ注釈」の「インデックス」セクションを参照してください。 これには、インデックス名のカスタマイズ、一意のインデックスの作成、複数列インデックスの作成が含まれます。

1 つのプロパティに複数のインデックス注釈を指定するには、IndexAnnotation のコンストラクターに IndexAttribute の配列を渡します。

modelBuilder
    .Entity<Department>()
    .Property(t => t.Name)
    .HasColumnAnnotation(
        "Index",  
        new IndexAnnotation(new[]
            {
                new IndexAttribute("Index1"),
                new IndexAttribute("Index2") { IsUnique = true }
            })));

CLR プロパティをデータベースの列にマップしないように指定する

CLR 型のプロパティをデータベース内の列にマップしないように指定する方法の例を次に示します。

modelBuilder.Entity<Department>().Ignore(t => t.Budget);

CLR プロパティをデータベース内の特定の列にマップする

Name CLR プロパティを DepartmentName データベース列にマップする例を次に示します。

modelBuilder.Entity<Department>()
    .Property(t => t.Name)
    .HasColumnName("DepartmentName");

モデルに定義されていない外部キーの名前を変更する

CLR 型に外部キーを定義しないことを選んだ場合で、なおかつデータベースにおける名前を指定したい場合は、次のようにします。

modelBuilder.Entity<Course>()
    .HasRequired(c => c.Department)
    .WithMany(t => t.Courses)
    .Map(m => m.MapKey("ChangedDepartmentID"));

文字列プロパティが Unicode コンテンツをサポートするかどうかを構成する

既定では、文字列は Unicode (SQL Server では nvarchar) です。 IsUnicode メソッドを使うと、文字列を varchar 型にする必要があることを指定できます。

modelBuilder.Entity<Department>()
    .Property(t => t.Name)
    .IsUnicode(false);

データベース列のデータ型を構成する

HasColumnType メソッドを使うと、同じ基本型の異なる表現にマップすることができます。 このメソッドを使っても、実行時にデータの変換を実行することはできません。 列を varchar に設定する方法としては、データベースに依存しない IsUnicode が推奨されていることに注意してください。

modelBuilder.Entity<Department>()   
    .Property(p => p.Name)   
    .HasColumnType("varchar");

複合型のプロパティを構成する

複合型のスカラー プロパティを構成するには 2 つの方法があります。

ComplexTypeConfiguration で Property を呼び出すことができます。

modelBuilder.ComplexType<Details>()
    .Property(t => t.Location)
    .HasMaxLength(20);

また、ドット表記を使って複合型のプロパティにアクセスすることもできます。

modelBuilder.Entity<OnsiteCourse>()
    .Property(t => t.Details.Location)
    .HasMaxLength(20);

オプティミスティック同時実行制御トークンとして使われるプロパティを構成する

エンティティ内のプロパティが同時実行制御トークンを表すことを指定するには、ConcurrencyCheck 属性または IsConcurrencyToken メソッドを使います。

modelBuilder.Entity<OfficeAssignment>()
    .Property(t => t.Timestamp)
    .IsConcurrencyToken();

また、IsRowVersion メソッドを使い、データベース内の行バージョンになるようにプロパティを構成することもできます。 プロパティを行バージョンとして設定すると、そのプロパティは自動的にオプティミスティック コンカレンシー トークンとして構成されます。

modelBuilder.Entity<OfficeAssignment>()
    .Property(t => t.Timestamp)
    .IsRowVersion();

型マッピング

クラスが複合型であることを指定する

慣例により、主キーが指定されていない型は複合型として扱われます。 Code First によって複合型が検出されないシナリオもあります (たとえば、ID というプロパティがあっても、それを主キーにしない場合など)。 このような場合は、fluent API を使い、型が複合型であることを明示的に指定します。

modelBuilder.ComplexType<Details>();

CLR エンティティ型をデータベースのテーブルにマップしないことを指定する

CLR 型をデータベース内のテーブルへのマップ対象から除外する方法の例を次に示します。

modelBuilder.Ignore<OnlineCourse>();

データベース内の特定のテーブルにエンティティ型をマップする

Department のすべてのプロパティは、t_ Department というテーブルの列にマップされます。

modelBuilder.Entity<Department>()  
    .ToTable("t_Department");

次のようにスキーマ名を指定することもできます。

modelBuilder.Entity<Department>()  
    .ToTable("t_Department", "school");

Table-Per-Hierarchy (TPH) の継承をマップする

TPH マッピング シナリオでは、継承階層のすべての型が 1 つのテーブルにマップされます。 各行の型を識別するために、識別子列が使われます。 Code First でモデルを作成する場合、TPH は継承階層に参加する型の既定の戦略です。 既定では、テーブルに "Discriminator" という名前の識別列が追加され、階層内の各型の CLR 型名が識別子値に使われます。 fluent API を使い、既定の動作を変更することができます。

modelBuilder.Entity<Course>()  
    .Map<Course>(m => m.Requires("Type").HasValue("Course"))  
    .Map<OnsiteCourse>(m => m.Requires("Type").HasValue("OnsiteCourse"));

Table-Per-Type (TPT) の継承をマップする

TPT マッピング シナリオでは、すべての型が個別のテーブルにマップされます。 基本データ型または派生型のみに属するプロパティは、その型にマップされたテーブルに格納されます。 派生型にマップされるテーブルには、派生テーブルと基本テーブルを結合する外部キーも格納されます。

modelBuilder.Entity<Course>().ToTable("Course");  
modelBuilder.Entity<OnsiteCourse>().ToTable("OnsiteCourse");

Table-Per-Concrete Class (TPC) の継承をマップする

TPC マッピング シナリオでは、階層内のすべての非抽象型が個々のテーブルにマップされます。 派生クラスにマップされたテーブルは、データベース内の基底クラスにマップされたテーブルとはリレーションシップがありません。 継承されたプロパティを含むクラスのすべてのプロパティは、対応するテーブルの列にマップされます。

MapInheritedProperties メソッドを呼び出して、各派生型を構成します。 MapInheritedProperties により、基底クラスから継承されたすべてのプロパティが、派生クラスのテーブルの新しい列に再マップされます。

Note

TPC の継承階層に参加しているテーブルは主キーを共有しないため、ID シードが同じデータベース生成値がある場合、サブクラスにマップされているテーブルに挿入すると、エンティティ キーが重複することに注意してください。 この問題を解決するには、テーブルごとに異なる初期シード値を指定するか、主キーのプロパティで ID をオフにします。 ID は、Code First を使う場合の整数キー プロパティの既定値です。

modelBuilder.Entity<Course>()
    .Property(c => c.CourseID)
    .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

modelBuilder.Entity<OnsiteCourse>().Map(m =>
{
    m.MapInheritedProperties();
    m.ToTable("OnsiteCourse");
});

modelBuilder.Entity<OnlineCourse>().Map(m =>
{
    m.MapInheritedProperties();
    m.ToTable("OnlineCourse");
});

エンティティ型のプロパティをデータベース内の複数のテーブルにマップする (エンティティ分割)

エンティティ分割を使うと、エンティティ型のプロパティを複数のテーブルに分散させることができます。 次の例では、Department エンティティが Department と DepartmentDetails という 2 つのテーブルに分割されています。 エンティティ分割によって Map メソッドが複数回呼び出され、プロパティのサブセットが特定のテーブルにマップされます。

modelBuilder.Entity<Department>()
    .Map(m =>
    {
        m.Properties(t => new { t.DepartmentID, t.Name });
        m.ToTable("Department");
    })
    .Map(m =>
    {
        m.Properties(t => new { t.DepartmentID, t.Administrator, t.StartDate, t.Budget });
        m.ToTable("DepartmentDetails");
    });

複数のエンティティ型をデータベース内の 1 つのテーブルにマップする (テーブル分割)

主キーを共有する 2 つのエンティティ型を 1 つのテーブルにマップする例を次に示します。

modelBuilder.Entity<OfficeAssignment>()
    .HasKey(t => t.InstructorID);

modelBuilder.Entity<Instructor>()
    .HasRequired(t => t.OfficeAssignment)
    .WithRequiredPrincipal(t => t.Instructor);

modelBuilder.Entity<Instructor>().ToTable("Instructor");

modelBuilder.Entity<OfficeAssignment>().ToTable("Instructor");

ストアド プロシージャの挿入、更新、削除にエンティティ型をマップする (EF6 以降)

EF6 以降では、挿入、更新、削除のストアド プロシージャを使うためにエンティティをマップすることができます。 詳細については、ストアドプロシージャの挿入、更新、および削除の Code First に関する記事を参照してください。

サンプルで使われているモデル

このページのサンプルには、次の Code First モデルが使われています。

using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
// add a reference to System.ComponentModel.DataAnnotations DLL
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System;

public class SchoolEntities : DbContext
{
    public DbSet<Course> Courses { get; set; }
    public DbSet<Department> Departments { get; set; }
    public DbSet<Instructor> Instructors { get; set; }
    public DbSet<OfficeAssignment> OfficeAssignments { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // Configure Code First to ignore PluralizingTableName convention
        // If you keep this convention then the generated tables will have pluralized names.
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
    }
}

public class Department
{
    public Department()
    {
        this.Courses = new HashSet<Course>();
    }
    // Primary key
    public int DepartmentID { get; set; }
    public string Name { get; set; }
    public decimal Budget { get; set; }
    public System.DateTime StartDate { get; set; }
    public int? Administrator { get; set; }

    // Navigation property
    public virtual ICollection<Course> Courses { get; private set; }
}

public class Course
{
    public Course()
    {
        this.Instructors = new HashSet<Instructor>();
    }
    // Primary key
    public int CourseID { get; set; }

    public string Title { get; set; }
    public int Credits { get; set; }

    // Foreign key
    public int DepartmentID { get; set; }

    // Navigation properties
    public virtual Department Department { get; set; }
    public virtual ICollection<Instructor> Instructors { get; private set; }
}

public partial class OnlineCourse : Course
{
    public string URL { get; set; }
}

public partial class OnsiteCourse : Course
{
    public OnsiteCourse()
    {
        Details = new Details();
    }

    public Details Details { get; set; }
}

public class Details
{
    public System.DateTime Time { get; set; }
    public string Location { get; set; }
    public string Days { get; set; }
}

public class Instructor
{
    public Instructor()
    {
        this.Courses = new List<Course>();
    }

    // Primary key
    public int InstructorID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
    public System.DateTime HireDate { get; set; }

    // Navigation properties
    public virtual ICollection<Course> Courses { get; private set; }
}

public class OfficeAssignment
{
    // Specifying InstructorID as a primary
    [Key()]
    public Int32 InstructorID { get; set; }

    public string Location { get; set; }

    // When Entity Framework sees Timestamp attribute
    // it configures ConcurrencyCheck and DatabaseGeneratedPattern=Computed.
    [Timestamp]
    public Byte[] Timestamp { get; set; }

    // Navigation property
    public virtual Instructor Instructor { get; set; }
}