Entity Framework Core 是如何根据实体类生成模型的?

Posted JimCarter

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Entity Framework Core 是如何根据实体类生成模型的?相关的知识,希望对你有一定的参考价值。

https://docs.microsoft.com/zh-cn/ef/core/modeling/

EF根据实体类上的一系列约定来创建模型。一般有两种方式来配置这些约定:

  • 在DbContext的OnModelCreating方法中使用fluent API配置:
    internal class MyContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Blog>().Property(b => b.Url).IsRequired();
        }
    }
  • 使用注解来配置模型
public class Blog
{
    public int BlogId { get; set; }
    [Required]
    public string Url { get; set; }
}

1. 约定详解

根据约定,context类中DbSet<T>属性中的T会被认为是实体并包含在模型中。在OnModelCreating方法中指定的实体也会包含在模型中。以下代码的Blog、Post、AuditEntry都是实体,都会生成模型:

internal class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<AuditEntry>();
    }
}
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
    public List<Post> Posts { get; set; }
}
public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public Blog Blog { get; set; }
}
public class AuditEntry
{
    public int AuditEntryId { get; set; }
    public string Username { get; set; }
    public string Action { get; set; }
}

因为:

  • Blog因为在DBSet<T>中。
  • Post因为是Blog.Posts的导航属性
  • AuditEntry因为是在OnModelCreating中进行了指定

1.1 常见约定

特性应用范围功能FluentAPI
[NotMapped]类或属性在模型中排除此类或此属性的映射modelBuilder.Ignore<BlogMetadata>();
modelBuilder.Entity<Blog>().Ignore(b => b.LoadedFromDatabase);
[Table("xxx")]设置这个类对应的数据库表名,默认表名是DbSet<T>的属性名modelBuilder.Entity<Blog>().ToTable("blogs");
[Table("xxx", Schema = "blogging")]*用来设置映射的表架构,默认的架构是dbomodelBuilder.Entity<Blog>().ToTable("blogs", schema: "blogging");
[Column("xxx")]属性用来设置要映射的列名modelBuilder.Entity<Blog>().Property(b => b.BlogId).HasColumnName("blog_id");
[Column(TypeName="varchar(200)")]属性用来设置列的数据类型
默认情况下DateTime会映射为datetime2(7),string映射到nvarchar(max)nvarchar(450)
modelBuilder.Entity<Blog>(eb =>{eb.Property(b => b.Url).HasColumnType("varchar(200)");eb.Property(b => b.Rating).HasColumnType("decimal(5, 2)");});
--映射到数据库视图modelBuilder.Entity<Blog>().ToView("blogsView", schema: "blogging");
--映射到表值函数(TVF[)链接
[Comment("xxxx")]类或属性表或列的注释modelBuilder.Entity<Blog>().HasComment("xxxx");
modelBuilder.Entity<Blog>().Property(b => b.Url).HasComment("The URL of the blog");
[MaxLength(500)]属性设置最大长度,适用于数组数据类型,如stringbyte[]modelBuilder.Entity<Blog>().Property(b => b.Url).HasMaxLength(500);
--设置精度和小数位
通常为decimalDateTime类型的属性
modelBuilder.Entity<Blog>().Property(b => b.Score).HasPrecision(14, 2);modelBuilder.Entity<Blog>().Property(b => b.LastUpdated).HasPrecision(3);
[Required]属性设置列为非空
默认情况下可空类型对应的列可以为null,值类型不为null
modelBuilder.Entity<Blog>().Property(b => b.Url).IsRequired();
[Key]属性设置主键modelBuilder.Entity<Car>().HasKey(c => c.LicensePlate);
1.这个特性没法应用到复合主键上,但是可以通过FluentAPI的方式:modelBuilder.Entity<Car>().HasKey(c => new { c.State, c.LicensePlate });
2.设置主键名称modelBuilder.Entity<Blog>().HasKey(b => b.BlogId).HasName("PrimaryKey_BlogId");,默认名称是PK_<type name>
[Index(nameof(Url),Name="Index_Url")]**给实体的Url属性对应的列上设置索引,并指定索引的名称modelBuilder.Entity<Blog>().HasIndex(b => b.Url).HasDatabaseName("Index_Url");
[Index(nameof(FirstName), nameof(LastName))]设置复合索引modelBuilder.Entity<Person>().HasIndex(p => new { p.FirstName, p.LastName });
[Index(nameof(Url), IsUnique = true)]设置唯一索引modelBuilder.Entity<Blog>().HasIndex(b => b.Url).IsUnique();
[Keyless]设置此实体里没有主键,框架不会对此实体的状态进行跟踪,即使有更改也不会保存到数据库中modelBuilder.Entity<Blog>().HasNoKey();

*:如果需要设置所有类的表架构,可以使用modelBuilder.HasDefaultSchema("blogging");

**:默认的索引名称是IX_<type name>_<property name>,如IX_SysUsers_RoleId

1.2 主键的类型和值

EF支持将.net的基础类型作为主键类型,包括stringGuidbyte[]等。但并不是所有的数据库都支持这种类型,有些情况下主键的值可以自动被转换为数据库所支持的类型,特殊情况下无法自动转换的需要你手动指定

当向context新add一个对象时,这个对象的主键值不能为默认值(如string类型不能为null)。但是有些类型的值可以由数据库自动生成所以有默认值也没关系(如主键类型是int、long)。在这种情况下efcore为了跟踪对象的状态会生成一个临时值赋值给主键,当调用SaveChanges方法后,这个临时值又会被数据库自动生成的值替换掉。

如果主键属性的值可以由数据库自动生成而add时你又给这个属性赋了值,则EF认为这个实体在数据库中已经存在,会进行update操作而不是insert。

1.3 替代键(Alternate Keys)

tbd

1.4 索引过滤器

tbd

2. 模型值的生成

上一小节介绍了,某些主键类型的值可由数据库自动生成。接下来会介绍更多的生成选项。

2.1 默认值

如果插入的行没有该列的值,将使用默认值

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    //设置默认值3
    modelBuilder.Entity<Blog>().Property(b => b.Rating).HasDefaultValue(3);
    //设置默认的sql片段(getdate())
    modelBuilder.Entity<Blog>().Property(b => b.Created).HasDefaultValueSql("getdate()");
}

