添加实体
ABP踩坑记录-目录
这里我以问答模块为例,记录一下我在创建实体类过程中碰到的一些坑。
审计属性
具体什么是审计属性我这里就不再介绍了,大家可以参考官方文档。
这里我是通过继承定义好的基类来获得相应的审计属性,大家如果有需求的话,也可以自己通过接口定义。
其中,abp提供的审计基类有两种,一种只包含UserId的FullAuditedEntity
,另一种则是添加了User的导航属性的FullAuditedEntity
,后一种可方便之后用AutoMapper来获取用户信息。
FullAuditedEntity
实质为FullAuditedEntity
这里可能会出现的坑就是一时手误会写成FullAuditedEntity
,这样的话它是把User类型实体的主键,算是不容易察觉的坑。
一对多关系
根据约定,在定义好实体间导航关系之后,EF Core会为其自动创建关系。
但在实际开发中,有时我们并不希望将一些导航属性暴露出来,例如:Image
类理应包含指向Question
和Answer
的导航属性。为此,我们可以通过隐藏属性(Shadow Properties)来化解这一尴尬。
在QincaiDbContext
中,我们重载OnModelCreating
方法:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity(e =>
{
// 添加隐藏属性
e.Property("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
(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(qt =>
{
qt.HasKey(e => new { e.QuestionId, e.TagName });
});
通过表达式的形式,我们可以很方便的创建新的复合主键。
另外,因为在QuestionTag
中的真正主键是QuestionId
和TagName
,所以我们还需要覆盖掉继承来的Id
属性:
public class QuestionTag : Entity
{
///
/// 无效Id,实际Id为QuestionId和TagName
///
[NotMapped]
public override string Id => $"{QuestionId}-{TagName}";
///
/// 问题Id
///
public int QuestionId { get; set; }
///
/// 标签名称
///
public string TagName { get; set; }
// ...
}
默认值
在官方文档中,使用默认值的方式是在构造函数中赋值,这里我使用的是C# 6.0中的属性初始化语法(Auto-property initializers)。从我目前的结果来说,与预期效果基本一致,而且更易于阅读。
形式如下:
public class Question : FullAuditedAggregateRoot, IPassivable
{
///
/// 问题状态(默认为true)
///
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 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 _orderItems = new List();
public IReadOnlyCollection 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是如何调用私有构造的过程,希望知道的大佬能指点一下。