创建模型

创建并配置模型

Entity Framework Core 使用一组约定来根据实体类的形状生成模型。 可指定其他配置以补充和/或替代约定的内容。

使用 fluent API 配置模型

Fluent API 配置具有最高优先级,并将替代约定和数据注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }

#region Required
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
#endregion
}

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

分组配置

为了减小 OnModelCreating 方法的大小,可以将实体类型的所有配置提取到实现 IEntityTypeConfiguration<TEntity> 的单独类中。

1
2
3
4
5
6
7
8
9
public class BlogEntityTypeConfiguration : IEntityTypeConfiguration<Blog>
{
public void Configure(EntityTypeBuilder<Blog> builder)
{
builder
.Property(b => b.Url)
.IsRequired();
}
}

然后,只需从 OnModelCreating 调用 Configure 方法。

1
new BlogEntityTypeConfiguration().Configure(modelBuilder.Entity<Blog>());

可以在给定程序集中应用实现 IEntityTypeConfiguration的类型中指定的所有配置。

1
modelBuilder.ApplyConfigurationsFromAssembly(typeof(BlogEntityTypeConfiguration).Assembly);

使用数据注释来配置模型

也可将特性(称为数据注释)应用于类和属性。 数据注释会替代约定,但会被 Fluent API 配置替代。

1
2
3
4
5
6
7
8
9
10
11
12
13
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
}

[Table("Blogs")]
public class Blog
{
public int BlogId { get; set; }

[Required]
public string Url { get; set; }
}

实体类型

在上下文中包含一种类型的 DbSet 意味着它包含在 EF Core 的模型中;我们通常将此类类型称为实体。

在模型中包含类型

按照约定,上下文的DbSet属性中公开的类型作为实体包含在模型中。 还包括在 OnModelCreating方法中指定的实体类型,以及通过递归探索其他发现的实体类型的导航属性找到的任何类型。

*** 下面的代码示例中包含了所有类型:***

包含Blog,因为它在上下文的DbSet属性中公开。
包含Post,因为它是通过Blog.Posts 导航属性发现的。
包含AuditEntry因为它是 OnModelCreating中指定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
2
3
4
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Ignore<BlogMetadata>();
}

从迁移中排除

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

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

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

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

表名称

每个实体类型都将设置为映射到与公开实体的 DbSet 属性名称相同的数据库表。 如果给定实体不存在 DbSet,则使用类名称,可以手动配置表名

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.ToTable("blogs");
}

表架构

使用关系数据库时,表按约定在数据库的默认架构中创建,你可以配置要在特定架构中创建的表,如下所示

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

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

1
2
3
4
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("blogging");
}

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

视图映射

可以使用 Fluent API 将实体类型映射到数据库视图,EF 假定数据库中已存在引用的视图,它不会在迁移中自动创建它。

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

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

表值函数映射

若要将实体映射到表值函数,函数必须是无参数的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BlogWithMultiplePosts
{
public string Url { get; set; }
public int PostCount { get; set; }
}

CREATE FUNCTION dbo.BlogsWithMultiplePosts()
RETURNS TABLE
AS
RETURN
(
SELECT b.Url, COUNT(p.BlogId) AS PostCount
FROM Blogs AS b
JOIN Posts AS p ON b.BlogId = p.BlogId
GROUP BY b.BlogId, b.Url
HAVING COUNT(p.BlogId) > 1
)

modelBuilder.Entity<BlogWithMultiplePosts>().HasNoKey().ToFunction("BlogsWithMultiplePosts");

通常情况下,实体属性将映射到 TVF 返回的匹配列。 如果 TVF 返回的列名称与实体属性的名称不同,则可以使用 HasColumnName 方法配置实体的列,就像映射到常规表一样。

表注释

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

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

共享类型实体类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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");
});
}
}

实体属性

模型中的每个实体类型都有一组属性,EF Core 将从数据库中读取和写入这些属性。 如果使用的是关系数据库,实体属性将映射到表列。

已包含和已排除的属性

按照约定,所有具有 Getter 和 Setter 的公共属性都将包含在模型中。

可以按如下所示排除特定属性:

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Ignore(b => b.LoadedFromDatabase);
}

列名

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

如果希望配置具有不同名称的列,可以按以下代码片段进行操作:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.BlogId)
.HasColumnName("blog_id");
}

列数据类型

还可以配置列以指定列的确切数据类型

1
2
3
4
5
6
7
8
9
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)");
});
}

最大长度

在向提供程序传递数据之前,实体框架不会执行任何最大长度的验证。 而是由提供程序或数据存储根据情况进行验证。

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.HasMaxLength(500);
}

精度和小数位数

哪些数据类型支持精度和小数位数取决于数据库,但在大多数数据库中,decimal 和 DateTime 类型支持这些 Facet

