有多种方式可以生成数据库列的值:主键列通常是自动递增的整数,其他列有默认值或一些计算值等等。本文详细介绍了使用EF Core生成各种配置值的各种模式。
在关系数据库中,可以用默认值去配置列;如果在该列插入的行没有设置该列的值(就是插入一行,但没手动设置值,那就会用默认值),那么就会使用默认值。
可以用以下方式给属性配置默认值:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>() // 获取Blog实体
.Property(b => b.Rating) // 取实体的Rating属性
.HasDefaultValue(3); // 设其默认值为3
}
还可以指定SQL片段来计算默认值:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Created)
.HasDefaultValueSql("getdate()");
}
在大多数关系数据库中,一列可被配置成在数据库中计算它的值,通常会使用一个引用其他列的表达式:
modelBuilder.Entity<Person>()
.Property(p => p.DisplayName)
.HasComputedColumnSql("[LastName] + ', ' + [FirstName]");
上面代码创建了一个虚拟计算列,每次从数据库中拿它的值时,都会计算它的值。你还可以指定存储一个计算列(有时也称持久化),意思是每次更新行时都会被计算,并与常规列一起存到磁盘上。
持久化(Persistence),即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。
这个概念虽然听说过,但是没了解过,这边简单记一下,持久化常用于本地保持对象。比如说,你在云端做了些操作,然后关机。下次打开电脑,再访问云端时,还是从上次的操作的界面开始,这是因为本地存了状态。
持久化的主要应用是将内存中的对象存储在数据库中,或者存储在磁盘文件中、XML数据文件中等等。
modelBuilder.Entity<Person>()
.Property(p => p.NameLength)
.HasComputedColumnSql("LEN([LastName]) + LEN([FirstName])", stored: true);
注意:
EF Core 5.0增加了对创建存储计算列的支持。
按照约定(EF Core的约定或者说EF Core的规定),在应用程序没有提供值的情况下,short、int、long或Guid类型的非复合主键被设置为为插入的实体生成值。这通常由数据库提供程序负责配置;例如,SQL Server中的数字主键会自动设置到IDENTITY列。
在上面可以看到EF Core自动为主键设置了值生成——但我们也可能想为非键属性做同样的事。你可以按以下方式配置任意属性来为插入实体生成值:
// 1. 数据标注
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public DateTime Inserted { get; set; }
}
// 2. fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Inserted)
.ValueGeneratedOnAdd();
}
类似的,也能配置属性来在添加或更新时生成值:
// 1. 数据标注
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime LastUpdated { get; set; }
}
// 2. fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.LastUpdated)
.ValueGeneratedOnAddOrUpdate();
}
警告:
与默认值或计算值不同,我们没有指定如何生成值;因为这取决于所使用的数据库提供程序。数据库提供程序可能会自动为某些属性类型设置值生成,但其他类型可能需要你手动设置值的生成方式。
例如,在SQL Server上,当GUID属性配置为在添加时生成值时,数据库提供程序就会自动执行生成客户端侧生成值,使用算法生成最佳序列的GUID值。但是,在DataTime属性上指定ValueGeneratedOnAdd不会有任何效果。
类似地,配置为(在添加或更新时生成并标记为并发标记的)byte[]属性使用rowversion数据类型设置,以便在数据库中自动生成值。
注意:
根据使用的数据库提供程序,值可以由EF在客户端侧生成,也可以在数据库中生成。如果该值由数据库生成,那么当你添加实体到上下文时,EF可能会分配一个临时值;这个临时值会被SaveChanges()期间数据库生成的值给替换掉。
有个常见的请求是拥有一个数据库列,该列包含首次插入该列的日期/时间(在添加时生成),或在最后更新该列时的日期/时间(在添加或更新时生成)。因为有许多策略都可以做到这一点,所以EF Core提供者通常不会自动设置日期/时间值生成——你必须自己配置。
将日期/时间列配置为行的创建时间戳,通常需要用适当的SQL函数配置默认值。例如,在SQL Server中你可以使用以下方式:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Created)
.HasDefaultValueSql("getdate()");
}
请确保选择适当的函数,因为可能存在多个函数(例如:GETDATE() vs. GETUTCDATE())
尽管存储计算列似乎是管理最后更新时间戳的好方案,但是数据库通常不允许在计算列中指定GETDATE()等函数。作为替代方案,你可以设置一个数据库触发器来达到相同的效果:
CREATE TRIGGER [dbo].[Blogs_UPDATE] ON [dbo].[Blogs]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF ((SELECT TRIGGER_NESTLEVEL()) > 1) RETURN;
DECLARE @Id INT
SELECT @Id = INSERTED.BlogId
FROM INSERTED
UPDATE dbo.Blogs
SET LastUpdated = GETDATE()
WHERE BlogId = @Id
END
尽管配置属性是为了值生成,但在许多情况下你仍然需要显式地为其指定值。这是否会真的起作用取决于已经配置的特定值的生成机制;虽然可以指定显式值来代替使用列的默认值,但这同样无法用计算列来完成。
要用一个显式值来覆盖生成值,只需要将属性设置为任何不是该属性类型的CLR默认值的值(string的null,int的0,Guid的Guid.Empty等)。
注意:
默认情况下,试图将显式值插入SQL Server IDENTITY会失败。
要为已经配置为在添加或更新时生成值的属性提供显式值,你还必须按下面方式配置属性:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().Property(b => b.LastUpdated)
.ValueGeneratedOnAddOrUpdate()
.Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Save);
}
除了上述的特定场景外,属性通常没配置值生成;这意味着始终提供一个值来存到数据库中取决于应用程序。在新实体添加到上下文之前,该值必须被分配给新的实体。
可是,有些情况下,你也许希望禁用按约定设置的值生成。例如,一个int类型的主键通常会隐式配置为添加时生成的值(例如SQL Server上标识列)。你可以通过以下方式禁用它:
// 1. 数据标注
public class Blog
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int BlogId { get; set; }
public string Url { get; set; }
}
// 2. fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.BlogId)
.ValueGeneratedNever();
}