EF Core 配置模型

蘆薈柚子茶 發表於 2022-05-13

0 前言

本文的第一節,會概述配置模型的作用(對資料模型的補充描述)。

第二節描述兩種配置方式,即:資料註釋(data annotations)和 Fluent API 方式。

第三節開始,主要是將常用的配置記錄下來,以便翻查。


1 概述

資料實體(Entity)的類名、屬性等,稱之為約定(conventions),約定主要是為了定義資料模型(Model)的形狀。

但是光靠約定可能不足以完整描述資料模型,有時我們的資料模型與我們的資料實體可能也有差異,這時,就可以通過資料註釋(data annotations)和 Fluent API 補充,具體請參考EF Core官方文件:建立並配置模型


2 配置方式

2.1 資料註釋(data annotations)

直接在資料實體上打上對應的標籤,如下例子中,標識表名為 Blogs,Url 屬性不能為 null

[Table("Blogs")]
public class Blog
{
    public int BlogId { get; set; }

    [Required]
    public string Url { get; set; }
}

注意:資料註釋的方式的優先順序高於約定(conventions)但低於 Fluent API,即資料註釋的方式會被 Fluent API 覆蓋。

2.2 Fluent API

對描述資料模型(Model)具有最高優先順序。

通過在派生的 DbContext 中,重寫 OnModelCreating 方法,並使用 ModelBuilder API 來配置模型。

internal class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 寫法1:鏈式配置
        modelBuilder.Entity<Blog>()
            .ToTable("Blogs")
            .Property(b => b.Url).IsRequired();
        
        // 寫法2:委託函式配置
        modelBuilder.Entity<Post>(eb => 
        {
            eb.ToTable("Posts");
            eb.Property(b => b.Title).IsRequired();
        });
    }
}

2.2.1 分組配置

可以實現類似於批量配置,具體請參考分組配置

modelBuilder.ApplyConfigurationsFromAssembly(typeof(BlogEntityTypeConfiguration).Assembly);

注意:應用配置的順序是不確定的,因此僅當順序不重要時才應使用此方法。

3 配置資料模型

3.1 在模型中包含型別

在上下文中包含於 DbSet 的類意味著它包含在 EF Core 的模型中;我們通常將這些類稱為實體。 EF Core 可以向資料庫中讀寫實體例項,如果使用的是關聯式資料庫,EF Core 可以通過遷移為實體建立表。

3.1.1 遷移時,建立表的情況

使用 EF Core 新增遷移時,哪些實體會被建立表呢?包含以下三種情況:

  1. 在 DbContext 的 DbSet 屬性中公開的實體類
  2. 在 DbContext 的 OnModelCreating 方法中指定的實體類
  3. 以上兩種情況的實體類內,通過遞迴探索導航屬性發現的實體類

下面是一個官方示例:

下面的程式碼示例中,資料模型中包含的實體類有:

  • 包含 Blog,因為它在上下文的 DbSet 屬性中公開。
  • 包含 Post,因為它是通過 Blog.Posts 導航屬性發現的。
  • 包含 AuditEntry因為它是 OnModelCreating 中指定的。
internal class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<AuditEntry>();
    }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; } // 導航屬性
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public Blog Blog { get; set; }
}

public class AuditEntry
{
    public int AuditEntryId { get; set; }
    public string Username { get; set; }
    public string Action { get; set; }
}

3.2 配置實體型別(Entity types)

3.2.1 資料註釋(data annotations)

//從模型(model)中排除型別(class)
[NotMapped]

//指定表名稱
[Table("blogs")]
//指定架構(scheme)
[Table("blogs", Schema = "blogging")]

//表註釋
[Comment("Blogs managed on the website")]

3.2.2 Fluent API

//從模型(model)中排除型別(class)
modelBuilder.Ignore<BlogMetadata>();
//從遷移中排除,生成遷移將不會包含 表AspNetUsers,但 IdentityUser 仍在模型中
modelBuilder.Entity<IdentityUser>().ToTable("AspNetUsers", t => t.ExcludeFromMigrations());

//指定表名稱
modelBuilder.Entity<Blog>().ToTable("blogs");
//指定架構(scheme)
modelBuilder.Entity<Blog>().ToTable("blogs", schema: "blogging");
//通用配置:預設架構名
modelBuilder.HasDefaultSchema("blogging");

//檢視對映
//對映到檢視將刪除預設表對映,但從 EF 5.0 開始,實體型別也可以顯式對映到表。 在這種情況下,查詢對映將用於查詢,表對映將用於更新。
modelBuilder.Entity<Blog>().ToView("blogsView", schema: "blogging");

//表註釋
modelBuilder.Entity<Blog>().HasComment("Blogs managed on the website");

//表值函式對映
modelBuilder.Entity<BlogWithMultiplePosts>().HasNoKey().ToFunction("BlogsWithMultiplePosts");

//共享型別實體(Shared-type entity types)
//不理解
//https://docs.microsoft.com/zh-cn/ef/core/modeling/entity-types?tabs=data-annotations#shared-type-entity-types

3.3 配置實體屬性(Entity properties)

3.3.1 資料註釋(data annotations)

//排除屬性
[NotMapped]
//備註
[Comment("The URL of the blog")]

[Column("blog_id")]
[Column(TypeName = "varchar(200)")]
[MaxLength(500)]

//精度和小數位
[Precision(14, 2)]
public decimal Score { get; set; }
[Precision(3)]
public DateTime LastUpdated { get; set; }

//nvarchar 表示 Unicode 資料,varchar 表示非 Unicode 資料
[Unicode(false)]
[Required]

//列排序
//預設情況下,在使用遷移建立表時,EF Core 首先為主鍵列排序,然後為實體型別和從屬型別的屬性排序,最後為基型別中的屬性排序。(順序:主鍵 >> 屬性 >> 基類屬性)
//在一般情況下,大多數資料庫僅支援在建立表時對列進行排序。 這意味著不能使用列順序特性對現有表中的列進行重新排序。
[Column(Order = 0)]
[Column(Order = 1)]

3.3.2 Fluent API

//排除屬性
modelBuilder.Entity<Blog>().Ignore(b => b.LoadedFromDatabase);
//備註
modelBuilder.Entity<Blog>().Property(b => b.Url).HasComment("The URL of the blog");

modelBuilder.Entity<Blog>().Property(b => b.BlogId).HasColumnName("blog_id");
modelBuilder.Entity<Blog>().Property(b => b.Url).HasColumnType("varchar(200)");
modelBuilder.Entity<Blog>().Property(b => b.Url).HasMaxLength(500);

//精度和小數位
modelBuilder.Entity<Blog>().Property(b => b.Score).HasPrecision(14, 2);
modelBuilder.Entity<Blog>().Property(b => b.LastUpdated).HasPrecision(3);

//nvarchar 表示 Unicode 資料,varchar 表示非 Unicode 資料
modelBuilder.Entity<Book>().Property(b => b.Isbn).IsUnicode(false);
modelBuilder.Entity<Blog>().Property(b => b.Url).IsRequired();

//可以定義文字列的排序規則,以確定如何比較和排序。 
//排序規則:https://docs.microsoft.com/zh-cn/ef/core/miscellaneous/collations-and-case-sensitivity
//例如,以下程式碼片段將 SQL Server 列配置為不區分大小寫
modelBuilder.Entity<Customer>().Property(c => c.Name)
    .UseCollation("SQL_Latin1_General_CP1_CI_AS");

modelBuilder.Entity<Employee>().Property(b => b.Id).HasColumnOrder(0);
modelBuilder.Entity<Employee>().Property(b => b.FirstName).HasColumnOrder(1);

3.4 配置主鍵、外來鍵、索引

3.4.1 資料註釋

//主鍵
[Key]
//無主鍵
[Keyless]

//外來鍵
[ForeignKey]
//反向屬性:https://docs.microsoft.com/zh-cn/ef/core/modeling/relationships?tabs=data-annotations%2Cfluent-api-simple-key%2Csimple-key#manual-configuration
[InverseProperty("Author")]

//索引
[Index(nameof(Url))]
public class Blog 
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}
//複合索引
[Index(nameof(FirstName), nameof(LastName))]
//唯一索引
[Index(nameof(Url), IsUnique = true)]
//索引名稱
[Index(nameof(Url), Name = "Index_Url")]

3.4.2 Fluent API

//主鍵
modelBuilder.Entity<Car>().HasKey(c => c.LicensePlate);
//複合主鍵
modelBuilder.Entity<Car>().HasKey(c => new { c.State, c.LicensePlate });
//無主鍵
modelBuilder.Entity<BlogPostsCount>().HasNoKey();