1
2
3
4
5
6
7
8
9
10
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);
}

如果不先定义精度,则永远不会定义小数位数,因此用于定义小数位数的 Fluent API 为 HasPrecision(precision, scale)。

Unicode

在某些关系数据库中,存在不同的类型来表示 Unicode 和非 Unicode 文本数据。 例如,在 SQL Server 中,nvarchar(x)用于表示 UTF-16 中的 Unicode 数据,而varchar(x)用于表示非 Unicode 数据
默认情况下,文本属性配置为 Unicode。 可以将列配置为非 Unicode,如下所示:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>()
.Property(b => b.Isbn)
.IsUnicode(false);
}

必需和可选属性

如果属性包含null是有效的,则该属性被视为可选属性。 如果null不是要分配给属性的有效值,则它被视为必需属性。 映射到关系数据库架构时,必需属性创建为不可为null的列,可选属性创建为可为null的列。

约定

按照约定,其.NET类型可包含null的属性将配置为可选属性,而.NET类型不能包含null的属性将配置为必需属性。 例如,所有具有.NET值类型(intdecimal、bool等)的属性都配置为必需,并且具有可为null.NET值类型的所有属性(int?decimal?、bool?等)配置为可选。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
}

显式配置

按约定为可选属性的属性可以配置为必需属性,如下所示:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}

列排序规则

可以定义文本列的排序规则,以确定如何比较和排序

1
2
modelBuilder.Entity<Customer>().Property(c => c.Name)
.UseCollation("SQL_Latin1_General_CP1_CI_AS");

如果数据库中的所有列都需要使用特定的排序规则,请改为在数据库级别定义排序规则。

列注释

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

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.HasComment("The URL of the blog");
}

列顺序

默认情况下,在使用迁移创建表时,EF Core 首先为主键列排序,然后为实体类型和从属类型的属性排序,最后为基类型中的属性排序。 但是,你可以指定不同的列顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
});
}

请注意,在一般情况下,大多数数据库仅支持在创建表时对列进行排序。 这意味着不能使用列顺序特性对现有表中的列进行重新排序

键用作每个实体实例的唯一标识符

配置主键

根据约定,名为 Id 或 Id 的属性将被配置为实体的主键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
internal class Car
{
public string Id { get; set; }

public string Make { get; set; }
public string Model { get; set; }
}

internal class Truck
{
public string TruckId { get; set; }

public string Make { get; set; }
public string Model { get; set; }
}

可将单个属性配置为实体的主键,如下所示:

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasKey(c => c.LicensePlate);
}

还可将多个属性配置为实体的键 - 这称为组合键。 约定仅在特定情况下设置组合键 - 例如,对于拥有的类型集合。

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

创造价值

对于非复合数字和GUID主键,EF Core根据约定设置值生成

主键名称

根据约定,在关系数据库上,主键使用名称PK_<type name> 进行创建。 可按如下方式配置主键约束的名称:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasKey(b => b.BlogId)
.HasName("PrimaryKey_BlogId");
}

键类型和值

向上下文添加新实体时,键属性必须始终具有非默认值,但某些类型将由数据库生成。 在这种情况下,当添加实体以用于跟踪时,EF 将尝试生成一个临时值。 调用 SaveChanges 后,临时值将替换为数据库生成的值。

如果键属性的值由数据库生成,并且在添加实体时指定了非默认值,则 EF 将假定该实体已存在于数据库中,并尝试更新它,而不是插入新的实体。

备用键

除主键外,备选键还充当每个实体实例的备用唯一标识符;它可以用作关系的目标
如果只想对列强制执行唯一性,请定义唯一索引而不是备选键

备选建通常根据需要引入,无需手动配置。 根据约定,当你将不是主键的属性标识为关系的目标时,会引入备选键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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);
}
}

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 string BlogUrl { get; set; }
public Blog Blog { get; set; }
}

还可将单个属性配置为备选键:

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasAlternateKey(c => c.LicensePlate);
}

还可将多个属性配置为备选键(即复合备选键):

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasAlternateKey(c => new { c.State, c.LicensePlate });
}

可配置备选键的索引和唯一约束的名称:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasAlternateKey(c => c.LicensePlate)
.HasName("AlternateKey_LicensePlate");
}

生成的值

数据库列的值可以通过多种方式生成:主键列通常是自动递增的整数,其他列具有默认值或计算值等。

默认值

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

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

还可以指定用于计算默认值的 SQL 片段:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Created)
.HasDefaultValueSql("getdate()");
}

计算列

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

以上命令将创建一个虚拟计算列,每次从数据库中提取时都会计算其值。 你也可以将计算列指定为存储(有时称为持久化)计算列,这意味着系统会在每次更新行时计算该列,并将其与常规列一起存储在磁盘上:

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

主键

显式配置值生成

EF Core 会自动为主键设置值生成 - 但我们可能希望对非键属性执行相同的操作。 你可以将任何属性配置为针对插入的实体生成其值,如下所示:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Inserted)
.ValueGeneratedOnAdd();
}

同样,可以将属性配置为在添加或更新时生成其值:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.LastUpdated)
.ValueGeneratedOnAddOrUpdate();
}

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

日期/时间值生成

EF Core 提供程序通常不会为日期/时间列自动设置值生成 - 你必须自行配置。

创建时间戳

若要将日期/时间列配置为包含行的创建时间戳,通常需要使用适当的 SQL 函数来配置默认值。

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Created)
.HasDefaultValueSql("getdate()");
}

更新时间戳

尽管存储计算列看起来非常适合管理上次更新时间戳,但数据库通常不允许在计算列中指定诸如 GETDATE() 之类的函数。 作为替代方法,你可以设置一个数据库触发器来达到同样的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,等等)以外的任意值。

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

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

无值生成

除了上述特定方案外,属性通常不会配置值生成;这意味着,始终由应用程序提供要保存到数据库的值。 必须先将此值分配给新实体,然后才能将新实体添加到上下文中。

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

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.BlogId)
.ValueGeneratedNever();
}

阴影和索引器属性

阴影属性不是在 .NET 实体类中定义的,但在 EF Core 模型中是为该实体类型定义的。 这些属性的值和状态纯粹保留在更改跟踪器中。 当数据库中存在不应在映射的实体类型上公开的数据时,阴影属性非常有用。

索引器属性是实体类型属性,由 .NET 实体类中的 索引器器提供支持。 可以使用 .NET 类实例上的索引器访问它们。 它还允许向实体类型添加其他属性,而无需更改 CLR 类。

外键阴影属性

阴影属性通常用于外键属性,其中两个实体之间的关系由数据库中的外键值表示,但这种关系是通过实体类型之间的导航属性来管理的。 根据约定,当发现关系,但在依赖实体类中找不到外键属性时,EF 将引入阴影属性。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 属性,因此将创建阴影属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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; }
}

访问阴影属性

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

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

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

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

配置索引器属性

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}
}

属性包实体类型

仅包含索引器属性的实体类型称为属性包实体类型。 这些实体类型没有阴影属性,EF 会改为创建索引器属性。 目前仅支持将 Dictionary<string, object> 作为属性包实体类型。 必须配置为具有唯一名称的共享类型实体 类型,并且必须使用 Set 调用实现相应的 DbSet 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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");
});
}
}

关系

关系定义两个实体之间的相关性。 在关系数据库中,关系由外键约束表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.BlogBlog.Posts 的反向导航属性(反之亦然)

约定

按约定发现的关系将始终以主体实体的主键为目标。 若要以备选键为目标,必须使用 Fluent API 执行配置。

完全定义的关系

无外键属性

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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; }
}

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

如果已存在同名的属性,则影子属性名称将带有数字后缀(如果属性是主键或属性的类型与主体键不兼容,则不会将其配置为外键。)

单个导航属性

仅包含一个导航属性(没有反向导航,也没有外键属性)就足以按约定定义关系。 还可包含一个导航属性和一个外键属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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; }
}

限制

如果在两个类型之间定义了多个导航属性(即不止一对指向彼此的导航),则由导航属性表示的关系是不明确的。 需要对它们进行手动配置才能解决这种不明确的关系。

级联删除

根据约定,对于必需关系,级联删除将设置为Cascade,对于可选关系,它将设置为ClientSetNullCascade表示也会删除依赖实体。ClientSetNull表示未加载到内存中的依赖实体将保持不变,必须手动删除或更新以指向有效的主体实体。 对于加载到内存中的实体,EF Core 将尝试将外键属性设置为 null

手动配置

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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; }
}

单个导航属性

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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; }
}

配置导航属性

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

1
2
3
4
5
6
7
8
9
10
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne();

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

外键

可使用 Fluent API 来配置哪个属性应用作给定关系的外键属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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; }
}

复合主键

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(…) 将阴影属性配置为外键,建议在将影子属性用作外键之前将其显式添加到模型中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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__<依赖类型名称><主体类型名称><外键属性名称>。 对于复合外键,<外键属性名称> 将成为外键属性名称的下划线分隔列表。

1
2
3
4
5
6
7
8
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");
}

没有导航属性

无需提供导航属性。 只需在关系的一侧提供外键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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<Blog>()
.WithMany()
.HasForeignKey(p => p.BlogId);
}
}

public class Blog
{
public int BlogId { get; set; }
public string Url { 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; }
}

主体键

如果想要外键引用主键外的属性,可使用 Fluent API 为关系配置主体键属性。 配置为主体键的属性将自动设置为备选键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
internal class MyContext : DbContext
{
public DbSet<Car> Cars { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<RecordOfSale>()
.HasOne(s => s.Car)
.WithMany(c => c.SaleHistory)
.HasForeignKey(s => new { s.CarState, s.CarLicensePlate })
.HasPrincipalKey(c => new { c.State, c.LicensePlate });
}
}

public class Car
{
public int CarId { get; set; }
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; }
}

必需关系和可选关系

如果实体类中具有外键属性,则关系的必需性取决于外键属性是必需还是可选,外键属性位于依赖实体类型上,因此如果按要求配置它们,则意味着每个依赖实体都需要具有相应的主体实体。

1
2
3
4
5
6
7
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.IsRequired();
}

级联删除

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

1
2
3
4
5
6
7
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.OnDelete(DeleteBehavior.Cascade);
}

其他关系模式

一对一

一对一关系在两侧都有引用导航属性。 它们遵循与一对多关系相同的约定,但在外键属性上引入了一个唯一索引,以确保只有一个依赖项与每个主体相关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }

public BlogImage BlogImage { get; set; }
}

public class BlogImage
{
public int BlogImageId { get; set; }
public byte[] Image { get; set; }
public string Caption { get; set; }

public int BlogId { get; set; }
public Blog Blog { get; set; }
}

在配置与 Fluent API 的关系时,可使用 HasOne 和 WithOne 方法。

显而易见,在一对多关系中,具有引用导航的实体是依赖项,而具有集合的实体是主体。 但在一对一的关系中并非如此,因此需要对其进行显式定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<BlogImage> BlogImages { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasOne(b => b.BlogImage)
.WithOne(i => i.Blog)
.HasForeignKey<BlogImage>(b => b.BlogForeignKey);
}
}

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

public BlogImage BlogImage { get; set; }
}

public class BlogImage
{
public int BlogImageId { get; set; }
public byte[] Image { get; set; }
public string Caption { get; set; }

public int BlogForeignKey { get; set; }
public Blog Blog { get; set; }
}

默认情况下依赖端为可选,但可视需要进行配置。 但是,EF 不会验证是否提供了依赖实体,因此此配置仅在数据库映射允许强制执行时才会产生影响。 此种情况的一个常见场景是默认使用表拆分的引用从属类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
modelBuilder.Entity<Order>(
ob =>
{
ob.OwnsOne(
o => o.ShippingAddress,
sa =>
{
sa.Property(p => p.Street).IsRequired();
sa.Property(p => p.City).IsRequired();
});

ob.Navigation(o => o.ShippingAddress)
.IsRequired();
});

通过此配置,与 ShippingAddress 对应的列将在数据库中标记为不可为 null。

多对多

多对多关系需要两端的集合导航属性。 与其他类型的关系一样,它们也可通过约定发现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }

public ICollection<Tag> Tags { get; set; }
}

public class Tag
{
public string TagId { get; set; }

public ICollection<Post> Posts { get; set; }
}

在数据库中实现此关系的方式是使用联接表,其中包含 Post 和 Tag 的外键。 例如,以下就是 EF 将在关系数据库中为上述模型创建的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CREATE TABLE [Posts] (
[PostId] int NOT NULL IDENTITY,
[Title] nvarchar(max) NULL,
[Content] nvarchar(max) NULL,
CONSTRAINT [PK_Posts] PRIMARY KEY ([PostId])
);

CREATE TABLE [Tags] (
[TagId] nvarchar(450) NOT NULL,
CONSTRAINT [PK_Tags] PRIMARY KEY ([TagId])
);

CREATE TABLE [PostTag] (
[PostsId] int NOT NULL,
[TagsId] nvarchar(450) NOT NULL,
CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
CONSTRAINT [FK_PostTag_Posts_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([PostId]) ON DELETE CASCADE,
CONSTRAINT [FK_PostTag_Tags_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([TagId]) ON DELETE CASCADE
);

在内部,EF 创建一个实体类型Dictionary<string, object> 来表示将称为联接实体类型的联接表。 Dictionary<string, object> 当前用于处理外键属性的任意组合,有关详细信息,请参阅属性包实体类型。 模型中可以存在多个多对多关系,因此必须为联接实体类型指定唯一的名称,在本例中为 PostTag。 允许此操作的功能称为共享类型实体类型。
按照约定,用于联接实体类型的 CLR 类型可能会在未来版本中更改以提高性能。 除非已显式配置,否则不要依赖于联接类型 Dictionary<string, object>,如下一节所述。

多对多导航称为跳过导航,因为它们有效地跳过联接实体类型。 如果使用的是批量配置,则可以从 GetSkipNavigations获取所有跳过导航。

1
2
3
4
5
6
7
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
foreach (var skipNavigation in entityType.GetSkipNavigations())
{
Console.WriteLine(entityType.DisplayName() + "." + skipNavigation.Name);
}
}

联接实体类型配置

将配置应用于联接实体类型是很常见的。 此操作可通过 UsingEntity 完成。

1
2
3
4
5
modelBuilder
.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity(j => j.ToTable("PostTags"));

其他数据可存储在联接实体类型中,但为此最好创建一个定制的 CLR 类型。 使用自定义联接实体类型配置关系时,需要显式指定这两个外键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
internal class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}

public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<PostTag>(
j => j
.HasOne(pt => pt.Tag)
.WithMany(t => t.PostTags)
.HasForeignKey(pt => pt.TagId),
j => j
.HasOne(pt => pt.Post)
.WithMany(p => p.PostTags)
.HasForeignKey(pt => pt.PostId),
j =>
{
j.Property(pt => pt.PublicationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
j.HasKey(t => new { t.PostId, t.TagId });
});
}
}

public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }

public ICollection<Tag> Tags { get; set; }
public List<PostTag> PostTags { get; set; }
}

public class Tag
{
public string TagId { get; set; }

public ICollection<Post> Posts { get; set; }
public List<PostTag> PostTags { get; set; }
}

public class PostTag
{
public DateTime PublicationDate { get; set; }

public int PostId { get; set; }
public Post Post { get; set; }

public string TagId { get; set; }
public Tag Tag { get; set; }
}

联接关系配置

EF 对联接实体类型使用两个一对多关系来表示多对多关系。 可在 UsingEntity 参数中配置这些关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<Dictionary<string, object>>(
"PostTag",
j => j
.HasOne<Tag>()
.WithMany()
.HasForeignKey("TagsId")
.HasConstraintName("FK_PostTag_Tags_TagId")
.OnDelete(DeleteBehavior.Cascade),
j => j
.HasOne<Post>()
.WithMany()
.HasForeignKey("PostsId")
.HasConstraintName("FK_PostTag_Posts_PostId")
.OnDelete(DeleteBehavior.ClientCascade));

间接多对多关系

还可表示多对多关系,只需添加联接实体类型并映射两个单独的一对多关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}

public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PostTag>()
.HasKey(t => new { t.PostId, t.TagId });

modelBuilder.Entity<PostTag>()
.HasOne(pt => pt.Post)
.WithMany(p => p.PostTags)
.HasForeignKey(pt => pt.PostId);

modelBuilder.Entity<PostTag>()
.HasOne(pt => pt.Tag)
.WithMany(t => t.PostTags)
.HasForeignKey(pt => pt.TagId);
}
}

public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }

public List<PostTag> PostTags { get; set; }
}

public class Tag
{
public string TagId { get; set; }

public List<PostTag> PostTags { get; set; }
}

public class PostTag
{
public DateTime PublicationDate { get; set; }

public int PostId { get; set; }
public Post Post { get; set; }

public string TagId { get; set; }
public Tag Tag { get; set; }
}

索引

索引是许多数据存储中的常见概念。 尽管它们在数据存储中的实现可能会有所不同,但它们可用于使基于列(或一组列)的查找更加高效。
可对列指定索引,如下所示:

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasIndex(b => b.Url);
}

根据约定,会在用作外键的每个属性(或属性集)中创建索引

复合索引

索引还可以跨多个列:

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>()
.HasIndex(p => new { p.FirstName, p.LastName });
}

索引唯一性

默认情况下,索引不唯一:对于索引的列集,允许多行具有相同的值。
可使索引唯一,如下所示:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasIndex(b => b.Url)
.IsUnique();
}

尝试为索引的列集插入多个具有相同值的实体将导致引发异常。

索引排序顺序

在大多数数据库中,索引涵盖的每个列可以是升序或降序。 对于仅包含一列的索引,这通常并不重要:数据库可以根据需要以相反的顺序遍历索引。 但是,对于复合索引,排序对于良好的性能至关重要,并且可以表示查询是否使用索引之间的差异。 通常,索引列的排序顺序应对应于查询的 ORDER BY 子句中指定的排序顺序。

索引排序顺序默认为升序。 可以按如下所示使所有列按降序排列:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasIndex(b => new { b.Url, b.Rating })
.IsDescending();
}

还可以按列指定排序顺序,如下所示:

1
2
3
4
5
6
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> 将成为以下划线分隔的属性名称列表。

可设置在数据库中创建的索引的名称:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasIndex(b => b.Url)
.HasDatabaseName("Index_Url");
}

索引筛选器

通过某些关系数据库,可指定筛选索引或部分索引。 这使你可以仅索引列值的子集,从而减少索引的大小并改善性能和磁盘空间的使用情况。
可使用 Fluent API 在索引上指定筛选器(以 SQL 表达式的形式提供):

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasIndex(b => b.Url)
.HasFilter("[Url] IS NOT NULL");
}

EF 为作为唯一索引一部分的所有可为 null 列添加 ‘IS NOT NULL’ 筛选器。 若要替代此约定,可提供一个 null 值。

1
2
3
4
5
6
7
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasIndex(b => b.Url)
.IsUnique()
.HasFilter(null);
}

包含列

通过某些关系数据库,可配置一组列,这些列包含在索引中,但不是其“键”的一部分。 当查询中的所有列都作为键列或非键列包含在索引中时,这可以显著提高查询性能,因为无需访问表本身。 在以下示例中,Url 列是索引键的一部分,因此对该列的任何查询筛选都可以使用索引。 但除此之外,仅访问 Title 和 PublishedOn 列的查询将不需要访问表,并且会更高效地运行:

1
2
3
4
5
6
7
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasIndex(p => p.Url)
.IncludeProperties(
p => new { p.Title, p.PublishedOn });
}

检查约束

检查约束是一项标准关系功能,让你可以定义一个条件,该条件必须适用于表中的所有行;任何违反约束的插入或修改数据的尝试都将失败。 检查约束类似于非 null 约束(禁止列中的空值)或唯一约束(禁止重复),但允许定义任意 SQL 表达式。

可使用 Fluent API 指定表的检查约束(以 SQL 表达式的形式提供):

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Product>()
.ToTable(b => b.HasCheckConstraint("CK_Prices", "[Price] > [DiscountedPrice]"));
}

继承

EF 可以将 .NET 类型层次结构映射到数据库。 这允许你像往常一样使用基类型和派生类型在代码中编写 .NET 实体,并让 EF 无缝创建适当的数据库架构、发出查询等。有关如何映射类型层次结构的实际细节取决于提供程序;本页介绍关系数据库上下文中的继

实体类型层次结构映射(TPH)

如果要映射层次结构中的 CLR 类型,就必须在模型上显式指定该类型。 例如,仅指定层次结构的基类型不会导致 EF Core 隐式包含其所有子类型。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 实例没有该属性。

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

每个层次结构一张表和鉴别器配置(TPT)

TPH 使用单个表来存储层次结构中所有类型的数据,并使用鉴别器列来标识每行表示的类型。

上面的模型映射到以下数据库架构(注意隐式创建的 Discriminator 列,它标识了每行中存储的 Blog 类型)。

可以配置鉴别器列的名称和类型以及用于标识层次结构中每种类型的值:

1
2
3
4
5
6
7
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasDiscriminator<string>("blog_type")
.HasValue<Blog>("blog_base")
.HasValue<RssBlog>("blog_rss");
}

在上面的示例中,EF 在层次结构的基本实体上隐式添加了鉴别器作为影子属性。 可以像配置任何其他属性一样配置此属性:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property("Discriminator")
.HasMaxLength(200);
}

最后,鉴别器也可以映射到实体中的常规 .NET 属性:

1
2
3
4
5
6
7
8
9
10
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasDiscriminator(b => b.BlogType);

modelBuilder.Entity<Blog>()
.Property(e => e.BlogType)
.HasMaxLength(200)
.HasColumnName("blog_type");
}

查询使用 TPH 模式的派生实体时,EF Core 会在查询中添加一个基于鉴别器列的谓词。
此筛选器确保对于结果中没有的基类型或同级类型,我们不会获得任何附加行。
对于基本实体类型,将跳过此筛选器谓词,因为查询基本实体将获得层次结构中所有实体的结果。
在具体化查询结果时,如果遇到未映射到模型中任何实体类型的鉴别器值,我们将引发异常,因为我们不知道如何具体化结果。
仅当数据库包含的行具有鉴别器值并且这些值未映射到 EF 模型时,才会发生此错误。
如果你有这样的数据,可以将 EF Core 模型中的鉴别器映射标记为不完整,以指示我们应始终添加筛选器谓词来查询层次结构中的任意类型。 IsComplete(false) 在鉴别器配置上调用会将映射标记为不完整。

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasDiscriminator()
.IsComplete(false);
}

共享列

默认情况下,当层次结构中的两个同级实体类型具有同名的属性时,它们将映射到两个单独的列。 但是,如果它们的类型相同,则可以映射到相同的数据库列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MyContext : DbContext
{
public DbSet<BlogBase> Blogs { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.HasColumnName("Url");

modelBuilder.Entity<RssBlog>()
.Property(b => b.Url)
.HasColumnName("Url");
}
}

public abstract class BlogBase
{
public int BlogId { get; set; }
}

public class Blog : BlogBase
{
public string Url { get; set; }
}

public class RssBlog : BlogBase
{
public string Url { get; set; }
}

使用强制转换查询共享列时,关系数据库提供程序(例如 SQL Server)不会自动使用鉴别器谓词。 查询 Url = (blog as RssBlog).Url 还将返回同级 Blog 行的 Url 值。 若要将查询限制为 RssBlog 实体,你需要在鉴别器上手动添加筛选器,例如 Url = blog is RssBlog ? (blog as RssBlog).Url : null。

