ABP框架之——資料訪問基礎架構

張飛洪[廈門]發表於2022-05-25

大家好,我是張飛洪,感謝您的閱讀,我會不定期和你分享學習心得,希望我的文章能成為你成長路上的一塊墊腳石,我們一起精進。

幾乎所有的業務應用程式都要適用一種資料庫基礎架構,用來實現資料訪問邏輯,以便從資料庫讀取或寫入資料,我們還需要處理資料庫事務,以確保資料來源中的一致性。

ABP框架可以與任何資料庫相容,同時它提供了EF Core和MongoDB的內建整合包。您將通過定義DbContext類、將實體對映到資料庫表、實現倉儲庫以及在有實體時部署載入相關實體的不同方式,學習如何將EF Core與ABP框架結合使用。您還將看到如何將MongoDB用作第二個資料庫提供程式選項。

本章介紹了ABP的基本資料訪問架構,包括以下主題:

  • 定義實體
  • 定義D庫
  • EF核心整合
  • 瞭解UoW

ABP通過介面和基類來標準化實體的定義

1 定義實體

1.1 聚合根類(AggregateRoot)

聚合一般包括多個實體或者值物件,聚合根可以理解為根實體或者叫主實體。聚合的概念我們會在後面第10節的DDD會詳細講到,這裡只是做個大概瞭解。

在ABP框架中,您可以從一個AggregateRoot類派生來定義主實體和聚合根,BasicAggregateRoot是定義聚合根的最簡單的類。
以下示例實體類派生自BasicAggregateRoot類:

namespace FormsApp
{
    public class Form : BasicAggregateRoot<Guid> //
    {
        public string Name { get; set; }
        public string Description { get; set; }
        public bool IsDraft { get; set; }
        public ICollection<Question> Questions { get; set; }
    }
}

BasicAggregateRoot只是將Id屬性定義為PK,並將PK型別作為泛型引數。在本例中,Form的PK型別是Guid。只要底層資料庫支援,就可以使用任何型別作為PK(例如intstring等)。

還有其他一些基類可以從中派生聚合根,如下所述:

  • AggregateRoot 有其他屬性來支援樂觀併發和物件擴充套件特性
  • CreationAuditedAggregateRoot 繼承自 AggregateRoot類,並新增 CreationTime (DateTime) 和 CreatorId (Guid) 屬性來儲存建立稽核資訊。
  • AuditedAggregateRoot 繼承* CreationAuditedAggregateRoot類,並新增 LastModificationTime (DateTime) 和LastModifierId (Guid)屬性來儲存修改稽核資訊。
  • FullAuditedAggregateRoot繼承自AuditedAggregateRoot類,並新增 DeletionTime (DateTime) 和 DeleterId (Guid) 屬性來儲存刪除稽核資訊。它還通過實現ISoftDelete介面新增了IsDeleted (bool),實現實體軟刪除。

1.2 實體類(Entity)

Entity基類類似於AggregateRoot類,但它們用於子集合實體,而不是主(根)實體。例如,上面的Form聚合根示例包含一系列問題子實體集合,它派生自實體類,如以下程式碼段所示:

public class Question : Entity<Guid> //
{
    public Guid FormId { get; set; }
    public string Title { get; set; }
    public bool AllowMultiSelect { get; set; }
    //public ICollection<Option> Options { get; set; }
}

AggregateRoot類一樣,Entity類還定義了給定型別的Id屬性。在本例中,Question實體還有一組Option,其中Option是另一種實體型別。

還有一些其他預定義的基本實體類,如CreationAuditedEntityAuditedEntityFullAuditedEntity。它們類似於上面介紹的審計聚合根類。

1.3 帶複合主鍵實體

關聯式資料庫支援CPK(複合鍵),即PK由多個值組成,複合鍵對於具有多對多關係表特別有用。
假設要為Form設定多個Managers,向Form類新增Managers集合屬性,如下所示:

public class Form : BasicAggregateRoot<Guid>
{
    ...
    public ICollection<FormManager> Managers { get; set; }
}
public class FormManager : Entity
{
    public Guid FormId { get; set; }
    public Guid UserId { get; set; }
    public Guid IsOwner { get; set; }
    public override object[] GetKeys()
    {
        return new object[] {FormId, UserId};
    }
}

從非泛型Entity類繼承時,必須實現GetKeys方法以返回鍵陣列。這樣,ABP可以在需要的地方使用CPK值。在本例中,FormIdUserId是其他表的FK,它們構建FormManager實體的CPK。

聚合根的CPKs

AggregateRoot類也有用於CPK的非通用版本,但為聚合根實體設定CPK並不常見。

1.4 GUID主鍵

ABP主要使用GUIDs作為預構建實體的PK型別。GUIDs通常與自動增量IDs(如intlong,由關聯式資料庫支援)進行比較。與自動遞增鍵相比,使用GUIDs作為PK有一些眾所周知的好處:

GUID vs 自動增量ID

1)GUID優點:

  • GUID 全域性唯一,適合分散式系統,方便拆分或合併表。
  • 無需資料庫往返即可在客戶端生成 GUID。
  • GUID 是無法猜測的,某些情況下它們可能更安全(例如,如果終端使用者看到一個實體的 ID,他們就找不到另一個實體的 ID)。

與自動遞增整數值相比,GUID也有一些缺點,如下所示:

2)GUID缺點:

  • GUID 佔16個位元組,int 4個位元組, long 8個位元組。
  • GUID 本質上不是連續的,這會導致聚集索引出現效能問題。

ABP 提供IGuidGenerator,預設生成順序Guid值,解決了聚集索引的效能問題。建議用IGuidGenerator設定Id,而不是Guid.NewGuid(),如果你不設定Id,倉儲庫預設會使用IGuidGenerator

GUID與自動增量PKs是軟體開發中的熱門話題,目前還沒有明確的贏家。ABP適用於任何PK型別,因此您可以根據自己的需求進行選擇。

Repository模式是抽象資料訪問程式碼的常用方法。在接下來的部分中,您將學習如何使用ABP框架的通用儲存庫方法查詢或運算元據庫中的資料。當需要擴充套件通用儲存庫並新增自己的儲存庫方法時,您還可以建立自定義儲存庫。

2 定義倉儲庫

2.1 通用倉儲庫

一旦有了一個實體,就可以直接注入並使用該實體的通用儲存庫。下面是一個使用儲存庫的示例類:

using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
namespace FormsApp
{
    public class FormService : ITransientDependency
    {
        private readonly IRepository<Form, Guid> _formRepository;
        public FormService(IRepository<Form, Guid> formRepository)
        {
            _formRepository = formRepository;
        }
        public async Task<List<Form>> GetDraftForms()
        {
            return await _formRepository.GetListAsync(f => f.IsDraft);
        }
    }
}

在本例中,我們注入了IRepository<Form, Guid>Form實體的預設通用儲存庫。然後,我們使用GetListAsync方法從資料庫中獲取經過篩選的表單列表。通用IRepository介面有兩個通用引數:實體型別(本例中為Form)和PK型別(本例中為Guid)。

非聚合根實體的儲存庫

預設情況下,通用儲存庫僅適用於聚合根實體,因為通過聚合根物件訪問聚合是最佳做法。但是,如果您使用的是關聯式資料庫,則可以為其他實體型別啟用通用儲存庫。我們將在EF Core整合部分看到如何配置。

2.2 增刪改查方法

通用儲存庫提供了許多用於查詢、插入、更新和刪除實體的內建方法。

  • InsertAsync 用於插入新實體
  • InsertManyAsync 用於插入多個實體
  • UpdateAsync 用於更新現有實體
  • UpdateManyAsync 用於更新多個實體
  • DeleteAsync 用於刪除現有實體
  • DeleteManyAsync 用於刪除多個實體

所有倉儲庫方法都是非同步的,強烈建議儘可能使用 async/await模式,因為在 .NET 中,將非同步與同步混合潛在的死鎖、超時和可伸縮性問題,不容易檢測。

如果您使用的是EF Core,這些方法可能不會立即執行實際的資料庫操作,因為EF Core使用的是更改跟蹤系統。它僅在呼叫DbContext.SaveChanges方法時儲存更改。噹噹前HTTP請求成功完成時,ABP 框架的UoW系統會自動呼叫SaveChanges方法。如果要立即將更改儲存到資料庫中,可以將autoSave引數作為true傳遞給儲存庫方法。

以下示例建立一個新的Form實體,並立即將其儲存到InsertAsync方法中的資料庫中:

1)autoSave

await _formRepository.InsertAsync(new Form(), autoSave: true);

EF Core 中,以上方法不會立即執行刷庫,因為 EF Core 使用更改跟蹤系統。它僅在你呼叫DbContext.SaveChanges方法時儲存更改。如果要立即執行,可以將autoSave設定為true。

2)CancellationToken