//備選鍵:https://docs.microsoft.com/zh-cn/ef/core/modeling/keys?tabs=fluent-api#alternate-keys
//備選鍵 HasPrincipalKey
modelBuilder.Entity<Post>()
            .HasOne(p => p.Blog)
            .WithMany(b => b.Posts)
            .HasForeignKey(p => p.BlogUrl)
            .HasPrincipalKey(b => b.Url);
//將單個屬性配置為備選鍵
modelBuilder.Entity<Car>()
        .HasAlternateKey(c => c.LicensePlate);
//複合備選鍵
modelBuilder.Entity<Car>()
        .HasAlternateKey(c => new { c.State, c.LicensePlate });
//配置備選鍵的索引和唯一約束的名稱
modelBuilder.Entity<Car>()
        .HasAlternateKey(c => c.LicensePlate)
        .HasName("AlternateKey_LicensePlate");

//配置關係
modelBuilder.Entity<Post>()
    		.HasOne(p => p.Blog)
    		.WithMany(b => b.Posts);
//配置外來鍵
modelBuilder.Entity<Post>()
            .HasOne(p => p.Blog)
            .WithMany(b => b.Posts)
            .HasForeignKey(p => p.BlogForeignKey);
//配置外來鍵約束的名稱
modelBuilder.Entity<Post>()
        .HasOne(p => p.Blog)
        .WithMany(b => b.Posts)
        .HasForeignKey(p => p.BlogId)
        .HasConstraintName("ForeignKey_Post_Blog");
//關係:https://docs.microsoft.com/zh-cn/ef/core/modeling/relationships

//索引
modelBuilder.Entity<Blog>().HasIndex(b => b.Url);
modelBuilder.Entity<Person>().HasIndex(p => new { p.FirstName, p.LastName });
//唯一索引
modelBuilder.Entity<Blog>().HasIndex(b => b.Url).IsUnique();
//索引名稱
modelBuilder.Entity<Blog>().HasIndex(b => b.Url).HasDatabaseName("Index_Url");
//索引篩選器:https://docs.microsoft.com/zh-cn/ef/core/modeling/indexes?tabs=fluent-api#index-filter
modelBuilder.Entity<Blog>().HasIndex(b => b.Url).HasFilter("[Url] IS NOT NULL");
//包含列:SQL Server 的 Include 關鍵字
modelBuilder.Entity<Post>().HasIndex(p => p.Url)
        .IncludeProperties(p => new { p.Title, p.PublishedOn });
//檢查約束
modelBuilder.Entity<Product>().HasCheckConstraint("CK_Prices", "[Price] > [DiscountedPrice]", c => c.HasName("CK_Product_Prices"));

3.5 值轉換(value conversions)

3.5.1 基本配置

假設將一個列舉和實體型別定義為:

public class Rider
{
    public int Id { get; set; }
    public EquineBeast Mount { get; set; }
}

public enum EquineBeast
{
    Donkey,
    Mule,
    Horse,
    Unicorn
}

可以將列舉值(如字串 "Donkey"、"Mule"等)儲存在資料庫中。

需要配置兩個函式:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion(
            v => v.ToString(),
            v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
}

3.5.2 批量配置

public class CurrencyConverter : ValueConverter<Currency, decimal>
{
    public CurrencyConverter()
        : base(
            v => v.Amount,
            v => new Currency(v))
    {
    }
}

配置

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder
        .Properties<Currency>()
        .HaveConversion<CurrencyConverter>();
}

3.5.3 ValueConverter 類

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var converter = new ValueConverter<EquineBeast, string>(
        v => v.ToString(),
        v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));

    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion(converter);
}

3.5.4 內建轉換器

下面是一個示例,更多的請翻查官方文件:內建轉換器

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion<string>();
}

4 資料種子(data seeding)

OnModelCreating 中配置種子資料:

modelBuilder.Entity<Blog>().HasData(new Blog { BlogId = 1, Url = "http://sample.com" });

匿名物件:

modelBuilder.Entity<Post>().HasData(
    new { BlogId = 1, PostId = 2, Title = "Second post", Content = "Test 2" });

多行資料

modelBuilder.Entity<Post>().OwnsOne(p => p.AuthorName).HasData(
    new { PostId = 1, First = "Andriy", Last = "Svyryd" },
    new { PostId = 2, First = "Diego", Last = "Vega" });

參考來源

EF Core官方文件:建立模型