每个类型一张表配置

在 TPT 映射模式中,所有类型都分别映射到各自的表。 仅属于某个基类型或派生类型的属性存储在映射到该类型的一个表中。 映射到派生类型的表还存储将派生表与基表联接的外键。

1
2
modelBuilder.Entity<Blog>().ToTable("Blogs");
modelBuilder.Entity<RssBlog>().ToTable("RssBlogs");

ToTable可以针对每个根实体类型调用 ,而不是对每个实体类型调用 modelBuilder.Entity().UseTptMappingStrategy() ,并且表名称将由 EF 生成。

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE [Blogs] (
[BlogId] int NOT NULL IDENTITY,
[Url] nvarchar(max) NULL,
CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);

CREATE TABLE [RssBlogs] (
[BlogId] int NOT NULL,
[RssUrl] nvarchar(max) NULL,
CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId]),
CONSTRAINT [FK_RssBlogs_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([BlogId]) ON DELETE NO ACTION
);

在许多情况下,与 TPH 相比,TPT 性能较差。 有关详细信息,请参阅性能文档。

每个具体表类型(TPC)

1
2
3
4
modelBuilder.Entity<Blog>().UseTpcMappingStrategy()
.ToTable("Blogs");
modelBuilder.Entity<RssBlog>()
.ToTable("RssBlogs");

ToTable将按约定生成表名称,而不是在每个实体类型上调用 ,而只需在每个根实体类型上调用 modelBuilder.Entity().UseTpcMappingStrategy() 。

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE [Blogs] (
[BlogId] int NOT NULL DEFAULT (NEXT VALUE FOR [BlogSequence]),
[Url] nvarchar(max) NULL,
CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);

CREATE TABLE [RssBlogs] (
[BlogId] int NOT NULL DEFAULT (NEXT VALUE FOR [BlogSequence]),
[Url] nvarchar(max) NULL,
[RssUrl] nvarchar(max) NULL,
CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId])
);

TPC 数据库架构

TPC 策略类似于 TPT 策略,只是为层次结构中的每个 具体 类型创建不同的表,但 不会 为 抽象 类型创建表,因此名称为“table-per-concrete-type”。 与 TPT 一样,表本身指示保存的对象的类型。 但是,与 TPT 映射不同,每个表都包含具体类型及其基类型中每个属性的列。 TPC 数据库架构非规范化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public abstract class Animal
{
protected Animal(string name)
{
Name = name;
}

public int Id { get; set; }
public string Name { get; set; }
public abstract string Species { get; }

public Food? Food { get; set; }
}

public abstract class Pet : Animal
{
protected Pet(string name)
: base(name)
{
}

public string? Vet { get; set; }

public ICollection<Human> Humans { get; } = new List<Human>();
}

public class FarmAnimal : Animal
{
public FarmAnimal(string name, string species)
: base(name)
{
Species = species;
}

public override string Species { get; }

[Precision(18, 2)]
public decimal Value { get; set; }

public override string ToString()
=> $"Farm animal '{Name}' ({Species}/{Id}) worth {Value:C} eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Cat : Pet
{
public Cat(string name, string educationLevel)
: base(name)
{
EducationLevel = educationLevel;
}

public string EducationLevel { get; set; }
public override string Species => "Felis catus";

public override string ToString()
=> $"Cat '{Name}' ({Species}/{Id}) with education '{EducationLevel}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Dog : Pet
{
public Dog(string name, string favoriteToy)
: base(name)
{
FavoriteToy = favoriteToy;
}

public string FavoriteToy { get; set; }
public override string Species => "Canis familiaris";

public override string ToString()
=> $"Dog '{Name}' ({Species}/{Id}) with favorite toy '{FavoriteToy}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Human : Animal
{
public Human(string name)
: base(name)
{
}

public override string Species => "Homo sapiens";

public Animal? FavoriteAnimal { get; set; }
public ICollection<Pet> Pets { get; } = new List<Pet>();

public override string ToString()
=> $"Human '{Name}' ({Species}/{Id}) with favorite animal '{FavoriteAnimal?.Name ?? "<Unknown>"}'" +
$" eats {Food?.ToString() ?? "<Unknown>"}";
}

为此层次结构创建的表为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
CREATE TABLE [Cats] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Vet] nvarchar(max) NULL,
[EducationLevel] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]));

CREATE TABLE [Dogs] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Vet] nvarchar(max) NULL,
[FavoriteToy] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]));

CREATE TABLE [FarmAnimals] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Value] decimal(18,2) NOT NULL,
[Species] nvarchar(max) NOT NULL,
CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]));

