ABP框架入門踩坑-添加實體


添加實體

ABP踩坑記錄-目錄

這里我以問答模塊為例,記錄一下我在創建實體類過程中碰到的一些坑。

類圖

審計屬性

具體什么是審計屬性我這里就不再介紹了,大家可以參考官方文檔

這里我是通過繼承定義好的基類來獲得相應的審計屬性,大家如果有需求的話,也可以自己通過接口定義。

其中,abp提供的審計基類有兩種,一種只包含UserId的FullAuditedEntity<TPrimaryKey>,另一種則是添加了User的導航屬性的FullAuditedEntity<TPrimaryKey, TUser>,后一種可方便之后用AutoMapper來獲取用戶信息。

FullAuditedEntity實質為FullAuditedEntity<int>

這里可能會出現的坑就是一時手誤會寫成FullAuditedEntity<User>,這樣的話它是把User類型實體的主鍵,算是不容易察覺的坑。

一對多關系

根據約定,在定義好實體間導航關系之后,EF Core會為其自動創建關系。

但在實際開發中,有時我們並不希望將一些導航屬性暴露出來,例如:Image類理應包含指向QuestionAnswer的導航屬性。為此,我們可以通過隱藏屬性(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就是以QuestionIdTagName為主鍵。

這里我們需要通過Fluent API來進行配置,在重載的OnModelCreating方法中添加:

modelBuilder.Entity<QuestionTag>(qt =>
{
    qt.HasKey(e => new { e.QuestionId, e.TagName });
});

通過表達式的形式,我們可以很方便的創建新的復合主鍵。

另外,因為在QuestionTag中的真正主鍵是QuestionIdTagName,所以我們還需要覆蓋掉繼承來的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是如何調用私有構造的過程,希望知道的大佬能指點一下。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM