乘风破浪,遇见最佳跨平台跨终端框架.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")]

配置关系

术语

  • 依赖实体:这是包含外键属性的实体。有时是指关系的“子级”。

  • 主体实体:这是包含主键/备选键属性的实体。有时是指关系的“父级”。

  • 主体键:唯一标识主体实体的属性。它可能是主键,也可能是备选键。

  • 外键:依赖实体中用于存储相关实体的主体键值的属性。

  • 导航属性:在引用相关实体的主体实体和/或依赖实体上定义的属性。

    • 集合导航属性:包含对许多相关实体的引用的导航属性。

    • 引用导航属性:保留对单个相关实体的引用的导航属性。

    • 反向导航属性:讨论特定导航属性时,此术语是指关系另一端上的导航属性。

  • 自引用关系:依赖实体类型与主体实体类型相同的关系。

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 int BlogId { get; set; }
    public Blog Blog { get; set; }
}
  • Post是依赖实体

  • Blog是主体实体

  • Blog.BlogId是主体键(在此示例中,它是主键,而不是备选键)

  • Post.BlogId是外键

  • Post.Blog是引用导航属性

  • Blog.Posts是集合导航属性

  • Post.Blog是Blog.Posts的反向导航属性(反之亦然)

约定

默认情况下,如果在类型上发现导航属性,将创建关系。如果当前数据库提供程序无法将属性指向的类型映射为标量类型,则属性被视为导航属性。

完全定义的关系

关系的最常见模式是在关系的两端定义导航属性,并在依赖实体类中定义外键属性

  • 如果在两种类型之间找到了一对导航属性,则它们将被配置为相同关系的反向导航属性。

  • 如果依赖实体包含一个名称与以下模式之一匹配的属性,则该属性将被配置为外键:

    • <navigation property name><principal key property name>
    • <navigation property name>Id
    • <principal entity name><principal key property name>
    • <principal entity name>Id
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 int BlogId { get; set; }
    public Blog Blog { get; set; }
}

无外键属性

虽然建议在依赖实体类中定义外键属性,但这不是必需的。

如果未找到外键属性,则将使用名称<navigation property name><principal key property name><principal entity name><principal key property name>引入外键属性。

此示例中,影子外键为BlogId,因为预先输入导航名称是多余的。

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; }
}

手动配置

若要在Fluent API中配置关系,首先应标识构成关系的导航属性。HasOneHasMany标识要开始配置的实体类型的导航属性。然后,将调用链接到WithOneWithMany以标识反向导航。HasOne/WithOne用于引用导航属性,HasMany/WithMany用于集合导航属性

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);
    }
}

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 Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int AuthorUserId { get; set; }
    public User Author { get; set; }

    public int ContributorUserId { get; set; }
    public User Contributor { get; set; }
}

public class User
{
    public string UserId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    [InverseProperty("Author")]
    public List<Post> AuthoredPosts { get; set; }

    [InverseProperty("Contributor")]
    public List<Post> ContributedToPosts { get; set; }
}

数据注释[ForeignKey][InverseProperty]在命名空间System.ComponentModel.DataAnnotations.Schema中可用。[Required]在命名空间System.ComponentModel.DataAnnotations中可用。

如果只有一个导航属性,则WithOneWithMany会发生无参数重载。这表示在关系的另一端,存在概念上的引用或集合,但实体类中不包含导航属性。

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

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .HasMany(b => b.Posts)
            .WithOne();
    }
}

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; }
}

配置导航属性

从EF Core 5.0开始支持配置导航属性。

创建导航属性后,可能需要进一步对其进行配置。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(b => b.Posts)
        .WithOne();

    modelBuilder.Entity<Blog>()
        .Navigation(b => b.Posts)
        .UsePropertyAccessMode(PropertyAccessMode.Property);
}

配置键

键是一个实体实现追踪的关键,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

外键

可使用数据注释来配置哪个属性应用作给定关系的外键属性。如果按约定未发现外键属性,通常可这样做:

数据注释使用案例

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 int BlogForeignKey { get; set; }

    [ForeignKey("BlogForeignKey")]
    public Blog Blog { get; set; }
}

Fluent API使用案例(简单键)

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.BlogForeignKey);
    }
}

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 int BlogForeignKey { get; set; }
    public Blog Blog { get; set; }
}

Fluent API使用案例(组合键)

internal class MyContext : DbContext
{
    public DbSet<Car> Cars { get; set; }

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

        modelBuilder.Entity<RecordOfSale>()
            .HasOne(s => s.Car)
            .WithMany(c => c.SaleHistory)
            .HasForeignKey(s => new { s.CarState, s.CarLicensePlate });
    }
}

public class Car
{
    public string State { get; set; }
    public string LicensePlate { get; set; }
    public string Make { get; set; }
    public string Model { get; set; }

    public List<RecordOfSale> SaleHistory { get; set; }
}

public class RecordOfSale
{
    public int RecordOfSaleId { get; set; }
    public DateTime DateSold { get; set; }
    public decimal Price { get; set; }

    public string CarState { get; set; }
    public string CarLicensePlate { get; set; }
    public Car Car { get; set; }
}

影子外键

可以使用的字符串重载HasForeignKey(...)将阴影属性配置为外键。建议在将影子属性用作外键之前将其显式添加到模型中。

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

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Add the shadow property to the model
        modelBuilder.Entity<Post>()
            .Property<int>("BlogForeignKey");

        // Use the shadow property as a foreign key
        modelBuilder.Entity<Post>()
            .HasOne(p => p.Blog)
            .WithMany(b => b.Posts)
            .HasForeignKey("BlogForeignKey");
    }
}

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; }
}

外键约束名称

根据约定,当以关系数据库作为目标时,外键约束将命名为FK__<依赖类型名称>_<主体类型名称>_<外键属性名称>。对于复合外键,<外键属性名称>将成为外键属性名称的下划线分隔列表。

还可配置约束名称,如下所示

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne(p => p.Blog)
        .WithMany(b => b.Posts)
        .HasForeignKey(p => p.BlogId)
        .HasConstraintName("ForeignKey_Post_Blog");
}

级联删除

可使用Fluent API显式配置给定关系的级联删除行为。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne(p => p.Blog)
        .WithMany(b => b.Posts)
        .OnDelete(DeleteBehavior.Cascade);
}

组合键

实体可以通过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");
}

配置表

定义表名

在实体类上使用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

排除特定类

使用NotMappedAttribute除了给属性注释,还可以给类注释,代表排除这个类对应的表映射。

数据注释使用案例

/// <summary>
/// 博客
/// </summary>
[NotMapped]
public class Blog : Entity<long>, IAggregateRoot
{

}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Ignore<Blog>();
}

从迁移中排除

从EF Core 5.0开始支持从迁移中排除表的特性。

有时,将相同的实体类型映射到多个DbContext类型中非常有用。在使用绑定上下文时尤其如此,对于每段绑定上下文,使用不同DbContext类型的情况很常见。

使用ExcludeFromMigrations配置迁移不会创建AspNetUsers该表,但IdentityUser仍包含在模型中,并且可正常使用。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<IdentityUser>()
        .ToTable("AspNetUsers", t => t.ExcludeFromMigrations());
}

如果需要再次使用迁移来管理表,则应创建不包括AspNetUsers的新迁移。下一次迁移将包含对表所做的任何更改。

表架构

使用关系数据库时,表按约定在数据库的默认架构中创建。例如,Microsoft SQL Server将使用dbo架构(SQLite不支持架构)。

SQL Server 2005还引入了“默认架构”的概念,用于解析未使用其完全限定名称引用的对象的名称。在SQL Server 2000中,首先检查的是调用数据库用户所拥有的架构,然后是DBO拥有的架构。在SQL Server 2005中,每个用户都有一个默认架构,用于指定服务器在解析对象的名称时将要搜索的第一个架构。可以使用CREATE USERALTER USERDEFAULT_SCHEMA选项设置和更改默认架构。如果未定义DEFAULT_SCHEMA,则数据库用户将把DBO作为其默认架构。

你可以使用Schema配置要在特定架构中创建的表

数据注释使用案例

[Table("blogs", Schema = "blogging")]
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .ToTable("blogs", schema: "blogging");
}

还可以在模型级别使用Fluent API定义默认架构,而不是为每个表指定架构

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasDefaultSchema("blogging");
}

请注意,设置默认架构也会影响其他数据库对象,例如序列。

视图映射

可以使用Fluent API将实体类型映射到数据库视图。