2.2 计算列

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

上述代码创建了一个虚拟的计算列,每次从数据库捞数据时就会计算他的值。如果需要将此列存储到数据库,则可以设置stored

modelBuilder.Entity<Person>().Property(p => p.NameLength).HasComputedColumnSql("LEN([LastName]) + LEN([FirstName])", stored: true);

2.3 自动生成列值

按照约定,如果程序没有提供值,数据将会为shortintlongGuid等类型的主键自动生成值并设置为标识列。

对于非主键来说,如果也想让数据库帮我们生成值,则可以使用[DatebaseGenerated]特性:

[DatabaseGenerated(DatabaseGeneratedOption.Identity)]//插入时会设置值
public DateTime Inserted { get; set; }

[DatabaseGenerated(DatabaseGeneratedOption.Computed)]//insert或add时都会更新值
public DateTime LastUpdated { get; set; }

对应的FluentAPI为:

modelBuilder.Entity<Blog>().Property(b => b.Inserted).ValueGeneratedOnAdd();
modelBuilder.Entity<Blog>().Property(b => b.LastUpdated).ValueGeneratedOnAddOrUpdate();

注意

  1. 与上几节的默认值计算列不同,这些值取决于具体所使用的数据库提供程序,我们没有手动指定这些值应该是什么。因所使用数据库提供程序的不同,值可能是由EF生成也有可能由数据库生成。如果是由数据库生成,则当add entity时,会被赋值一个临时值,当savechanges之后会被替换为数据库里的真实值。
  2. 在SQL Server上,当GUID属性配置为DatabaseGeneratedOption.Identity时,provider将使用算法生成最佳顺序的GUID值。但是DatabaseGeneratedOption.Identity对DateTime类型的不起任何作用。日期类型需要使用之前介绍的默认值方式.HasDefaultValueSql("getdate()");
  3. 同理对并发标记的属性DatabaseGeneratedOption.Identity也不起任何作用。

