乘風破浪,遇見最佳跨平臺跨終端框架.Net Core/.Net生態 - EFCore兩種配置模型的方式(Fluent API+數據註釋)及值對象、字符集

前言

Entity Framework Core使用一組約定來根據實體類的形狀生成模型。可指定其他配置以補充和/或替代約定的內容。

image

常見的方式包括

  • Fluent API方式配置
  • 數據註釋方式配置

image

配置模型

Fluent API方式配置

可在DbContext的派生上下文中重寫實現OnModelCreating方法,並使用ModelBuilder API來配置模型。

注意:Fluent API方式具有最高優先級,可以替代約定和數據註釋。

/// <summary>
/// 博客
/// </summary>
public class Blog : Entity<long>, IAggregateRoot
{
    /// <summary>
    /// 地址
    /// </summary>
    public string Url { get; private set; }
}

直接在OnModelCreating方法中,基於modelBuilder.Entity<TEnity>進行配置即可。

/// <summary>
/// 練習上下文
/// </summary>
public class PractingContext : DbContext
{
    public PractingContext(DbContextOptions<PractingContext> options) : base(options)
    {

    }

    public DbSet<Blog> Blogs { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .ToTable("blog")
            .Property(p=>p.Url)
            .IsRequired();

        base.OnModelCreating(modelBuilder);
    }
}

爲了不讓OnModelCreating方法膨脹,可以把這個配置邏輯進行分組,拆成實體類型配置(EntityTypeConfiguration)實現類中去

internal class BlogEntityTypeConfiguration : IEntityTypeConfiguration<Blog>
{
    public void Configure(EntityTypeBuilder<Blog> builder)
    {
        builder.ToTable("blog");
        builder.Property(p => p.Url).IsRequired();
    }
}

同時在OnModelCreating方法中應用這個實體類型配置

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    new BlogEntityTypeConfiguration().Configure(modelBuilder.Entity<Blog>());
    base.OnModelCreating(modelBuilder);
}

或者

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfiguration(new BlogEntityTypeConfiguration());
    base.OnModelCreating(modelBuilder);
}

image

還有一種更加方便的方式,就是根據程序集來註冊實體類型配置,使用ApplyConfigurationsFromAssembly,它會掃描指定程序集中所有繼承了IEntityTypeConfiguration的實體類型配置派生類。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfigurationsFromAssembly(typeof(PractingContext).Assembly);
    base.OnModelCreating(modelBuilder);
}

數據註釋方式配置

可以通過數據註釋的方式配置模型,這些Attribute都在System.ComponentModel.DataAnnotations下。

/// <summary>
/// 隨筆
/// </summary>
[Table("posts")]
public class Post : Entity<long>, IAggregateRoot
{
    /// <summary>
    /// 博客ID
    /// </summary>
    [Required]
    public long BlogId { get; private set; }

    /// <summary>
    /// 內容
    /// </summary>
    [Required]
    public string Content { get; private set; }
}

image

數據註釋方式清單

Attribute 描述 舉例
KeyAttribute 表示唯一標識實體的一個或多個屬性 [Key]
ColumnAttribute 獲取或設置該屬性將映射到的列的從零開始的順序 [Column(Order = 1)]
ForeignKeyAttribute 表示一個實體屬性的組合外鍵 [ForeignKey("Passport")]
RequiredAttribute 指定數據字段值是必需的 [Required]
MaxLengthAttribute 指定屬性中允許的數組或字符串數據的最大長度 [MaxLength(2000)]
MinLengthAttribute 指定屬性中允許的數組或字符串數據的最小長度 MinLength(5)
NotMappedAttribute 指定屬性不需要映射 [NotMapped]
ComplexTypeAttribute 指定屬性爲複雜類型 [ComplexType]
ConcurrencyCheckAttribute 指定屬性參與樂觀併發檢查 [ConcurrencyCheck]
TimestampAttribute 指定列的數據類型指定爲行版本 [Timestamp]
TableAttribute 指定某個類爲與數據庫表相關聯的實體類 [Table("posts")]
ColumnAttribute 指定屬性將映射到的數據庫列信息 [Column("postcontent", TypeName = "nvarchar")]
DatabaseGeneratedAttribute 指定數據庫生成屬性值的方式 [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
IndexAttribute 指定要在數據庫中生成的索引 [Index(nameof(BlogId))]
CommentAttribute 標記一個類、屬性或字段,並在相應的數據庫表或列上設置註釋 [Comment("The URL of the blog")]

配置模型

鍵是一個實體實現追蹤的關鍵,Code First有個隱形約定,就是它將查找名爲Id的屬性或者尋找<type name>Id的組合的屬性,並將此映射數據庫爲主鍵列。

如果沒有以上約定的主鍵屬性,那麼會報錯

image

System.InvalidOperationException
  HResult=0x80131509
  Message=The entity type 'BlogDetail' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'. For more information on keyless entity types, see https://go.microsoft.com/fwlink/?linkid=2141943.

這時候我們也可以指定其中某個屬性爲主鍵,這時候可以使用KeyAttribute,它表示唯一標識實體的一個或多個屬性。

其定義是

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public sealed class KeyAttribute : Attribute
{
    public KeyAttribute();
}

數據註釋使用案例

/// <summary>
/// 博客詳情
/// </summary>
[Table("blogdetail")]
public class BlogDetail
{
    /// <summary>
    /// 主要追蹤鍵
    /// </summary>
    [Key]
    public int PrimaryTrackingKey { get; set; }

    /// <summary>
    /// 標題
    /// </summary>
    public string Title { get; set; }

    /// <summary>
    /// 博客名稱
    /// </summary>
    public string BloggerName { get; set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<BlogDetail>()
        .HasKey(c => c.PrimaryTrackingKey);
}

image

組合鍵

實體可以通過KeyAttribute指定多個屬性爲主鍵以此形成組合主鍵,但是多個屬性形成組合鍵時需要通過ColumnAttribute指定屬性的順序。

其定義是

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class ColumnAttribute : Attribute
{
    public ColumnAttribute();

    public ColumnAttribute(string name);

    public string Name { get; }

    public int Order { get; set; }

    public string TypeName { get; set; }
}

還可通過PrimaryKeyAttribute的方式標記,其定義是

[AttributeUsage(AttributeTargets.Class)]
public sealed class PrimaryKeyAttribute : Attribute
{
    public PrimaryKeyAttribute(string propertyName, params string[] additionalPropertyNames)
    {
        Check.NotEmpty(propertyName, nameof(propertyName));
        Check.HasNoEmptyElements(additionalPropertyNames, nameof(additionalPropertyNames));

        PropertyNames = new List<string> { propertyName };
        ((List<string>)PropertyNames).AddRange(additionalPropertyNames);
    }

    public IReadOnlyList<string> PropertyNames { get; }
}

數據註釋使用案例(EF Core)

/// <summary>
/// 車
/// </summary>
[Table("car")]
[PrimaryKey(nameof(State), nameof(LicensePlate))]
public class Car
{
    public string State { get; set; }
    public string LicensePlate { get; set; }

    public string Make { get; set; }
    public string Model { get; set; }
}

數據註釋使用案例(EF 6)

/// <summary>
/// 護照
/// </summary>
public class Passport
{
    /// <summary>
    /// 護照編號
    /// </summary>
    [Key]
    [Column(Order = 1)]
    public int PassportNumber { get; set; }

    /// <summary>
    /// 發證國家
    /// </summary>
    [Key]
    [Column(Order = 2)]
    public string IssuingCountry { get; set; }

    /// <summary>
    /// 發證時間
    /// </summary>
    public DateTime Issued { get; set; }

    /// <summary>
    /// 過期時間
    /// </summary>
    public DateTime Expires { get; set; }
}

然而這個方法已經被廢棄。

System.InvalidOperationException:“The entity type 'Passport' has multiple properties with the [Key] attribute. Composite primary keys can only be set using 'HasKey' in 'OnModelCreating'.”

想要配置組合鍵,只能通過Fluent API方式配置。

internal class PassportEnityTypeConfiguration : IEntityTypeConfiguration<Passport>
{
    public void Configure(EntityTypeBuilder<Passport> builder)
    {
        builder.ToTable("passports");
        builder.HasKey(x => new { x.PassportNumber, x.IssuingCountry });
    }
}

image

定義主鍵名稱

在關係型數據庫中,默認主鍵使用名稱PK_<type name>進行創建。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<BlogDetail>()
        .HasKey(b => b.PrimaryTrackingKey)
        .HasName("PrimaryKey_BlogDetailId");
}

鍵類型和值

雖然EF對鍵類型有廣泛的支持(stringGuidbyte[]等),但並非所有數據庫都支持將這些類型做主鍵。某些情況下,鍵值可以自動轉換爲支持的類型,否則需要手動指定轉換

向上下文添加新實體時,鍵屬性必須始終具有非默認值,但某些類型將由數據庫生成。在這種情況下,當添加實體以用於跟蹤時,EF將嘗試生成一個臨時值。調用SaveChanges後,臨時值將替換爲數據庫生成的值。

如果鍵屬性的值由數據庫生成,並且在添加實體時指定了非默認值,則EF將假定該實體已存在於數據庫中,並嘗試更新它,而不是插入新的實體

若要爲已配置爲在添加或更新時生成值的屬性提供顯式值,還必須按以下方式配置該屬性:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>().Property(b => b.LastUpdated)
        .ValueGeneratedOnAddOrUpdate()
        .Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Save);
}

組合外鍵

如果實體有組合外鍵,還可以通過ForeignKeyAttribute來註釋指定。

其定義是

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class ForeignKeyAttribute : Attribute
{
    public ForeignKeyAttribute(string name);

    public string Name { get; }
}

數據註釋使用案例

/// <summary>
/// 護照
/// </summary>
[Table("passport")]
public class Passport
{
    /// <summary>
    /// 簽證ID
    /// </summary>
    [Key]
    public int StampId { get; set; }

    /// <summary>
    /// 護照編號
    /// </summary>
    [ForeignKey("Passport")]
    [Column(Order = 1)]
    public int PassportNumber { get; set; }

    /// <summary>
    /// 發證國家
    /// </summary>
    [ForeignKey("Passport")]
    [Column(Order = 2)]
    public string IssuingCountry { get; set; }

    /// <summary>
    /// 發證時間
    /// </summary>
    public DateTime Issued { get; set; }

    /// <summary>
    /// 過期時間
    /// </summary>
    public DateTime Expires { get; set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Passport>()
        .HasForeignKey(p => p.PassportNumber)
		.HasForeignKey(p => p.IssuingCountry);
}

備用鍵

除主鍵外,備選鍵還充當每個實體實例的備用唯一標識符;它可以用作關係的目標。使用關係數據庫時,它會映射到備選鍵列上的唯一索引/約束的概念以及引用該列的一個或多個外鍵約束。

在EF中,備選鍵是隻讀的,並且提供對唯一索引的其他語義,因爲它們可以用作外鍵的目標。

備選建通常根據需要引入,無需手動配置。 根據約定,當你將不是主鍵的屬性標識爲關係的目標時,會引入備選鍵: HasPrincipalKey

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

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>()
            .HasOne(p => p.Blog)
            .WithMany(b => b.Posts)
            .HasForeignKey(p => p.BlogUrl)
            .HasPrincipalKey(b => b.Url);
    }
}

還可將單個屬性配置爲備選鍵: HasAlternateKey

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Car>()
        .HasAlternateKey(c => c.LicensePlate);
}

還可將多個屬性配置爲備選鍵(即複合備選鍵)

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Car>()
        .HasAlternateKey(c => new { c.State, c.LicensePlate });
}

根據約定,爲備選鍵引入的索引和約束將命名爲AK_<type name>_<property name>(複合備選鍵<property name>成爲下劃線分隔的屬性名稱列表)。

可配置備選鍵的索引和唯一約束的名稱:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Car>()
        .HasAlternateKey(c => c.LicensePlate)
        .HasName("AlternateKey_LicensePlate");
}

必須

可以通過RequiredAttribute來指示實體的指定屬性是必需的。

其定義爲

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class RequiredAttribute : ValidationAttribute
{
    public RequiredAttribute();

    public bool AllowEmptyStrings { get; set; }

    public override bool IsValid(object value);
}

數據註釋使用案例

/// <summary>
/// 隨筆
/// </summary>
[Table("posts")]
public class Post : Entity<long>, IAggregateRoot
{
    /// <summary>
    /// 博客ID
    /// </summary>
    [Required]
    public long BlogId { get; private set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .Property(b => b.BlogId)
        .IsRequired();
}

image

但是需要注意的是,即使有些時候指定屬性被設置爲必須了,但是其數據庫中的值仍然可能爲Null

可選屬性

當屬性包含Null被視爲有效的,則該屬性被視爲可選屬性。

C# 8引入了一項名爲可爲null引用類型(NRT)的新功能,該功能允許對引用類型進行批註,指示引用類型能否包含null

默認情況下,新項目模板中會啓用可爲Null的引用類型,但在現有項目中保持禁用狀態,除非顯式選擇加入。

可爲Null的引用類型通過以下方式影響EFCore的行爲:

  • 如果禁用可爲null的引用類型,則使用.NET引用類型的所有屬性都按約定((例如string))配置爲可選。
  • 如果啓用了可爲null的引用類型,則基於屬性的.NET類型的C#爲Null性來配置屬性:string?將配置爲可選屬性,但string將配置爲必需屬性。
public class Customer
{
    public int Id { get; set; }
    public string FirstName { get; set; } // Required by convention
    public string LastName { get; set; } // Required by convention
    public string? MiddleName { get; set; } // Optional by convention

