ABP框架入门踩坑-添加实体
添加实体
这里我以问答模块为例,记录一下我在创建实体类过程中碰到的一些坑。
审计属性
具体什么是审计属性我这里就不再介绍了,大家可以参考官方文档。
这里我是通过继承定义好的基类来获得相应的审计属性,大家如果有需求的话,也可以自己通过接口定义。
其中,abp提供的审计基类有两种,一种只包含UserId的FullAuditedEntity<TPrimaryKey>
,另一种则是添加了User的导航属性的FullAuditedEntity<TPrimaryKey, TUser>
,后一种可方便之后用AutoMapper来获取用户信息。
FullAuditedEntity
实质为FullAuditedEntity<int>
这里可能会出现的坑就是一时手误会写成FullAuditedEntity<User>
,这样的话它是把User类型实体的主键,算是不容易察觉的坑。
一对多关系
根据约定,在定义好实体间导航关系之后,EF Core会为其自动创建关系。
但在实际开发中,有时我们并不希望将一些导航属性暴露出来,例如:Image
类理应包含指向Question
和Answer
的导航属性。为此,我们可以通过隐藏属性(Shadow Properties)来化解这一尴尬。
在QincaiDbContext
中,我们重载OnModelCreating
方法:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Image>(e =>
{
// 添加隐藏属性
e.Property<int>("QuestionId");
// 配置外键
e.HasOne(typeof(Question))
.WithMany(nameof(Question.Images))
.HasForeignKey("QuestionId");
});
}
以上就是完整的步骤,当然有人会觉得奇怪因为完全不做配置也是可以用的,这是EF Core已经根据约定自动为我们创建了隐藏属性:
Shadow properties can be created by convention when a relationship is discovered but no foreign key property is found in the dependent entity class. In this case, a shadow foreign key property will be introduced. The shadow foreign key property will be named
<navigation property name><principal key property name>
(the navigation on the dependent entity, which points to the principal entity, is used for the naming).-- From Microsoft Docs
这里EF Core为我们创建的隐藏属性将命名为<导航属性名称><对应主键名称>
,即像我们这里有一个导航属性Question
,其Question
类的主键为Id
,那么隐藏属性就是QuestionId
。
复合主键
在一些特殊情况下,我们所需的主键可能是由多个属性决定的,比如QuestionTag
就是以QuestionId
和TagName
为主键。
这里我们需要通过Fluent API来进行配置,在重载的OnModelCreating
方法中添加:
modelBuilder.Entity<QuestionTag>(qt =>
{
qt.HasKey(e => new { e.QuestionId, e.TagName });
});
通过表达式的形式,我们可以很方便的创建新的复合主键。
另外,因为在QuestionTag
中的真正主键是QuestionId
和TagName
,所以我们还需要覆盖掉继承来的Id
属性:
public class QuestionTag : Entity<string>
{
/// <summary>
/// 无效Id,实际Id为QuestionId和TagName
/// </summary>
[NotMapped]
public override string Id => $"{QuestionId}-{TagName}";
/// <summary>
/// 问题Id
/// </summary>
public int QuestionId { get; set; }
/// <summary>
/// 标签名称
/// </summary>
public string TagName { get; set; }
// ...
}
默认值
在官方文档中,使用默认值的方式是在构造函数中赋值,这里我使用的是C# 6.0中的属性初始化语法(Auto-property initializers)。从我目前的结果来说,与预期效果基本一致,而且更易于阅读。
形式如下:
public class Question : FullAuditedAggregateRoot<int, User>, IPassivable
{
/// <summary>
/// 问题状态(默认为true)
/// </summary>
public bool IsActive { get; set; } = true;
// ...
}
构造函数
这是个一直被我忽略的地方,在此之前常常使用的是默认空构造函数,但若需要一个有参构造函数,且这个参数并不直接对应某个属性,如:
// 此处仅为举例说明
public class Question
{
public Category Category { get; set; }
// ...
// 这里构造的参数并不直接对应某个属性
public Question(string categoryName)
{
Category = new Category { Name = categoryName };
}
}
当你添加迁移的时候就会报如下错误:No suitable constructor found for entity type 'Question'. The following constructors had parameters that could not be bound to properties of the entity type: cannot bind 'categoryName' in 'Question(string categoryName)'.
大概就是EF Core不能推断出categoryName
是什么。
解决方法很简单,手动添加一个空构造函数即可。
按照常识,我们添加新的构造函数:
public class Question
{
// ...
// 空的构造函数
public Question() {}
}
可事实上,我们并不希望有人使用这个空的构造函数,因为它会缺少一些空值检测等判定。
经过查找资料,我在微软的eShopOnWeb示例项目中找到了如下写法:
public class Order : BaseEntity, IAggregateRoot
{
// 注意这里是private
private Order()
{
// required by EF
}
// 含参构造函数包括了空值检测
public Order(string buyerId, Address shipToAddress, List<OrderItem> items)
{
Guard.Against.NullOrEmpty(buyerId, nameof(buyerId));
Guard.Against.Null(shipToAddress, nameof(shipToAddress));
Guard.Against.Null(items, nameof(items));
BuyerId = buyerId;
ShipToAddress = shipToAddress;
_orderItems = items;
}
public string BuyerId { get; private set; }
public DateTimeOffset OrderDate { get; private set; } = DateTimeOffset.Now;
public Address ShipToAddress { get; private set; }
private readonly List<OrderItem> _orderItems = new List<OrderItem>();
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();
// ...
}
回过头,我又去确认了EF Core的文档:
When EF Core creates instances of these types, such as for the results of a query, it will first call the default parameterless constructor and then set each property to the value from the database
...
The constructor can be public, private, or have any other accessibility.
-- From Microsoft Docs
也就是,EF Core在创建实例时,会首先去调用无参构造函数,且无论该构造函数是何访问类型。
那么问题就解决了,我们只需添加私有的无参构造函数即可。
PS:但还是没找到EF Core是如何调用私有构造的过程,希望知道的大佬能指点一下。