所有倉儲庫預設帶有一個CancellationToken引數,在需要的時候用來取消資料庫操作,比如關閉瀏覽器後,無需繼續執行冗長的資料庫查詢操作。大部分情況下,我們無需手動傳入cancellation token,因為ABP框架會自動從HTTP請求中捕捉並使用取消令牌。

2.3 查詢單個實體

  • GetAsync:根據Id或表示式返回單個實體。如果未找到請求的實體,則丟擲EntityNotFoundException
  • FindAsync:根據Id或表示式返回單個實體。如果未找到請求的實體,則返回null。

FindAsync適用於有自定義邏輯,否則使用GetAsync

public async Task<Form> GetFormAsync(Guid formId)
{
    return await _formRepository.GetAsync(formId);
}
public async Task<Form> GetFormAsync(string name)
{
    return await _formRepository.GetAsync(form => form.Name == name);
}

2.4 查詢實體列表

  • GetListAsync:返回滿足給定條件的所有實體或實體列表
  • GetPagedListAsync:分頁查詢
public async Task<List<Form>> GetFormsAsync(string name)
{
    return await _formRepository.GetListAsync(form => form.Name.Contains(name));
}

2.5 LINQ高階查詢

public class FormService2 : ITransientDependency
{
    private readonly IRepository<Form, Guid>  _formRepository;
    private readonly IAsyncQueryableExecuter  _asyncExecuter;
    public FormService2(IRepository<Form, Guid> formRepository,IAsyncQueryableExecuter asyncExecuter)
    {
        _formRepository = formRepository;
        _asyncExecuter = asyncExecuter;
    } 
  

    public async Task<List<Form>> GetOrderedFormsAsync(string name)
    {
    	//var queryable = await _formRepository.WithDetailsAsync(x => x.Category);
        var queryable = await _formRepository.GetQueryableAsync();
        var query = from form in queryable
            where form.Name.Contains(name)
            orderby form.Name
            select form;
        return await _asyncExecuter.ToListAsync(query);
    } 
}

為什麼不用return await query.ToListAsync() ?

ToListAsync它是由 EF Core定義的擴充套件方法,位於Microsoft.EntityFrameworkCoreNuGet 包內。如果你想保持你的應用層獨立於 ORM,ABP 的IAsyncQueryableExecuter服務提供了必要的抽象。

2.6 非同步擴充套件方法

ABP 框架為IRepository介面提供所有標準非同步 LINQ 擴充套件方法:

AllAsync, AnyAsync, AverageAsync, ContainsAsync, CountAsync, FirstAsync, FirstOrDefaultAsync, LastAsync, LastOrDefaultAsync, LongCountAsync, MaxAsync, MinAsync, SingleAsync, SingleOrDefaultAsync, SumAsync, ToArrayAsync, ToListAsync.

public async Task<int> GetCountAsync()
{
    return await _formRepository.CountAsync(x => x.Name.StartsWith("A"));
}

注意:以上方法只對IRepository有效。

2.6 複合主鍵查詢

複合主鍵不能使用該IRepository<TEntity, TKey>介面,因為它是獲取單個 PK ( Id) 型別。我們可以使用IRepository介面。

public class FormManagementService : ITransientDependency
{
    private readonly IRepository<FormManager> _formManagerRepository;
    public FormManagementService(IRepository<FormManager> formManagerRepository)
    {
        _formManagerRepository = formManagerRepository;
    }
    public async Task<List<FormManager>> GetManagersAsync(Guid formId)
    {
        return await _formManagerRepository.GetListAsync(fm => fm.FormId == formId);
    }
}

2.7 其他倉儲庫型別

  • IBasicRepository<TEntity, TPrimaryKey>和IBasicRepository提供基本的倉儲庫方法,但它們不支援 LINQ 和IQueryable功能。
  • IReadOnlyRepository<TEntity, TKey>, IReadOnlyRepository,IReadOnlyBasicRepository<Tentity, TKey>和IReadOnlyBasicRepository<TEntity, TKey>提供獲取資料的方法,但不包括任何操作方法。

2.8 自定義儲存庫

public interface IFormRepository : IRepository<Form, Guid>
{
    Task<List<Form>> GetListAsync(string name,bool includeDrafts = false);
}
  • 定義在Domain專案中
  • 從通用倉儲庫派生
  • 如果不想包含通用倉儲庫的方法,也可以派生自IRepository(無泛型引數)介面,這是一個空介面

結尾

由於文章有點長,分作上下兩篇,下篇待續……

相關文章