    // Note the following use of constructor binding, which avoids compiled warnings
    // for uninitialized non-nullable properties.
    public Customer(string firstName, string lastName, string? middleName = null)
    {
        FirstName = firstName;
        LastName = lastName;
        MiddleName = middleName;
    }
}

列排序規則

從EF Core 5.0開始支持使用UseCollation設置列排序規則,可以定義文本列的排序規則,以確定如何比較和排序。

Fluent API使用案例

modelBuilder.Entity<Customer>()
	.Property(c => c.Name)
    .UseCollation("SQL_Latin1_General_CP1_CI_AS");

SQL_Latin1_General_CP1_CI_AS意味着將SQL Server列配置爲不區分大小寫,這個也可以針對數據庫維度進行配置

modelBuilder.UseCollation("SQL_Latin1_General_CP1_CS_AS");

定義表名

在實體類上使用TableAttribute可以自定義映射的表名。

其定義爲

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class TableAttribute : Attribute
{

    public TableAttribute(string name);

    public string Name { get; }

    public string Schema { get; set; }
}

數據註釋使用案例

/// <summary>
/// 隨筆
/// </summary>
[Table("posts")]
public class Post : Entity<long>, IAggregateRoot
{
    /// <summary>
    /// 博客ID
    /// </summary>
    [Required]
    public long BlogId { get; private set; }

    /// <summary>
    /// 內容
    /// </summary>
    [Required]
    [MinLength(5), MaxLength(2000)]
    public string Content { get; private set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .ToTable("posts");
}

image

定義列名

按照約定,使用關係數據庫時,實體屬性將映射到與屬性同名的表列

如果希望配置具有不同名稱的列,在實體類的字段屬性上使用ColumnAttribute可以自定義映射的列名。

其定義爲

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class ColumnAttribute : Attribute
{
    public ColumnAttribute();

    public ColumnAttribute(string name);

    public string Name { get; }

    public int Order { get; set; }

    public string TypeName { get; set; }
}

數據註釋使用案例

/// <summary>
/// 隨筆
/// </summary>
[Table("posts")]
public class Post : Entity<long>, IAggregateRoot
{
    /// <summary>
    /// 博客ID
    /// </summary>
    [Required]
    public long BlogId { get; private set; }

    /// <summary>
    /// 內容
    /// </summary>
    [Required]
    [MinLength(5), MaxLength(2000)]
    [Column("postcontent", TypeName = "nvarchar")]
    public string Content { get; private set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .Property(b => b.Content)
        .HasColumnName("postcontent");
}

image

列數據類型

使用關係數據庫時,數據庫提供程序會根據屬性的.NET類型選擇數據類型。它還會考慮其他元數據,例如配置的最大長度、屬性是否是主鍵的一部分等等。

例如,SQLServer將DateTime屬性映射到datetime2(7)列,將string屬性映射到nvarchar(max)列(或對於用作鍵的屬性,映射到nvarchar(450))。

還可以配置列以指定列的確切數據類型,在使用ColumnAttribute時不僅可以指定列名,還可以通過TypeName指定列的字段類型。

例如,以下代碼將Url配置爲非unicode字符串,其最大長度爲200,並將Rating配置爲十進制,其精度爲5,小數位數爲2

數據註釋使用案例

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

    [Column(TypeName = "varchar(200)")]
    public string Url { get; set; }

    [Column(TypeName = "decimal(5, 2)")]
    public decimal Rating { get; set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>(
        eb =>
        {
            eb.Property(b => b.Url).HasColumnType("varchar(200)");
            eb.Property(b => b.Rating).HasColumnType("decimal(5, 2)");
        });
}

精度和小數位數

某些關係數據類型支持精度和小數位數Facet,它們用於控制可以存儲哪些值,以及列需要多少存儲

哪些數據類型支持精度和小數位數取決於數據庫,但在大多數數據庫中,decimalDateTime類型支持這些Facet

  • 對於decimal屬性,精度用於定義表示列將包含的任何值所需的最大位數,小數位數用於定義所需的最大小數位數。
  • 對於DateTime屬性,精度用於定義表示秒的小數部分所需的最大位數,不使用小數位數。

在向提供程序傳遞數據之前,實體框架不會執行任何精度或小數位數的驗證。而是由提供程序或數據存儲根據情況進行驗證。例如,當面向SQLServer時,數據類型爲datetime的列不允許設置精度,而datetime2的精度可以介於0和7之間(含這兩個值)。

Score屬性配置爲精度爲14和小數位數爲2將導致在SQLServer上創建decimal(14,2)類型的列,將LastUpdated屬性配置爲精度爲3將導致創建datetime2(3)類型的列:

EF Core 6.0中引入了用於配置精度和小數位數的數據註釋: [Precision(precision, scale)]

public class Blog
{
    public int BlogId { get; set; }
	
    [Precision(14, 2)]
    public decimal Score { get; set; }
	
    [Precision(3)]
    public DateTime LastUpdated { get; set; }
}

EF Core 5.0中引入了用於配置精度和小數位數的Fluent API:HasPrecision(precision, scale)

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Score)
        .HasPrecision(14, 2);

    modelBuilder.Entity<Blog>()
        .Property(b => b.LastUpdated)
        .HasPrecision(3);
}

如果不先定義精度,則永遠不會定義小數位數。

Unicode

在某些關係數據庫中,存在不同的類型來表示Unicode和非Unicode文本數據。

例如,在SQLServer中,nvarchar(x)用於表示UTF-16中的Unicode數據,而varchar(x)用於表示非Unicode數據。這裏的n前綴來自SQL-92標準中的National(Unicode)數據類型。

Unicode全稱:Universal Multiple-Octet Coded Character Set,通用多八位字符集,簡稱UCS

Unicode通過採用兩個字節編碼每個字符,Unicode支持的字符範圍更大,存儲Unicode字符所需要的空間更大。

ncharnvarchar列最多可以有4,000個字符,而不象charvarchar字符那樣可以有8,000個字符。

對於不支持此概念的數據庫,配置此概念將不起作用。

默認情況下,文本屬性配置爲Unicode。可以將列配置爲非Unicode,如下所示:

EF Core 6.0中引入了用於配置Unicode的數據註釋: [Unicode(false)]

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }

    [Unicode(false)]
    [MaxLength(22)]
    public string Isbn { get; set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Book>()
        .Property(b => b.Isbn)
        .IsUnicode(false);
}

列順序

從EF Core 6.0中開始支持可以設置列順序。

默認情況下,在使用遷移創建表時,EF Core首先爲主鍵列排序,然後爲實體類型和從屬類型的屬性排序,最後爲基類型中的屬性排序

但是,你可以在使用ColumnAttribute時通過Order指定不同的列順序:

數據註釋使用案例

public class EntityBase
{
    [Column(Order = 0)]
    public int Id { get; set; }
}

public class PersonBase : EntityBase
{
    [Column(Order = 1)]
    public string FirstName { get; set; }

    [Column(Order = 2)]
    public string LastName { get; set; }
}

public class Employee : PersonBase
{
    public string Department { get; set; }
    public decimal AnnualSalary { get; set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Employee>(x =>
    {
        x.Property(b => b.Id)
            .HasColumnOrder(0);

        x.Property(b => b.FirstName)
            .HasColumnOrder(1);

        x.Property(b => b.LastName)
            .HasColumnOrder(2);
    });
}

注意:在一般情況下,大多數數據庫僅支持在創建表時對列進行排序。這意味着不能使用列順序特性對現有表中的列進行重新排序

列註釋

從EF Core 5.0開始支持使用CommentAttribute對數據庫列設置任意文本註釋,從而在數據庫中記錄架構。

其定義爲

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)]
public sealed class CommentAttribute : Attribute
{
    public CommentAttribute([NotNull] string comment)
    {
        Check.NotEmpty(comment, nameof(comment));

        Comment = comment;
    }

    public string Comment { get; }
}

數據註釋使用案例

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

    [Comment("The URL of the blog")]
    public string Url { get; set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .HasComment("The URL of the blog");
}

image

最大長度

可以通過MaxLengthAttribute來指示實體的指定數組或者字符串屬性的最大長度。

配置最大長度可向數據庫提供程序提供有關爲給定屬性選擇適當列數據類型的提示。最大長度僅適用於數組數據類型,如stringbyte[]

其定義爲

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class MaxLengthAttribute : ValidationAttribute
{
    public MaxLengthAttribute();

    public MaxLengthAttribute(int length);

    public int Length { get; }

    public override string FormatErrorMessage(string name);

    public override bool IsValid(object value);
}

將最大長度配置爲2000將導致在SQL Server上創建nvarchar(2000)類型的列:

數據註釋使用案例

/// <summary>
/// 隨筆
/// </summary>
[Table("posts")]
public class Post : Entity<long>, IAggregateRoot
{
    /// <summary>
    /// 內容
    /// </summary>
    [Required]
    [MaxLength(2000)]
    public string Content { get; private set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .Property(b => b.Content)
        .HasMaxLength(2000);
}

image

最小長度

可以通過MinLengthAttribute來指示實體的指定數組或者字符串屬性的最小長度。

其定義爲

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class MinLengthAttribute : ValidationAttribute
{
    public MinLengthAttribute(int length);

    public int Length { get; }

    public override string FormatErrorMessage(string name);

    public override bool IsValid(object value);
}

數據註釋使用案例

/// <summary>
/// 隨筆
/// </summary>
[Table("posts")]
public class Post : Entity<long>, IAggregateRoot
{
    /// <summary>
    /// 內容
    /// </summary>
    [Required]
    [MinLength(5), MaxLength(2000)]
    public string Content { get; private set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .Property(b => b.Content)
        .HasMinLength(5);
}

排除特定屬性

按照約定,所有具有Getter和Setter的公共屬性都將包含在模型中。

實體中有時候我們並不是所有的字段屬性都需要被映射存儲,如果有這種情況可以使用NotMappedAttribute來註釋。

其定義爲

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class NotMappedAttribute : Attribute
{
    public NotMappedAttribute();
}

數據註釋使用案例

/// <summary>
/// 博客詳情
/// </summary>
[Table("blogdetail")]
public class BlogDetail
{
    /// <summary>
    /// 標題
    /// </summary>
    [Required(ErrorMessage = "Title is Required")]
    public string Title { get; set; }

    /// <summary>
    /// 博客名稱
    /// </summary>
    [MaxLength(10, ErrorMessage = "BloggerName must be 10 characters or less"), MinLength(5)]
    public string BloggerName { get; set; }

    /// <summary>
    /// 博客編碼
    /// </summary>
    [NotMapped]
    public string BlogCode
    {
        get
        {
            return Title.Substring(0, 1) + ":" + BloggerName.Substring(0, 1);
        }
    }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<BlogDetail>()
        .Ignore(b => b.BlogCode);
}

自定義驗證錯誤消息

實際上,前面說到的Attribute都繼承了ValidationAttribute基類,我們看看這個基類定義

public abstract class ValidationAttribute : Attribute
{
    protected ValidationAttribute();

    protected ValidationAttribute(Func<string> errorMessageAccessor);

    protected ValidationAttribute(string errorMessage);

    public string ErrorMessage { get; set; }

    public string ErrorMessageResourceName { get; set; }

    public Type ErrorMessageResourceType { get; set; }

    public virtual bool RequiresValidationContext { get; }

    protected string ErrorMessageString { get; }

    public virtual string FormatErrorMessage(string name);

    public ValidationResult GetValidationResult(object value, ValidationContext validationContext);

    public virtual bool IsValid(object value);

    public void Validate(object value, ValidationContext validationContext);

    public void Validate(object value, string name);

    protected virtual ValidationResult IsValid(object value, ValidationContext validationContext);
}

可以看到它是有一個錯誤消息ErrorMessage可以設定的,我們可以指定某一個註釋校驗條件不成立的時候,定義它的錯誤消息

/// <summary>
/// 博客詳情
/// </summary>
[Table("blogdetail")]
public class BlogDetail
{
    /// <summary>
    /// 標題
    /// </summary>
    [Required(ErrorMessage = "Title is Required")]
    public string Title { get; set; }

    /// <summary>
    /// 博客名稱
    /// </summary>
    [MaxLength(10, ErrorMessage = "BloggerName must be 10 characters or less"), MinLength(5)]
    public string BloggerName { get; set; }
}

值對象

當我們定義了一個值對象類,我們應該給它ComplexTypeAttribute標記,這樣框架纔會把它當作值對象處理。

其定義爲

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ComplexTypeAttribute : Attribute
{
    public ComplexTypeAttribute();
}

使用案例

/// <summary>
/// 博客附件信息
/// </summary>
[ComplexType]
public class BlogAssetInfo
{
    /// <summary>
    /// 創建時間
    /// </summary>
    public DateTime? DateCreated { get; private set; }

    /// <summary>
    /// 博客描述
    /// </summary>
    [MaxLength(250)]
    public string Description { get; private set; }
}
/// <summary>
/// 博客附件
/// </summary>
[Table("blogassets")]
public class BlogAsset : Entity<long>, IAggregateRoot
{
    /// <summary>
    /// 地址
    /// </summary>
    public string Url { get; private set; }

    /// <summary>
    /// 附件信息
    /// </summary>
    public BlogAssetInfo Assets { get; private set; }
}

不過遺憾的是,好像並不能按預期的工作,還是繼續用Fluent API方式配置值對象吧!

併發檢查(樂觀併發)

樂觀併發包括樂觀地嘗試將實體保存到數據庫,希望數據在加載實體後未發生更改。 如果事實證明數據已更改,則會引發異常,必須在嘗試再次保存之前解決衝突。當嘗試保存使用外鍵關聯的實體時,如果檢測到樂觀併發異常,SaveChanges將引發DbUpdateConcurrencyException

使用ConcurrencyCheckAttribute可以標記一個或多個屬性,以便在用戶編輯或刪除實體時,將該屬性用於數據庫中的併發檢查。

其定義爲

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public sealed class ConcurrencyCheckAttribute : Attribute
{
    public ConcurrencyCheckAttribute();
}

使用案例

/// <summary>
/// 博客詳情
/// </summary>
[Table("blogdetail")]
public class BlogDetail
{
    /// <summary>
    /// 主要追蹤鍵
    /// </summary>
    [Key]
    public int PrimaryTrackingKey { get; set; }

