添加实体

添加实体

这里我以问答模块为例,记录一下我在创建实体类过程中碰到的一些坑。

类图

审计属性

具体什么是审计属性我这里就不再介绍了,大家可以参考官方文档

这里我是通过继承定义好的基类来获得相应的审计属性,大家如果有需求的话,也可以自己通过接口定义。

其中,abp 提供的审计基类有两种,一种只包含 UserId 的FullAuditedEntity<TPrimaryKey>,另一种则是添加了 User 的导航属性的FullAuditedEntity<TPrimaryKey, TUser>,后一种可方便之后用 AutoMapper 来获取用户信息。

FullAuditedEntity 实质为 FullAuditedEntity<int>

这里可能会出现的坑就是一时手误会写成 FullAuditedEntity,这样的话它是把 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 是如何调用私有构造的过程,希望知道的大佬能指点一下。

支付宝收款码
Copyright © yiluomyt 2020