EF Core中对充血模型提供了比较好的支持,本小节我们来学习如何在EF Core中把充血模型风格的实体类映射到数据库表中。
充血模型中的实体类和POCO类相比,有如下的特征:
- 特征一:属性是只读的或者只能被类内部的代码修改。
- 特征二:定义了有参构造方法。
- 特征三:有的成员变量没有对应属性,但是这些成员变量需要映射为数据库表中的列,也就是我们需要把私有成员变量映射到数据库表中的列。
- 特征四:有的属性是只读的,也就是它的值是从数据库中读取出来的,但是我们不能修改属性值。
- 特征五:有的属性不需要映射到数据列,仅在运行时被使用。
我们来研究如何在EF Core中实现这些特征。
首先,我们来实现特征一,也就是”实体类包含只读属性或者属性只能被类内部的代码修改”。
因为EF Core会把数据库表的列直接加载到属性对应的成员变量中,所以我们可以把属性的set定义为private或者init,然后通过构造方法为这些属性赋初始值。
然后,我们实现特征二,也就是”实体类可能包含有参构造方法”。
因为如果开发人员能够调用实体类对象的无参构造方法,那么就有实体类对象存在非法值的可能性,所以我们应该避免用户直接调用实体类的无参构造方法。
EF Core中的实体类如果没有无参构造方法,则有参构造方法中的参数的名字必须和属性的名字一致,因为在EF Core从数据库中加载数据的时候,它会用反射来调用有参构造方法以初始化实体类的对象。
只有构造方法的参数名字和属性的名字一致,EF Core才知道构造方法中参数和数据库表的对应关系。
因此,要避免开发人员调用实体类的无参构造方法,保证EF Core能正确地从数据库中读取数据并赋值给实体类对象,我们有如下两种实现方式:
- 实体类中可以定义无参构造方法,但是无参构造方法定义为private。由于开发人员无法直接调用私有的无参构造方法,而只能调用有参构造方法,这样就可以避免开发人员创建包含非法属性值的对象;EF Core可以调用私有构造方法,因此EF Core在从数据库中加载数据到实体类对象的时候,会调用这个私有构造方法创建实体类的对象,然后对各个属性进行赋值。
- 实体类中不定义无参构造方法,只定义有意义的有参构造方法,但是要求构造方法中的参数和名字和属性的名字一致。无论是开发人员还是EF Core加载数据的时候,都调用这些无参构造方法来完成对象的初始化。
第一种方式比较简单,而且对于有参构造方法的参数名等没有限制,但是这种方式存在着开发人员通过反射调用私有的无参构造方法来创建包含非法值对象的可能。
第二种方式对开发人员和EF Core具有同样的限制,避免了第一种方式中通过反射来跳过限制的问题,不过它对于参数名字等的限制更加严格。
接下来,我们实现特征三,也就是”把不属于属性的成员变量映射为数据列”,在EF Core中我们只要在配置实体类的代码中,使用builder.Property来配置即可。
接下来,我们实现特征四,也就是”从数据列中读取值的只读属性”。
EF Core中提供了”支持字段”(backing field)来支持这种写法,具体用法是,在配置实体类的代码中,使用HashField(“成员变量名”)来配置属性。
最后,我们实现特征五,也就是”不需要映射数据列的属性”。
在EF Core中我们只要在配置实体类的代码中,使用Ignore来配置忽略相关属性即可。
下面通过User类来综合演示这5种特征的实现,如以下代码所示:
/// <summary>
/// 为了让编译器帮我们生成User类的ToString()方法
/// 从而简化在控制台中输出User对象的代码
/// 我们把User类定义为record class
/// </summary>
public record class User
{
//ID由数据库生成自增字段,无法修改值,修饰为init
public int Id { get; init; } //特征一
//在对象创建的时候初始化
public DateTime CreatedDate { get; init; } //特征一
//由类内部的代码修改,因此我们把它们的set操作修饰为私有
public string UserName { get; private set; } //特征一
public int Credit { get; private set; }
//密码不应该被外界读取,因此声明用于存储密码的字段为私有成员变量,并映射到数据库表中
private string? passwordHash; //特征三
//Remark是一个只读属性,它的值只能从数据库中读取
private string? remark;
public string? Remark //特征四
{
get { return remark; }
}
//Tag属性不被映射到数据库表中
public string? Tag { get; set; } //特征五
//采用把无参构造方法设置为私有的方式来实现特征二
private User() { } //特征二
public User(string name)
{
this.UserName = name;
this.CreatedDate = DateTime.Now;
this.Credit = 10;
}
public void ChangeUserName(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("用户名不能为空");
}
this.UserName = name;
}
public void ChangePassword(string password)
{
if (string.IsNullOrEmpty(password))
{
throw new ArgumentException("密码不能为空");
}
this.passwordHash = password;
}
}
因为特征三、四、五还需要通过Fluent API来完成,所以我们对User实体类进行配置,如下面代码所示:
public void Configure(EntityTypeBuilder<User> builder)
{
//配置 User 类中的 passwordHash 字段。
builder.Property("passwordHash"); //特征三
//Remark 是一个公开的属性,但其值存储在私有字段 remark 中。通过 HasField,EF Core 知道应该将数据存储到 remark 字段中
builder.Property(p=>p.Remark).HasField("remark"); //特征四
//如果某些属性只在内存中使用,而不需要存储到数据库中,可以通过 Ignore 方法排除它
builder.Ignore(p => p.Tag); //特征五
}
接下来,我们编写测试代码,如下面代码所示:
User u1 = new("ichi");
u1.Tag = "myTag";
u1.ChangePassword("123456");
_context.Users.Add(u1);
_context.SaveChanges();
因为User类的无参构造方法是私有的,所以我们只能调用设定初始用户名的构造方法,这样就保证了对象值的合法性。
上面的代码会在数据库中插入一条记录,我们修改数据库中Remark列的值为”ICHIDEMO”,然后执行如下代码查询:
User? user = await _context.User.FirstOrDefaultAsync(u => u.UserName == "ICHI");
Console.WriteLine(user);
程序运行结果如下图,这和我们期望的结果一致:
