ABP框架之——數據訪問基礎架構
大家好,我是張飛洪,感謝您的閱讀,我會不定期和你分享閱讀心得,希望我的文章能成為你成長路上的一塊墊腳石,我們一起精進。
幾乎所有的業務應用程式都要適用一種資料庫基礎架構,用來實現數據訪問邏輯,以便從資料庫讀取或寫入數據,我們還需要處理資料庫事務,以確保數據源中的一致性。
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(例如int
, string
等)。
還有其他一些基類可以從中派生聚合根,如下所述:
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
是另一種實體類型。
還有一些其他預定義的基本實體類,如CreationAuditedEntity
, AuditedEntity
和FullAuditedEntity
。它們類似於上面介紹的審計聚合根類。
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值。在本例中,FormId
和UserId
是其他表的FK,它們構建FormManager
實體的CPK。
聚合根的CPKs
AggregateRoot
類也有用於CPK的非通用版本,但為聚合根實體設置CPK並不常見。
1.4 GUID主鍵
ABP主要使用GUIDs作為預構建實體的PK類型。GUIDs通常與自動增量IDs(如int
或long
,由關係資料庫支援)進行比較。與自動遞增鍵相比,使用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(無泛型參數)介面,這是一個空介面
結尾
由於文章有點長,分作上下兩篇,下篇待續……