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都是实体,都会生成模型:
Blog
因为在DBSet<T>
中。Post
因为是Blog.Posts
的导航属性AuditEntry
因为是在OnModelCreating
中进行了指定
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AuditEntry>();
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public Blog Blog { get; set; }
}
public class AuditEntry
{
public int AuditEntryId { get; set; }
public string Username { get; set; }
public string Action { get; set; }
}
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一个对象时,这个对象的主键值不能为默认值,但是有些类型的值可以由数据库自动生成所以有默认值也没关系。在这种情况下EF为了跟踪对象状态的需要会生成一个临时值,当调用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之后会被替换为数据库里的真实值。
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]
或1621327561
特性,这二者的区别是:
- 每次insert或者update时,数据库会为
1621327561
特性自动生成值。这个特性通常应用到byte[]
类型上,对应的数据库类型是rowversion
,此时timestamp等效于concurrencycheck。 [ConcurrencyCheck]
可应用到多个属性上,数据库不为这些属性生成值。- 二者解决并发冲突的原理是相同的,都是在update或delete的where语句后除了主键外额外添加了条件。
二者生成的sql如下,可参考:
//timestamp 有两次select
exec sp_executesql N'UPDATE [dbo].[Students]
SET [StudentName] = @0
WHERE (([StudentId] = @1) AND ([RowVersion] = @2))
SELECT [RowVersion]
FROM [dbo].[Students]
WHERE @@ROWCOUNT > 0 AND [StudentId] = @1',N'@0 nvarchar(max) ,@1 int,@2 binary(8)',@0=N'Steve',@1=1,@2=0x00000000000007D1
go
//concurrencycheck有一次select
exec sp_executesql N'UPDATE [dbo].[Students]
SET [StudentName] = @0
WHERE (([StudentId] = @1) AND ([StudentName] = @2))
',N'@0 nvarchar(max) ,@1 int,@2 nvarchar(max) ',@0=N'Steve',@1=1,@2=N'Bill'
go
对应的用法如下:
//1.特性
[ConcurrencyCheck]
public string LastName { get; set; }
[Timestamp]
public byte[] Timestamp { get; set; }
//2.FluentAPI
modelBuilder.Entity<Person>().Property(p => p.LastName).IsConcurrencyToken();
modelBuilder.Entity<Blog>().Property(p => p.Timestamp).IsRowVersion();
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 中的所有约定