CREATE TABLE [Humans] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[FavoriteAnimalId] int NULL,
CONSTRAINT [PK_Humans] PRIMARY KEY ([Id]));

请注意:

没有 用于 Animal 或 Pet 类型的表,因为这些表位于 abstract 对象模型中。 请记住,C# 不允许抽象类型的实例,因此不存在将抽象类型实例保存到数据库的情况。

对于每个具体类型,将重复基类型中的属性映射。 例如,每个表都有一列 Name ,Cats 和 Dogs 都有一列 Vet 。

密钥生成

选择的继承映射策略会对生成和管理主键值的方式产生影响。 TPH 中的键很简单,因为每个实体实例都由单个表中的一行表示。 可以使用任何类型的键值生成,并且不需要其他约束。

对于 TPT 策略,表中始终有一行映射到层次结构的基类型。 可以在此行上使用任何类型的键生成,其他表的键使用外键约束链接到此表。

对于 TPC 来说,事情会稍微复杂一些。 首先,请务必了解 EF Core 要求层次结构中的所有实体都具有唯一的键值,即使实体具有不同的类型。 例如,使用我们的示例模型,狗不能具有与 Cat 相同的 Id 键值。 其次,与 TPT 不同,没有共同表可以充当键值所在的单个位置,并且可以生成键值。 这意味着无法使用简单 Identity 列。

对于支持序列的数据库,可以使用每个表的默认约束中引用的单个序列来生成键值。 这是上面所示的 TPC 表中使用的策略,其中每个表具有以下项:

1
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])

AnimalSequence 是 EF Core 创建的数据库序列。 将 EF Core 数据库提供程序用于SQL Server时,默认将此策略用于 TPC 层次结构。 支持序列的其他数据库的数据库提供程序应具有类似的默认值。 使用序列的其他密钥生成策略(如 Hi-Lo 模式)也可用于 TPC。

虽然标准标识列不适用于 TPC,但如果每个表都配置了适当的种子和增量,则可以使用标识列,以便为每个表生成的值永远不会发生冲突。 例如:

1
2
3
4
modelBuilder.Entity<Cat>().ToTable("Cats", tb => tb.Property(e => e.Id).UseIdentityColumn(1, 4));
modelBuilder.Entity<Dog>().ToTable("Dogs", tb => tb.Property(e => e.Id).UseIdentityColumn(2, 4));
modelBuilder.Entity<FarmAnimal>().ToTable("FarmAnimals", tb => tb.Property(e => e.Id).UseIdentityColumn(3, 4));
modelBuilder.Entity<Human>().ToTable("Humans", tb => tb.Property(e => e.Id).UseIdentityColumn(4, 4));

外键约束

TPC 映射策略创建非规范化的 SQL 架构 - 这是一些数据库纯粹主义者反对它的原因之一。 例如,请考虑外键列 FavoriteAnimalId。 此列中的值必须与某些动物的主键值匹配。 使用 TPH 或 TPT 时,可以使用简单的 FK 约束在数据库中强制实施此操作。

1
CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])

但是,使用 TPC 时,任何给定动物的主键都存储在对应于该动物的具体类型的表中。 例如,猫的主键存储在 Cats.Id 列中,而狗的主键存储在 Dogs.Id 列中,依此而行。 这意味着无法为此关系创建 FK 约束。

实际上,只要应用程序不尝试插入无效数据,就不是问题。 例如,如果所有数据都由 EF Core 插入并使用导航来关联实体,则保证 FK 列将始终保持有效的 PK 值。

摘要和指南

总之,TPH 通常适用于大多数应用程序,并且对于各种方案都是很好的默认值,因此,如果不需要 TPC,请不要增加 TPC 的复杂性。 具体而言,如果代码将主要查询许多类型的实体(例如针对基类型编写查询),则倾向于使用 TPH 和 TPC。

话亦然,当代码主要查询单叶类型的实体,并且基准显示与 TPH 相比,TPC 也是一个很好的映射策略。

仅当受外部因素限制时,才使用 TPT。

序列

序列在数据库中生成唯一的顺序数值。 序列不与特定表关联,可以设置多个表以从同一序列提取值。

基本用法

可以在模型中设置序列,然后使用它为属性生成值:

1
2
3
4
5
6
7
8
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasSequence<int>("OrderNumbers");

modelBuilder.Entity<Order>()
.Property(o => o.OrderNo)
.HasDefaultValueSql("NEXT VALUE FOR OrderNumbers");
}

请注意,从序列生成值的特定SQL是特定于数据库的;上面的示例适用于 SQL Server 但在其他数据库上将失败。 有关详细信息,请查阅数据库的文档。

配置序列设置

还可以配置序列的其他方面,例如其架构、起始值、增量等:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasSequence<int>("OrderNumbers", schema: "shared")
.StartsAt(1000)
.IncrementsBy(5);
}