2.4 覆盖生成的默认值

可以给类型为T的属性设置一个非default(T)的值来覆盖生成的默认值。同时调用SetAfterSaveBehavior方法:

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

2.5 禁用给主键自动生成值

[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int BlogId { get; set; }

//FluentAPI
modelBuilder.Entity<Blog>().Property(b => b.BlogId).ValueGeneratedNever();

3. 并发标记

可以使用[ConcurrencyCheck]1632475113特性。这二者的区别是:

  • 每次insert或者update时,数据库会为1632475113特性自动生成/更新值。所以需要再select timestampversion一遍拿到这列的最新值,下次update的时候才不会有并发冲突。而[ConcurrencyCheck]相关列的值,数据库并不会帮你更新,需要自己手动更新。
  • 1632475113特性通常应用到byte[]类型上,对应的数据库类型是rowversion[ConcurrencyCheck]可以应用到任意类型上。
  • [ConcurrencyCheck]可应用到多个属性上,数据库不为这些属性生成值。 但是一个表只能有一个rowversion列,所以1632475113只能应用到一个属性上。
  • 二者解决并发冲突的原理是相同的,都是在update或delete的where语句后附加额外的过滤条件。

二者使用方法如下:

//1. 使用特性
class Person
{
	[ConcurrencyCheck]
	public string LastName { get; set; }
	[ConcurrencyCheck]
	public string FirstName{ get; set; }
}
class Blog
{
	[Timestamp]
	public byte[] TimestampVersion { get; set; }
}

//2. 或使用FluentAPI配置    
modelBuilder.Entity<Person>().Property(p => p.LastName).IsConcurrencyToken();
modelBuilder.Entity<Person>().Property(p => p.FirstName).IsConcurrencyToken();

modelBuilder.Entity<Blog>().Property(p => p.TimestampVersion ).IsRowVersion();

当进行更新时,二者生成的sql如下,进一步的信息可点击这里

//timestamp 会再次把TimestampVersion select出来
SET NOCOUNT ON;
UPDATE [Blogs] SET [Url] = @p0
WHERE [Id] = @p1 AND [TimestampVersion] = @p2;
SELECT [TimestampVersion]
FROM [Blogs]
WHERE @@ROWCOUNT = 1 AND [Id] = @p1;

//concurrencycheck:有两个属性应用了特性,则where后面会多跟两个参数
SET NOCOUNT ON;
UPDATE [Person] SET [Age] = @p0
WHERE [Id] = @p1 AND [FirstName] = @p2 AND [LastName] = @p3;
SELECT @@ROWCOUNT;

4. 影子属性

定义:一种在实体类中不存在,但是在模型类中存在的属性。也可以称之为阴影属性。

使用场景:当数据库中的数据不想在实体类上公开时,非常有用。或者你既想维持两个实体的关系,但又不想公开外键属性时。

影子属性最常用于外键属性,导航属性用来维持两个实体之间的关系,当EF发现这两个实体类中不存在对应关系的外键时,就会引入影子属性作为外键。该属性的命名格式为<navigation property name><principal key property name><principal key property name>(与外键的命名规则一样),参考以下代码:

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

    // 这个类中没有BlogId属性,所以EF会创建一个名为BlogId的影子属性
    public Blog Blog { get; set; }
}

4.1 手动创建影子属性

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    //创建了一个DateTime类型的名为LastUpdated的影子属性,如果Blog中已经有LastUpdated了则不会有作用
    modelBuilder.Entity<Blog>().Property<DateTime>("LastUpdated");
}

4.2 访问影子属性

影子属性本质上是通过change tracker访问的,所以如果查询之后调用了AsNoTrack方法,就没法访问影子属性了。

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

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

5. 索引器属性

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

以上是关于Entity Framework Core 是如何根据实体类生成模型的?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Entity Framework Core 获取主键值

Entity Framework Core 是如何根据实体类生成模型的?

Entity Framework Core 2.0:如何配置一次抽象基类

如何关闭 Entity Framework Core 5 中的所有约定

Entity Framework Core SQLite 如何使用相对数据源

如何在 Entity Framework Core 中运行存储过程?