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")] * | 类 | 用来设置映射的表架构,默认的架构是dbo | modelBuilder.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)] | 属性 | 设置最大长度,适用于数组数据类型,如string 和byte[] | modelBuilder.Entity<Blog>().Property(b => b.Url).HasMaxLength(500); |
- | - | 设置精度和小数位 通常为 decimal 或DateTime 类型的属性 | 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的基础类型作为主键类型,包括string
、Guid
和byte[]
等。但并不是所有的数据库都支持这种类型,有些情况下主键的值可以自动被转换为数据库所支持的类型,特殊情况下无法自动转换的需要你手动指定。
当向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 自动生成列值
按照约定,如果程序没有提供值,数据将会为short
、int
、long
、Guid
等类型的主键自动生成值并设置为标识列。
对于非主键来说,如果也想让数据库帮我们生成值,则可以使用[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();
注意
- 与上几节的默认值和计算列不同,这些值取决于具体所使用的数据库提供程序,我们没有手动指定这些值应该是什么。因所使用数据库提供程序的不同,值可能是由EF生成也有可能由数据库生成。如果是由数据库生成,则当add entity时,会被赋值一个临时值,当savechanges之后会被替换为数据库里的真实值。
- 在SQL Server上,当GUID属性配置为
DatabaseGeneratedOption.Identity
时,provider将使用算法生成最佳顺序的GUID值。但是DatabaseGeneratedOption.Identity
对DateTime类型的不起任何作用。日期类型需要使用之前介绍的默认值方式.HasDefaultValueSql("getdate()");
- 同理对并发标记的属性
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 中的所有约定