modelBuilder.Entity<Blog>()
    .ToView("blogsView", schema: "blogging");

映射到视图将删除默认表映射,但从EF 5.0开始,实体类型也可以显式映射到表。在这种情况下,查询映射将用于查询,表映射将用于更新。

表注释

从EF Core 5.0中引入了通过数据注释设置注释的功能。

可以对数据库表设置任意文本注释,从而在数据库中记录架构。

数据注释使用案例

[Comment("Blogs managed on the website")]
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>().ToTable(
        tableBuilder => tableBuilder.HasComment("Blogs managed on the website"));
}

共享类型实体类型

从EF Core 5.0开始对共享类型实体类型的支持。

使用相同CLR类型的实体类型称为共享类型实体类型。需要为这些实体类型配置一个唯一的名称,除了CLR类型之外,在使用共享类型实体类型时必须提供该名称。这意味着,必须使用Set调用来实现对应的DbSet属性。

internal class MyContext : DbContext
{
    public DbSet<Dictionary<string, object>> Blogs => Set<Dictionary<string, object>>("Blog");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.SharedTypeEntity<Dictionary<string, object>>(
            "Blog", bb =>
            {
                bb.Property<int>("BlogId");
                bb.Property<string>("Url");
                bb.Property<DateTime>("LastUpdated");
            });
    }
}

配置列

定义列名

按照约定,使用关系数据库时,实体属性将映射到与属性同名的表列

如果希望配置具有不同名称的列,在实体类的字段属性上使用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

列默认值

在关系数据库中,可以为列配置默认值;如果插入的行没有该列的值,则将使用默认值。

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Rating)
        .HasDefaultValue(3);
}

image

还可以指定用于计算默认值的SQL片段(前提是数据库支持)

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Created)
        .HasDefaultValueSql("getdate()");
}

实测MYSQL不支持!

image

计算列

从EF Core 5.0开始支持创建存储计算列

在大多数关系数据库中,可以将列配置为在数据库中计算其值,并且通常使用引用其他列的表达式

modelBuilder.Entity<Person>()
    .Property(p => p.DisplayName)
    .HasComputedColumnSql("[LastName] + ', ' + [FirstName]");

实测MYSQL不支持!

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);
}

列排序规则

从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");

必须属性

可以通过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;
    }
}

自定义验证错误消息

实际上,前面说到的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; }
}

外键阴影属性

阴影属性通常用于外键属性,其中两个实体之间的关系由数据库中的外键值表示,但这种关系是通过实体类型之间的导航属性来管理的。

根据约定,当发现关系,但在依赖实体类中找不到外键属性时,EF将引入阴影属性

例如,以下代码列表将导致BlogId阴影属性引入Post实体:

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

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; }

    // Since there is no CLR property which holds the foreign
    // key for this relationship, a shadow property is created.
    public Blog Blog { get; set; }
}

配置阴影属性

可以使用Fluent API来配置阴影属性。调用Property的字符串重载后,可以链接针对其他属性的任何配置调用。

在下面的示例中,由于Blog没有名为LastUpdated的CLR属性,因此将创建阴影属性:

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

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .Property<DateTime>("LastUpdated");
    }
}

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

image

如果提供给Property方法的名称与现有属性(阴影属性或实体类上定义的属性)的名称匹配,则代码将配置该现有属性,而不是引入新的阴影属性

可以通过ChangeTracker API获取和更改阴影属性值:

context.Entry(Blog).Property("LastUpdated").CurrentValue = DateTime.Now;

可以通过EF.Property静态方法在LINQ查询中引用阴影属性:

var blogs = context.Blogs
    .OrderBy(b => EF.Property<DateTime>(b, "LastUpdated"));

在无跟踪查询后无法访问阴影属性,因为更改跟踪器不会跟踪返回的实体。

可以使用Fluent API来配置索引器属性。调用IndexerProperty方法后,可以链接针对其他属性的任何配置调用。

在下面的示例中,Blog定义了一个索引器,该索引器将用于创建索引器属性。

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

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>().IndexerProperty<DateTime>("LastUpdated");
    }
}

public class Blog
{
    private readonly Dictionary<string, object> _data = new Dictionary<string, object>();
    public int BlogId { get; set; }

    public object this[string key]
    {
        get => _data[key];
        set => _data[key] = value;
    }
}

如果提供给IndexerProperty方法的名称与现有索引器属性的名称匹配,则代码将配置该现有属性。如果实体类型具有属性,该属性由实体类上的属性提供支持,则会引发异常,因为只能通过索引器访问索引器属性。

可以通过EF.Property静态方法(如上所示)或通过使用CLR索引器属性在LINQ查询中引用索引器属性。

属性包实体类型

从EF Core 5.0开始支持对属性包实体类型

仅包含索引器属性的实体类型称为属性包实体类型。这些实体类型没有阴影属性,EF会改为创建索引器属性。目前仅支持将Dictionary<string,object>作为属性包实体类型。

必须配置为具有唯一名称的共享类型实体类型,并且必须使用Set调用实现相应的DbSet属性。

internal class MyContext : DbContext
{
    public DbSet<Dictionary<string, object>> Blogs => Set<Dictionary<string, object>>("Blog");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.SharedTypeEntity<Dictionary<string, object>>(
            "Blog", bb =>
            {
                bb.Property<int>("BlogId");
                bb.Property<string>("Url");
                bb.Property<DateTime>("LastUpdated");
            });
    }
}

无论使用哪种普通实体类型(包括从属实体类型),都可以使用属性包实体类型。

但是,它们有一些限制:

  • 它们不能具有阴影属性。
  • 不支持索引器导航
  • 不支持继承
  • 某些关系模型构建API缺少共享类型实体类型的重载
  • 其他类型不能标记为属性包

配置并发

乐观并发

EF Core实现乐观并发,假定并发冲突相对较少。与悲观方法(即预先锁定数据,然后才继续修改数据)不同,乐观并发不采用锁,但如果数据自查询后发生更改,则数据修改会安排在保存时失败。此并发故障报告给应用程序,应用程序可能会通过重试新数据的整个操作来相应地处理它。

在EF Core中,乐观并发是通过将属性配置为并发令牌来实现的。查询实体时会加载和跟踪并发令牌,就像任何其他属性一样。然后,SaveChanges()期间执行更新或删除操作时,会将数据库上的并发令牌值与EF Core读取的原始值进行比较

并发检查(乐观并发)

乐观并发包括乐观地尝试将实体保存到数据库,希望数据在加载实体后未发生更改。 如果事实证明数据已更改,则会引发异常,必须在尝试再次保存之前解决冲突。当尝试保存使用外键关联的实体时,如果检测到乐观并发异常,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; }

}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<BlogDetail>()
        .Property(p => p.BloggerName)
        .IsConcurrencyToken();
}
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

解决并发冲突

无论并发令牌如何设置,若要实现乐观并发,应用程序必须正确处理发生并发冲突并DbUpdateConcurrencyException引发的情况,这称为解决并发冲突

一种选择是仅通知用户更新由于更改冲突而失败;然后,用户可以加载新数据并重试。或者,如果应用程序正在执行自动更新,只需在重新查询数据后立即循环并重试。

解决并发问题时,一种更复杂的方法是将挂起的更改与数据库中的新值合并。合并哪些值的确切详细信息取决于应用程序,该过程可能由用户界面指示,其中显示了两组值。

有三组值可用于帮助解决并发冲突:

  • “当前值”是应用程序尝试写入数据库的值。
  • “原始值”是在进行任何编辑之前最初从数据库中检索的值。
  • “数据库值”是当前存储在数据库中的值。

处理并发冲突的常规方法是:

  1. SaveChanges期间捕获DbUpdateConcurrencyException
  2. 使用DbUpdateConcurrencyException.Entries为受影响的实体准备一组新更改。
  3. 刷新并发令牌的原始值以反映数据库中的当前值。
  4. 重试该过程,直到不发生任何冲突。
using var context = new PersonContext();
// Fetch a person from database and change phone number
var person = context.People.Single(p => p.PersonId == 1);
person.PhoneNumber = "555-555-5555";

// Change the person's name in the database to simulate a concurrency conflict
context.Database.ExecuteSqlRaw(
    "UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1");