    /// <summary>
    /// 博客名稱
    /// </summary>
    [ConcurrencyCheck, MaxLength(10, ErrorMessage = "BloggerName must be 10 characters or less"), MinLength(5)]
    public string BloggerName { get; set; }

}
var blogDetail = context.BlogDetails.FirstOrDefaultAsync().Result;
blogDetail.Modify("Entity Framework Core3", "TaylorShi3");
//var blogDetail = new BlogDetail("Entity Framework Core基礎篇", "TaylorShi");
context.Update(blogDetail);
context.SaveChanges();

調用SaveChanges時,由於BloggerName字段上的ConcurrencyCheck註釋,將在更新中使用該屬性的原始值。該命令將嘗試通過不僅篩選鍵值而且篩選BloggerName的原始值來定位正確的行。

info: 2022/11/8 23:44:03.322 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (21ms) [Parameters=[@p2='1', @p0='TaylorShi3' (Size = 10), @p3='TaylorShi2' (Size = 10), @p1='Entity Framework Core3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      UPDATE `blogdetail` SET `BloggerName` = @p0, `Title` = @p1
      WHERE `PrimaryTrackingKey` = @p2 AND `BloggerName` = @p3;
      SELECT ROW_COUNT();

這裏我們看到,在更新數據3的時候,它還去檢索了數據2,這樣可以防止併發時別人已經修改了這行數據,如果別人修改了這行數據,那麼這個更新將會失敗DbUpdateConcurrencyException

image

那麼如何避免這個問題呢?參考了SO上的一個回答,如果這個實體已經被刪除,那麼放棄提交這個修改,如果實體還存在,就優先將我們當前的值保存下去。

var blogDetail = context.BlogDetails.FirstOrDefaultAsync().Result;
blogDetail.Modify("Entity Framework Core3", "TaylorShi3");
//var blogDetail = new BlogDetail("Entity Framework Core基礎篇", "TaylorShi");
context.Update(blogDetail);

// 在客戶端優先時解決樂觀併發異常
bool saveFailed;
do
{
    saveFailed = false;
    try
    {
        context.SaveChanges();
    }
    catch (DbUpdateConcurrencyException ex)
    {
        saveFailed = true;
        // 獲取拋出DbUpdateConcurrencyException異常的實體
        var entry = ex.Entries.Single();
        // 如果這個實體狀態爲已刪除
        if (entry.State == EntityState.Deleted)
        {
            // 設置實體的EntityState爲Detached,放棄更新或放棄刪除拋出異常的實體
            entry.State = EntityState.Detached;
        }
        else
        {
            // 以Context中的數據覆蓋數據中的數據
            entry.OriginalValues.SetValues(entry.GetDatabaseValues());
        }
    }
}
while (saveFailed);

如果你希望優先數據庫的數據來處理這個異常,也可以寫成

// 通過數據庫優先解決樂觀併發異常
bool saveFailed;
do
{
    saveFailed = false;
    try
    {
        context.SaveChanges();
    }
    catch (DbUpdateConcurrencyException ex)
    {
        saveFailed = true;

        // 重載數據庫的數據覆蓋本地Context中的數據
        ex.Entries.Single().Reload();
    }
}
while (saveFailed);

有時,你可能想要將數據庫中的當前值與實體中的當前值組合在一起。這通常需要一些自定義邏輯或用戶交互,這時候可能需要自定義方案來解決。

// 自定義樂觀併發異常的解決方案
bool saveFailed;
do
{
    saveFailed = false;
    try
    {
        context.SaveChanges();
    }
    catch (System.Data.Entity.Infrastructure.DbUpdateConcurrencyException ex)
    {
        saveFailed = true;

        var entry = ex.Entries.Single();
        var currentValues = entry.CurrentValues;
        var databaseValues = entry.GetDatabaseValues();

        var resolvedValues = databaseValues.Clone();

        HaveUserResolveConcurrency(currentValues, databaseValues, resolvedValues);

        entry.OriginalValues.SetValues(databaseValues);
        entry.CurrentValues.SetValues(resolvedValues);
    }
}
while (saveFailed);

public static void HaveUserResolveConcurrency(DbPropertyValues currentValues, DbPropertyValues databaseValues, DbPropertyValues resolvedValues)
{
    // Show the current, database, and resolved values to the user and have
    // them edit the resolved values to get the correct resolution.
}

還可以寫成使用對象自定義樂觀併發異常的解決方案

// 使用對象自定義樂觀併發異常的解決方案
bool saveFailed;
do
{
    saveFailed = false;
    try
    {
        context.SaveChanges();
    }
    catch (System.Data.Entity.Infrastructure.DbUpdateConcurrencyException ex)
    {
        saveFailed = true;

        var entry = ex.Entries.Single();
        var databaseValues = entry.GetDatabaseValues();
        var databaseValuesAsBlogDetail = (BlogDetail)databaseValues.ToObject();

        var resolvedValuesAsBlogDetail = (BlogDetail)databaseValues.ToObject();

        HaveUserResolveConcurrency((BlogDetail)entry.Entity, databaseValuesAsBlogDetail, resolvedValuesAsBlogDetail);

        entry.OriginalValues.SetValues(databaseValues);
        entry.CurrentValues.SetValues(resolvedValuesAsBlogDetail);
    }
}
while (saveFailed);

public static void HaveUserResolveConcurrency(BlogDetail entity, BlogDetail databaseValues, BlogDetail resolvedValues)
{
    // Show the current, database, and resolved values to the user and have
    // them update the resolved values to get the correct resolution.
}

時間戳併發檢查

使用行的版本號(Version)或時間戳(TimeStamp)字段進行併發檢查是更常見的情況。

可以使用它來替代前面的ConcurrencyCheck方案。

使用時間戳(TimeStamp)字段還可以確保這是一個不爲Null的列,不管創建還是更新數據它都會更新值。

image

行版本類型(也稱爲序列號)是保證數據庫中唯一的二進制數。 它不表示實際時間。行版本數據在視覺上沒有意義。

針對字節數組類型的屬性,可以使用TimeStampAttribute標記它來實現併發檢查。

其定義爲

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public sealed class TimestampAttribute : Attribute
{
    public TimestampAttribute();
}

使用案例

/// <summary>
/// 博客
/// </summary>
public class Blog : Entity<long>, IAggregateRoot
{
    public Blog()
    {

    }

    public Blog(string url)
    {
        this.Url = url;
    }

    public void Update(string url)
    {
        this.Url = url;
    }

    /// <summary>
    /// 地址
    /// </summary>
    public string Url { get; private set; }

    /// <summary>
    /// 時間戳
    /// </summary>
    [Timestamp]
    public Byte[] TimeStamp { get; set; }
}
var blog = context.Blogs.FirstOrDefaultAsync().Result;
blog.Update("https://www.cnblogs.com/taylorshi/p/16862811.html");
context.Update(blog);
//var blog = new Blog("https://www.cnblogs.com/taylorshi/p/16862811.html");
//context.Add(blog);
context.SaveChanges();
info: 2022/11/9 00:38:03.214 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (18ms) [Parameters=[@p1='1', @p2='2022-11-08T16:37:47.6878820' (Nullable = true) (DbType = DateTime), @p0='https://www.cnblogs.com/taylorshi/p/16862811.html' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      UPDATE `blogs` SET `Url` = @p0
      WHERE `Id` = @p1 AND `TimeStamp` = @p2;
      SELECT `TimeStamp`
      FROM `blogs`
      WHERE ROW_COUNT() = 1 AND `Id` = @p1;

我們看到,當更新數據的時候,它一樣的去檢驗了時間戳是否是原始值,如果被人改變了,那麼就會觸發樂觀併發異常。

image

Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.

計算屬性

計算屬性是一種重要的數據庫能力,當我們從將模型類映射到數據庫的時候,我們不希望實體框架更新列,但是在插入或更新數據後,我們希望從數據庫返回這些計算屬性的值。

使用DatabaseGeneratedAttribute可以標記字段屬性爲計算屬性,同時我們還需要使用DatabaseGeneratedOption指定它的枚舉選項。

其定義爲

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class DatabaseGeneratedAttribute : Attribute
{
    public DatabaseGeneratedAttribute(DatabaseGeneratedOption databaseGeneratedOption);

    public DatabaseGeneratedOption DatabaseGeneratedOption { get; }
}

其中DatabaseGeneratedOption定義爲

public enum DatabaseGeneratedOption
{
    //
    // 數據庫不生成值:
    //
    None = 0,
    //
    // 當行被插入後數據庫生成一個值
    //
    Identity = 1,
    //
    // 當行被插入或者更新後數據庫生成一個值
    //
    Computed = 2
}

在默認情況下,整數類型的鍵屬性會被當做數據庫的標識鍵,等同於設置了DatabaseGeneratedOption.Identity效果,如果不需要這樣,則需要設置爲None

使用案例

/// <summary>
/// 隨筆
/// </summary>
[Table("posts")]
public class Post : Entity<long>, IAggregateRoot
{
    /// <summary>
    /// 創建時間
    /// </summary>
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public DateTime DateCreated { get; private set; }

    /// <summary>
    /// 修改時間
    /// </summary>
    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
    public DateTime DateModifyed { get; private set; }
}

image

我們多次修改數據,發現DateCreated的值不會變,而每次修改數據DateModifyed值都會變,這正是DatabaseGeneratedOption兩個枚舉的區別所在。

  • DatabaseGeneratedOption.Identity,僅在插入數據時數據庫生成值。
  • DatabaseGeneratedOption.Computed,在插入和更新數據時都會由數據庫生成新值。

索引

從EF Core 5.0開始支持通過數據註釋來配置索引,所以Microsoft.EntityFrameworkCore >= 5.0.0

可以使用IndexAttribute在一個或者多個列中標記該屬性爲索引。EF會在創建數據庫時之後同步創建這裏指定的索引。

其定義爲

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class IndexAttribute : Attribute
{
    private bool? _isUnique;
    private string _name;

    public IndexAttribute([CanBeNull] params string[] propertyNames)
    {
        Check.NotEmpty(propertyNames, nameof(propertyNames));
        Check.HasNoEmptyElements(propertyNames, nameof(propertyNames));

        PropertyNames = propertyNames.ToList();
    }

    public IReadOnlyList<string> PropertyNames { get; }

    public string Name
    {
        get => _name;
        [param: NotNull] set => _name = Check.NotNull(value, nameof(value));
    }


    public bool IsUnique
    {
        get => _isUnique ?? false;
        set => _isUnique = value;
    }

    public bool IsUniqueHasValue
        => _isUnique.HasValue;
}

單列索引

數據註釋使用案例

/// <summary>
/// 隨筆
/// </summary>
[Table("posts")]
[Index(nameof(BlogId))]
public class Post : Entity<long>, IAggregateRoot
{
    /// <summary>
    /// 博客ID
    /// </summary>
    [Required]
    public long BlogId { get; private set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasIndex(b => b.BlogId);
}

實際效果:IX_posts_BlogId

image

複合索引

使用IndexAttribute可以標記多個列屬性爲索引,稱爲複合索引。

複合索引可以加快對索引列進行篩選的查詢速度,還可以加快僅對索引覆蓋的第一列進行篩選的查詢速度。

數據註釋使用案例

/// <summary>
/// 個人
/// </summary>
[Table("person")]
[Index(nameof(FirstName), nameof(LastName))]
public class Person : Entity<long>, IAggregateRoot
{
    /// <summary>
    /// 名
    /// </summary>
    public string FirstName { get; private set; }

    /// <summary>
    /// 姓
    /// </summary>
    public string LastName { get; private set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .HasIndex(p => new { p.FirstName, p.LastName });
}

實際效果:IX_person_FirstName_LastName

image

索引唯一性

默認情況下,索引不具備唯一性,可以多行出現相同的值,如果要讓索引具備唯一性,可以通過IsUnique = true設置。

嘗試爲索引的列集插入多個具有相同值的實體將導致引發異常。

數據註釋使用案例

/// <summary>
/// 個人
/// </summary>
[Table("person")]
[Index(nameof(PersonId), IsUnique = true)]
public class Person : Entity<long>, IAggregateRoot
{
    /// <summary>
    /// 標識
    /// </summary>
    public int PersonId { get; private set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .HasIndex(b => b.PersonId)
        .IsUnique();
}

實際效果:IX_person_PersonId -> UNIQUE

image

索引排序順序(>= EF Core 7.0)

使用案例

索引排序順序默認爲升序,可設置AllDescending = true使所有列按降序排列。

數據註釋使用案例

[Index(nameof(Url), nameof(Rating), AllDescending = true)]
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
    public int Rating { get; set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasIndex(b => new { b.Url, b.Rating })
        .IsDescending();
}

還可以按列指定順序

數據註釋使用案例

[Index(nameof(Url), nameof(Rating), IsDescending = new[] { false, true })]
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
    public int Rating { get; set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasIndex(b => new { b.Url, b.Rating })
        .IsDescending(false, true);
}

定義索引名稱

在數據庫中創建的索引默認會命名爲IX_<type name>_<property name>,對於複合索引,<property name>將成爲以下劃線分隔的屬性名稱列表。

可以通過Name = "CustomIndexName"來指定索引名稱。

數據註釋使用案例

/// <summary>
/// 個人
/// </summary>
[Table("person")]
[Index(nameof(Url), Name = "Person_Url", IsUnique = true)]
public class Person : Entity<long>, IAggregateRoot
{
    /// <summary>
    /// 鏈接
    /// </summary>
    public string Url { get; private set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .HasIndex(b => b.Url)
        .HasDatabaseName("Person_Url");
}

索引篩選器

有些數據庫支持篩選索引或者部分索引,這使你可以僅索引列值的子集,從而減少索引的大小並改善性能和磁盤空間的使用情況。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .HasIndex(b => b.Url)
        .HasFilter("[Url] IS NOT NULL");
}

SQLServer中,默認EF會給所有唯一索引添加IS NOT NULL篩選器,若要修改它,可以給它設置一個值

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .HasIndex(b => b.Url)
        .IsUnique()
        .HasFilter(null);
}

包含列

有些關係型數據庫,可以配置一組列歸到索引中,這樣除了對索引列查詢可以使用索引提高性能,還有僅訪問包含列的查詢的時候也可以不需要訪問表。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .HasIndex(p => p.Url)
        .IncludeProperties(
            p => new { p.FirstName, p.LastName });
}

檢查約束

在關係型數據中,有一項標準功能叫檢查約束,可以定義一個約束條件,這個條件需要適用於表中所有行,當插入或修改數據不符合這個約束條件的時候都將失敗。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Product>()
        .ToTable(b => b.HasCheckConstraint("CK_Prices", "[Price] > [DiscountedPrice]"));
}

可在同一個表上定義多個檢查約束,每個約束都有自己的名稱。

一些常見的檢查約束可通過社區包EFCore.CheckConstraints進行配置。

正確使用索引

查詢能否快速運行的主要決定因素是它是否在恰當的位置使用索引:

  • 數據庫通常用於保存大量數據,而遍歷整個表的查詢往往是嚴重性能問題的根源。
  • 索引問題不容易發現,因爲給定的查詢是否會使用索引並不是顯而易見的。

發現索引問題的一個好方法是:先準確定位慢速查詢,然後通過數據庫的常用工具檢查其查詢計劃

在使用索引時要記住的一些一般準則

  • 索引能加快查詢,但也會減緩更新,因爲它們需要保持最新狀態。避免定義不需要的索引,並考慮使用索引篩選器將索引限制爲行的子集,從而減少此開銷。
  • 複合索引可加速篩選多列的查詢,也可加速不篩選所有索引列的查詢,具體取決於排序。例如,列A和列B上的索引加快按A和B篩選的查詢以及僅按A篩選的查詢,但不加快僅按B篩選的查詢。
  • 如果查詢按表達式篩選列(例如price/2),則不能使用簡單索引。但是,你可以爲表達式定義存儲的持久化列,並對該列創建索引。一些數據庫還支持表達式索引,可以直接使用這些索引加快按任何表達式篩選的查詢。
  • 不同數據庫允許以各種不同的方式配置索引,在許多情況下,EFCore提供程序都通過FluentAPI公開這些索引。例如,你可以通過SQLServer提供程序配置索引是否爲聚集索引,或設置其填充因子。

值對象(Value-Object)

什麼場景需要值對象

對於實體而言,標識是必不可少的,但是系統中有許多對象和數據項不需要標識和標識跟蹤,這樣我們可以把它設計爲值對象(Value-Object)

image

Order聚合中的Address值對象

在Order實體建模爲具有標識的實體,在其內部包含一組特性(如OrderId、OrderDate、OrderItems等),但地址(Address)只是由國家、地區、街道、城市等組成的複雜對象值,在此域中沒有標識,因此建模時可設計爲值對象(Value-Object)

值對象特徵

值對象兩個特徵

  • 沒有標識,不需要標識和標識追蹤
  • 不可變,創建對象之後,值對象的值必須是不可變的,在構造對象時就必須提供所需的值,不允許在對象生命週期內更改。

如何實現值對象

值對象應該基於值對象基類來實現,而不是基於標識。

public abstract class ValueObject
{
    protected static bool EqualOperator(ValueObject left, ValueObject right)
    {
        if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
        {
            return false;
        }
        return ReferenceEquals(left, right) || left.Equals(right);
    }

    protected static bool NotEqualOperator(ValueObject left, ValueObject right)
    {
        return !(EqualOperator(left, right));
    }

    protected abstract IEnumerable<object> GetEqualityComponents();

    public override bool Equals(object obj)
    {
        if (obj == null || obj.GetType() != GetType())
        {
            return false;
        }

        var other = (ValueObject)obj;

        return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
    }

    public override int GetHashCode()
    {
        return GetEqualityComponents()
            .Select(x => x != null ? x.GetHashCode() : 0)
            .Aggregate((x, y) => x ^ y);
    }
    // Other utility methods
}

注意,ValueObject是抽象類(abstract class),可根據需要重載==!=運算符,如果需要重載,可以將比較委託給Equals來實現。

public static bool operator ==(ValueObject one, ValueObject two)
{
    return EqualOperator(one, two);
}

public static bool operator !=(ValueObject one, ValueObject two)
{
    return NotEqualOperator(one, two);
}

設計具體業務值對象時可以繼承自ValueObject基類

/// <summary>
/// 地址
/// </summary>
public class Address : ValueObject
{
    public String Street { get; private set; }
    public String City { get; private set; }
    public String State { get; private set; }
    public String Country { get; private set; }
    public String ZipCode { get; private set; }

    public Address() { }

    public Address(string street, string city, string state, string country, string zipcode)
    {
        Street = street;
        City = city;
        State = state;
        Country = country;
        ZipCode = zipcode;
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        // Using a yield return statement to return each element one at a time
        yield return Street;
        yield return City;
        yield return State;
        yield return Country;
        yield return ZipCode;
    }
}

這裏需要在實現類中重寫抽象方法GetEqualityComponents

特別注意的是,值對象Address實現類是沒有標識的,我們沒有給它定義任何Id字段。

對值對象的寫的保護

由於值對象的不可變屬性,所以值對象應該是隻讀屬性,但是爲了避免反序列化時不會阻止反序列化器分配值,我們可以將其設計成private set即可,這樣也有效控制了其可讀程度。

/// <summary>
/// 訂單
/// </summary>
public class Order : Entity<long>, IAggregateRoot
{
    public Address Address { get; private set; }
}

值對象的比較

static void Main(string[] args)
{
    var one = new Address("沙頭街道", "深圳", "廣東省","中國", "518000");
    var two = new Address("沙頭街道", "深圳", "廣東省", "中國", "518000");

    Console.WriteLine(EqualityComparer<Address>.Default.Equals(one, two)); // True
    Console.WriteLine(object.Equals(one, two)); // True
    Console.WriteLine(one.Equals(two)); // True
    Console.WriteLine(one == two); // True
    Console.ReadKey();
}

實際運行結果

image

爲什麼==運算符結果是False呢?因爲我們還沒重載==運算符。

image

再次運行,就全部爲True了

image

持久化保存值對象

從EF Core 2.0版本開始就支持以從屬固有實體類型(Owned Entity Type)的形式來持久保存值對象。

固有實體類型(Owned Entity Type)允許在任何實體中映射具有以下特徵的類型:

  • 用作屬性且不具有在域模型中顯示定義它自己的標識,比如值對象

查詢所有者時,固有實體類型將默認包括在內。

事實上,固有實體類型是有標識的,只是這個標識並非完全屬於他們自己,它由三部分組成:

  • 所有者標識
  • 指向它們的導航屬性
  • 對於固有類型的集合,一個獨立的組成部分(EF Core 2.2版本以上版本開始支持)

在數據庫上下文實現類中我們重寫OnModelCreating方法,在這裏應用針對實體基礎結構配置(EntityTypeConfiguration)

/// <summary>
/// 練習上下文
/// </summary>
public class PractingContext : DbContext
{
    public PractingContext(DbContextOptions<PractingContext> options) : base(options)
    {

    }

    public DbSet<Order> Orders { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
        base.OnModelCreating(modelBuilder);
    }
}

這裏我們定義了一個針對Order實體的基礎結構配置OrderEntityTypeConfiguration,它定義了Order實體的持久性基礎結構,其定義如下

internal class OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.ToTable("orders");
        builder.HasKey(o => o.Id);
        builder.Ignore(b => b.DomainEvents);

        builder.OwnsOne(o => o.Address);
    }
}

這裏我們通過builder.OwnsOne(o => o.Address)方法,指定了Address屬性爲Order實體的固有實體(Owned Entity)。

默認情況下,EF Core會將固有實體屬性的列命名爲EntityProperty_OwnedEntityProperty,例如Address_Street,他們都將出現在其從屬的實體表中。

image

還可以通過Property().HasColumnName()來命名列名。

builder.OwnsOne(o => o.Address, a =>
{
    a.Property(p => p.Street).HasColumnName("Street");
    a.Property(p => p.City).HasColumnName("City");
    a.Property(p => p.State).HasColumnName("State");
    a.Property(p => p.Country).HasColumnName("Country");
    a.Property(p => p.ZipCode).HasColumnName("ZipCode");
});

image

還可以連貫性映射

/// <summary>
/// 訂單
/// </summary>
public class Order : Entity<long>, IAggregateRoot
{
    public OrderDetails OrderDetails { get; private set; }
}

/// <summary>
/// 訂單詳情
/// </summary>
public class OrderDetails
{
    /// <summary>
    /// 賬單地址
    /// </summary>
    public Address BillingAddress { get; private set; }

    /// <summary>
    /// 發貨地址
    /// </summary>
    public Address ShippingAddress { get; private set; }
}

那麼可以使用

builder.OwnsOne(o => o.OrderDetails, od =>
{
    od.OwnsOne(d => d.BillingAddress);
    od.OwnsOne(d => d.ShippingAddress);
});

image

builder.OwnsOne(o => o.OrderDetails, od =>
{
    od.OwnsOne(d => d.BillingAddress, a =>
    {
        a.Property(p => p.Street).HasColumnName("Billing_Street");
        a.Property(p => p.City).HasColumnName("Billing_City");
        a.Property(p => p.State).HasColumnName("Billing_State");
        a.Property(p => p.Country).HasColumnName("Billing_Country");
        a.Property(p => p.ZipCode).HasColumnName("Billing_ZipCode");
    });
    od.OwnsOne(d => d.ShippingAddress, a =>
    {
        a.Property(p => p.Street).HasColumnName("Shipping_Street");
        a.Property(p => p.City).HasColumnName("Shipping_City");
        a.Property(p => p.State).HasColumnName("Shipping_State");
        a.Property(p => p.Country).HasColumnName("Shipping_Country");
        a.Property(p => p.ZipCode).HasColumnName("Shipping_ZipCode");
    });
});

image

使用值對象限制

  • 不能創建固有類型的DbSet<T>
  • 不能對固有類型調用ModelBuilder.Entity<T>()
  • 不支持使用同一表格中所有者映射的可選固有類型(Address?),因爲對每個屬性都進行了映射。
  • 沒有對固有類型的繼承映射支持,但應能夠以不同固有類型的形式映射同一繼承層次結構的兩個葉類型。

字符集

字符集

在計算機系統中,所有的數據都以二進制存儲,所有的運算也以二進制表示,人類語言和符號也需要轉化成二進制的形式,才能存儲在計算機中,於是需要有一個從人類語言到二進制編碼的映射表。這個映射表就叫做字符集(Character set)

ASCII

最早的字符集叫American Standard Code for Information Interchange(美國信息交換標準代碼),簡稱ASCII,由American National Standard Institute(美國國家標準協會)制定。在ASCII字符集中,字母A對應的字符編碼是65,轉換成二進制是01000001,由於二進制表示比較長,通常使用十六進制41

GB2312、GBK

ASCII字符集總共規定了128種字符規範,但是並沒有涵蓋西文字母之外的字符,當需要計算機顯示存儲中文的時候,就需要一種對中文進行編碼的字符集,GB2312就是解決中文編碼的字符集,由國家標準委員會發布。同時考慮到中文語境中往往也需要使用西文字母,GB2312也實現了對ASCII的向下兼容,原理是西文字母使用和ASCII中相同的代碼,但是GB2312只涵蓋了6000多個漢字,還有很多沒有包含在其中,所以又出現了GBKGB18030,兩種字符集都是在GB2312的基礎上進行了擴展。

Unicode

可以看到,光是簡體中文,就先後出現了至少三種字符集,繁體中文方面也有BIG5等字符集,幾乎每種語言都需要有一個自己的字符集,每個字符集使用了自己的編碼規則,往往互不兼容。同一個字符在不同字符集下的字符代碼不同,這使得跨語言交流的過程中雙方必須要使用相同的字符編碼才能不出現亂碼的情況。爲了解決傳統字符編碼的侷限性,國際標準化組織制定的通用字符集UCS誕生了,通用多八位編碼字符集(Universal Multiple-Octet Coded Character Set)也叫通用字符集(Universal Character Set,UCS),是由ISO制定的ISO10646(或稱ISO/IEC10646)標準所定義的標準字符集。

image

https://home.unicode.org

同時由Xerox、Apple等軟件製造商於1988年組成的統一碼聯盟,也推出了Unicode來解決這一問題,從Unicode 2.0開始,Unicode採用了與ISO 10646-1相同的字庫和字碼;ISO也承諾,ISO 10646將不會替超出U+10FFFFUCS-4編碼賦值,以使得兩者保持一致。兩個項目仍都獨立存在,並獨立地公佈各自的標準。但統一碼聯盟和ISO/IEC JTC1/SC2都同意保持兩者標準的碼錶兼容,並緊密地共同調整任何未來的擴展。基本上後期,兩個標準就事實合併了,大家使用Unicode描述會更多一些,Unicode在一個字符集中包含了世界上所有文字和符號,統一編碼,來終結不同編碼產生亂碼的問題

字符編碼UTF-8

Unicode統一了所有字符的編碼,是一個Character Set,也就是字符集,字符集只是給所有的字符一個唯一編號,但是卻沒有規定如何存儲,一個編號爲65的字符,只需要一個字節就可以存下,但是編號40657的字符需要兩個字節的空間纔可以裝下,而更靠後的字符可能會需要三個甚至四個字節的空間。

這時,用什麼規則存儲Unicode字符就成了關鍵,我們可以規定,一個字符使用四個字節存儲,也就是32位,這樣就能涵蓋現有Unicode包含的所有字符,這種編碼方式叫做UTF-32(UTF是UCS Transformation Format的縮寫)UTF-32的規則雖然簡單,但是缺陷也很明顯,假設使用UTF-32ASCII分別對一個只有西文字母的文檔編碼,前者需要花費的空間是後者的四倍(ASCII每個字符只需要一個字節存儲)。

在存儲和網絡傳輸中,通常使用更爲節省空間的變長編碼方式UTF-8UTF-8代表8位一組表示Unicode字符的格式,使用1-4個字節來表示字符

UTF-8的編碼規則如下(U+後面的數字代表Unicode字符代碼):

  • U+ 0000 ~ U+ 007F: 0XXXXXXX
  • U+ 0080 ~ U+ 07FF: 110XXXXX 10XXXXXX
  • U+ 0800 ~ U+ FFFF: 1110XXXX 10XXXXXX 10XXXXXX
  • U+10000 ~ U+1FFFF: 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX

可以看到,UTF-8通過開頭的標誌位位數實現了變長對於單字節字符,只佔用一個字節,實現了向下兼容ASCII,並且能和UTF-32一樣,包含Unicode中的所有字符,又能有效減少存儲傳輸過程中佔用的空間

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章