实体 (Entities)
实体是 Entity Framework Core 5 的核心. 一个实体是一个对象, 它映射到一个或多个数据库中的表. Entity Framework Core 5 使用 code-first 方法 (代码先行方法). 也就是说, 你创建实体, Entity Framework 会生成数据库架构来存储你的实体数据.
Person 实体
我们第一个实体是 Person
实体, 它含有 Id
, first name
, last name
, 以及一对多的地址 (address
). 地址也有自己的实体. 第一步, 我们先创建一个没有地址的 Person
实体.
首先, 在 Core
项目中创建一个 Entities
文件夹. 然后, 添加 Person
类到 Entities
文件夹中. 然后依次添加 Id
属性, FirstName
, LastName
, 以及 EmailAddress
字符串属性, 如下所示:
namespace EFCore5WebApp.Core.Entities {
public class Person {
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string EmailAddress { get; set; }
}
}
Address 实体
现在创建 Address
类. Address
类含有 Id
, 地址1
, 地址2
, 城市
, 州
, 国家
, person Id
, 以及邮编
属性, 如下所示:
namespace EFCore5WebApp.Core.Entities {
public class Address {
public int Id { get; set; }
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Country { get; set; }
public string ZipCode { get; set; }
public int PersonId { get; set; }
}
}
简单的导航属性 (Simple Navigation Property)
现在, 我们将 Address
导航属性加到 Person
类中. 这样就可以让一个 person
拥与 address
记录一对多的关系了. 如下所示:
namespace EFCore5WebApp.Core.Entities {
public class Person {
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string EmailAddress { get; set; }
public List<Address> Addresses { get; set; } = new List<Address>();
}
}
导航属性的细节在第11章做详细介绍. 它的目的是链接一个实体与另一个实体. 在上述代码中, Addresses
属性是一个 Address
值的列表, 并且每一个 Address
值都会有一个 Id
值.
在我们的示例中, 我们定义了一对多关系, 在 Person
实体以及它映射到 Address
实体集合之间. 导航属性也可以是一对一的关系, 只需要将属性设置为实体属性, 而非集合即可.
实体属性约束
Entity Framework 有两种方式来配置实体之外的默认列与关系设置. 本章中, 我们会介绍如何使用数据注解 (data annotation) 来添加实体属性约束. 下一章中, 我们会介绍 fluent API 来进行配置. 两种方式都是有效的方法, 在约束实体上. 并且各有所长.
为了在我们的 .NET 标准 "Core" 项目中使用特性 (attribute
), 我们需要安装 "System.ComponentModel.DataAnnotations"
NuGet 包, 如下图所示.
然后, 使用 Required
特性来修饰 FirstName
, LastName
, 和 EmailAddress
列, 来设置其为必须提供. 如下所示:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace EFCore5WebApp.Core.Entities
{
public class Person
{
public int Id { get; set; }
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
[Required]
public string EmailAddress { get; set; }
public List<Address> Addresses { get; set; } = new List<Address>();
}
}
此外, 我们可以限制字符串属性的长度, 通过 MaxLength
特性来修饰类属性. 下面设置 FirstName
和 LastName
属性值字符数最多为 255 个字符. 如下所示:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace EFCore5WebApp.Core.Entities
{
public class Person
{
public int Id { get; set; }
[Required]
[MaxLength(255)]
public string FirstName { get; set; }
[Required]
[MaxLength(255)]
public string LastName { get; set; }
[Required]
public string EmailAddress { get; set; }
public List<Address> Addresses { get; set; } = new List<Address>();
}
}
当 Entity Framework Core 去创建数据库架构时, FirstName
和 LastName
字段会被创建为 nvarchar(255)
列, 并且两个列不允许为 NULL
, 因为它们被设置为 required
.
实体架构特性 (Entity Schema Attribute)
类级别的实体架构特性可以用于定义整个实体的配置. 一个十分普遍的配置便是 Table
注解, 它可以定义实体对应在数据库中的 table
以及 schema
.
例如, 如果我们想要存储 Person
实体的数据表命名为 Persons
, 我们可以使用 Table
注解来修饰该类, 并传入 "Persons"
. 如下所示:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace EFCore5WebApp.Core.Entities
{
[Table("Persons")]
public class Person
{
public int Id { get; set; }
[Required]
[MaxLength(255)]
public string FirstName { get; set; }
[Required]
[MaxLength(255)]
public string LastName { get; set; }
[Required]
public string EmailAddress { get; set; }
public List<Address> Addresses { get; set; } = new List<Address>();
}
}
不仅如此, 你还可以在一个给定的数据库架构中创建一个实体, 只需要在 Table
特性中指定 Schema
属性即可. 例如, 要使用 dbo
架构来存储 person
记录. 你可以使用:
[Table("Persons", Schema = "dbo")]
public class Person
然后 Person
类就会被存储在 dbo
架构中, 名为 Persons
的表中.
更多实体架构特性
另一个常用的实体架构特性是 NotMapped
特性. 它如其名, 表示不会在数据库中存储相关的属性. 针对计算属性, 不存储该属性是很有用的. 例如, 我们可以添加一个全名 (FullName
) 属性, 它是连接 FirstName
和 LastName
得到的, 如下所示:
[NotMapped]
public string FullName => $"{FirstName} {LastName}";
如果你希望数据库的列名不使用属性名, 而使用其他的名字. 你可以使用 Column
属性特性. 你传入列名到该特性中, 如下所示:
[Column("Person_Id")]
public int Id { get; set; }
这在某些情况下非常好用. 比如, 在你的团队中有数据库管理员, 并且它希望使用不同的命名约定, 而你希望在代码中使用实体.
Column
特性还有 TypeName
属性, 就像它的名字一样, 你可以使用该属性来设置数据库列的类型. 例如, 你想使用 varchar
类型来代替 nvarchar
类型, 在 FirstName
属性上. 你只需要告诉 Entity Framework 映射 FirstName
为 varchar(255)
, 如下所示:
[Column(TypeName = "varchar(255)")]
public string FirstName { get; set; }
映射主键列
Entity Framework 默认会标记名为 Id
或 EntityNameId
的属性为为主键 (Primary Key). 也就是说, 我们的 Person
类, Entity Framework 会将 Id
属性设置为数据库的主键.
比较在意是
EntityNameId
还是PersonId
. 猜测是需要将EntityName
替换成模型名.
如果你想修改这个命名规则, 那么你需要简单的使用 Key
属性, 例如:
[Key]
public int Person_Id { get; set; }
有些时候使用 Key 注解很有用, 例如一个检查表, 包含字符编码, 描述, 以及类型. 若你想使用 Code
作为主键, 可以参考如下代码:
namespace EFCore5WebApp.Core.Entities
{
public class LookUp
{
[Key]
public string Code { get; set; }
public string Description { get; set; }
public int LookUpType { get; set; }
}
}
枚举映射
Entity Framework 有一个很有用的特性. 它可以将枚举数据映射到数据库中的一个整数上. 然而在实体中, 我们依旧可以使用枚举类型. 在我们的代码中, 我会使用一个通用的 lookup
表来存储州和国家数据, 并将其类型设置为实体中的枚举. 如下所示:
namespace EFCore5WebApp.Core.Entities
{
public enum LookUpType
{
State = 0,
Country = 1
}
public class LookUp
{
[Key]
public string Code { get; set; }
public string Description { get; set; }
public LookUpType LookUpType { get; set; }
}
}
LookUp
可以被解释为 查找. 不明白作者意图, 可能是想一同一种查找类型. 按照州名查找, 或国家名查找所做的标记值吧? 也许.
实体继承映射 [???]
这部分逻辑有些不明. 解释不够.
在 Entity Framework Core 5 中有两种继承树的类型 (type of inheritance trees). 分别是 table-per-hierarchy 和 table-per-type. 你在使用继承 (派生类) 时, 默认采用的是 table-per-hierarchy. 它会将层级合并到一个单一的表中. 如果你要使用 table-per-type, 你需要使用 [Table()] 特性, 在派生类型上, 去要求 Entity Framework Core 5 去映射派生类型到它自身的表上. 下面我们来看看两种方法的细节.
Table-per-Hierarchy
使用 Table-per-Hierarchy 时, 你定义了基类, 和派生类. 并指定鉴别列 (discrimination column), 用于找出匹配类型 (基类/派生类) 的行. 你只允许有一个子类, 在使用 Table-per-Hierarchy 继承时, 该类会被包含[不知云云, 不知道是否是我翻译有问题]. 例如, 我们有一个 Shape
实体, 它有 Width
和 Height
属性. 然后我们希望从 Shape
派生出 Cube
实体, 它会增加 Length
属性. 我们的代码看起来会是:
public class Shape {
public int Width { get; set; }
public int Height { get; set; }
}
public class Cube: Shape {
public int Length { get; set; }
}
public class MyContext: DbContext {
public DbSet<Shape> Shapes { get; set; }
public DbSet<Cybe> Cube { get; set; }
}
在我们的代码中, Entity Framework 会创建一个影子属性, 名为 "Discriminator"
, 用于存储实体的名字. 你可以显式的使用 DbContext
类中的 OnModelCreating()
方法来映射该影子属性. 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<Shape>()
.HasDiscriminator<string>("ShapeType")
.HasValue<Shape>("S")
.HasValue<Cube>("C");
}
在上面的案例中, 我们将 discrimination column 命名为 "ShapeType"
, 并将 Shape
的行的值设置为 "S"
, 将 Cube
行的值设为 "C"
.
Table-per-Type
Table-per-Type 继承映射允许你将派生实体存储在其自身的表中. 这是 Entity Framework Core 5 中引入的新特性. 我们有两种方式显式的告诉 Entity Framework Core 5 我们想要使用的表名. 两种方式分别为 Table
特性, 以及 OnModelCreating()
方法中的 模型 builder API.
首先我们来看看 Table
特性. 我们将代码修改为如下形式:
[Table("Shape")]
public class Shape {
public int Width { get; set; }
public int Height { get; set; }
}
[Table("Cube")]
public class Cube: Shape {
public int Length { get; set; }
}
class MyContext: DbContext {
public DbSet<Shape> Shapes { get; set; }
public DbSet<Cube> Cube { get; set; }
}
第二种方法, 是使用 DbContext
中的 OnModelCreating()
方法. 这样我们可以完全不用注解. 要实现这个结果, 我们在 OnModelCreating()
方法中的模型 builder API 上使用 ToTable()
方法. 如下所示:
public class Shape {
public int Width { get; set; }
public int Height { get; set; }
}
public class Cube: Shape {
public int Length { get; set; }
}
class MyContext: DbContext {
public DbSet<Shape> Shapes { get; set; }
public DbSet<Cube> Cube { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<Shape>().ToTable("Shape");
modleBuilder.Entity<Cube>().ToTable("Cube");
}
}
小结
本章中, 我们包含了很多的内容. 你学习到了如何创建一个简单的 Person
实体对象, 并且它可以带有 Address
导航属性. 不仅如此, 我们还介绍了如何使用 Table
类特性来修改 Entity Framework Core
映射到最终数据库表. 然后, 我们介绍了如何进一步配置实体属性映射, 通过属性特性来提供指令, 指示 Entity Framework Core 如何构建映射的数据库列. 会在第4,5章中生成它.
下一章, 我们会介绍 Entity Framework Core 数据库上下文类的细节. 数据库上下文类会用于连接我们的实体和目标数据库. 使用它来执行创建, 读取, 更新, 删除, 以及聚合等操作. 所有的这些细节, 会在后续的章节中进行介绍.