var saved = false;
while (!saved)
{
    try
    {
        // Attempt to save changes to the database
        context.SaveChanges();
        saved = true;
    }
    catch (DbUpdateConcurrencyException ex)
    {
        foreach (var entry in ex.Entries)
        {
            if (entry.Entity is Person)
            {
                var proposedValues = entry.CurrentValues;
                var databaseValues = entry.GetDatabaseValues();

                foreach (var property in proposedValues.Properties)
                {
                    var proposedValue = proposedValues[property];
                    var databaseValue = databaseValues[property];

                    // TODO: decide which value should be written to database
                    // proposedValues[property] = <value to be saved>;
                }

                // Refresh original values to bypass next concurrency check
                entry.OriginalValues.SetValues(databaseValues);
            }
            else
            {
                throw new NotSupportedException(
                    "Don't know how to handle concurrency conflicts for "
                    + entry.Metadata.Name);
            }
        }
    }
}

那么如何避免这个问题呢?参考了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; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(p => p.TimeStamp)
        .IsRowVersion();
}
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.

特殊配置

计算属性

按照约定,如果应用程序未提供值,则将类型为shortintlongGuid的非复合主键设置为针对插入的实体生成值。数据库提供程序通常负责必要的配置;例如,SQLServer中的数字主键会自动设置为IDENTITY列。

计算属性是一种重要的数据库能力,当我们从将模型类映射到数据库的时候,我们不希望实体框架更新列,但是在插入或更新数据后,我们希望从数据库返回这些计算属性的值。

EF Core会自动为主键设置值生成,但我们可能希望对非键属性执行相同的操作。你可以将任何属性配置为针对插入的实体生成其值,使用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; }
}

Fluent API使用案例

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .Property(b => b.DateCreated).ValueGeneratedOnAdd()
        .Property(b => b.DateModifyed).ValueGeneratedOnAddOrUpdate();
}

image

我们多次修改数据,发现DateCreated的值不会变,而每次修改数据DateModifyed值都会变,这正是DatabaseGeneratedOption两个枚举的区别所在。

  • DatabaseGeneratedOption.Identity,仅在插入数据时数据库生成值。
  • DatabaseGeneratedOption.Computed,在插入和更新数据时都会由数据库生成新值。

与默认值或计算列不同,我们没有指定值的生成方式;这取决于所使用的数据库提供程序。数据库提供程序可能会自动为某些属性类型设置值生成,但其他属性类型可能需要你手动设置值的生成方式。

例如,在SQLServer上,如果GUID属性配置为在添加时生成值,提供程序会自动在客户端执行值生成,并使用算法生成最佳顺序GUID值。但是,在DateTime属性上指定ValueGeneratedOnAdd将不起作用。

同样,配置为在添加或更新时生成值并标记为并发标记的byte[]属性将设置为rowversion数据类型,以便在数据库中自动生成值。但是,指定ValueGeneratedOnAdd不起作用。

根据所使用的数据库提供程序,值可能由EF在客户端生成或在数据库中生成。如果值是由数据库生成的,那么EF可能会在你将实体添加到上下文时分配一个临时值;在SaveChanges()期间,此临时值将替换为数据库生成的值。

若要为已配置为在添加或更新时生成值的属性提供显式值,还必须按以下方式配置该属性: Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Save)

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

在某些情况下,你可能希望禁用按约定设置的值生成:ValueGeneratedNever

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

值对象

当我们定义了一个值对象类,我们应该给它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方式配置值对象吧!

继承

实体类型层次结构映射

按照约定,EF不会自动扫描基类型或派生类型;这意味着,如果要映射层次结构中的CLR类型,就必须在模型上显式指定该类型。例如,仅指定层次结构的基类型不会导致EFCore隐式包含其所有子类型。

以下示例将为Blog及其子类RssBlog公开DbSet。如果Blog有任何其他子类,它不会包含在模型中。

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

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

public class RssBlog : Blog
{
    public string RssUrl { get; set; }
}

使用TPH映射时,数据库列会根据需要自动设置为可为null。例如,RssUrl列可为null,因为常规Blog实例没有该属性。

如果不想为层次结构中的一个或多个实体公开DbSet,还可以使用Fluent API确保将它们包含在模型中。

如果不依赖约定,则可以使用HasBaseType显式指定基类型。还可以使用.HasBaseType((Type)null)从层次结构中删除实体类型。

索引

从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中的所有字符,又能有效减少存储传输过程中占用的空间

参考

posted @ 2022-11-06 15:54  TaylorShi  阅读(468)  评论(0编辑  收藏  举报