大家好,我是張飛洪,感謝您的閱讀,我會不定期和你分享學習心得,希望我的文章能成為你成長路上的一塊墊腳石,我們一起精進。
EF Core整合
EF Core是微軟的ORM,可以使用它與主流的資料庫提供商合作,如SQL Server、Oracle、MySQL、PostgreSQL和Cosmos DB。當您使用ABP命令列介面(CLI)建立新的ABP解決方案時,它是預設的資料庫提供程式。
預設情況下,啟動模板使用SQL Server。如果您更喜歡其他的資料庫管理系統(DBMS),可以在建立新解決方案時指定-DBMS引數,如下所示:
abp new DemoApp -dbms MySQL
您可以參考ABP的文件,瞭解最新支援的資料庫選項,以及如何切換到其他現成資料庫提供程式。
在接下來您將瞭解到:
- 如何配置DBMS;
- 如何定義DbContext類;
- 如何註冊到依賴注入(DI)系統;
- 如何將實體對映到資料庫表;
- 如何使用Code First和為實體建立自定義儲存庫;
- 如何為實體載入相關資料的不同方式。
3.1 配置 DBMS
我們使用AbpDbContextOptions
在模組的ConfigureServices
方法中配置DBMS
。以下示例使用SQL Server
作為DBMS
進行配置:
Configure<AbpDbContextOptions>(options =>
{
options.UseSqlServer();
});
當然,如果希望配置不同的DBMS,那麼UseSqlServer()
方法呼叫將有所不同。我們不需要設定連線字串,因為它是從ConnectionString:Default
配置自動獲得的。你可以檢視appsettings.json
檔案,以檢視和更改連線字串。
配置了DBMS,但還沒有定義DbContext物件,這是在EF Core中使用資料庫所必需的,我接下來看看如何配置:
3.2 定義 DbContext
DbContext是EF Core中與資料庫互動的主要物件。通常建立一個繼承自DbContext的類來建立自己的DbContext。使用ABP框架,我們將繼承AbpDbContext。
下面是一個使用ABP框架的DbContext類定義示例:
using Microsoft.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
namespace FormsApp
{
public class FormsAppDbContext : AbpDbContext<FormsAppDbContext>
{
public DbSet<Form> Forms { get; set; }
public FormsAppDbContext(DbContextOptions<FormsAppDbContext> options)
: base(options)
{
}
}
}
FormsAppDbContext
繼承自AbpDbContext<FormsAppDbContext>
。AbpDbContext
是一個泛型類,將DbContext
型別作為泛型引數。它還迫使我們建立一個建構函式。然後,我們就可以為實體新增DbSet
屬性。
一旦定義了DbContext,我們就應該向DI系統註冊它,以便在應用程式中使用它。
3.3 向 DI 註冊 DbContext
AddAbpDbContext
擴充套件方法用於向DI系統註冊DbContext
類。您可以在模組的ConfigureServices
方法中使用此方法(它位於啟動解決方案的EntityFrameworkCore
專案中),如以下程式碼塊所示:
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAbpDbContext<FormsAppDbContext> (options =>
{
//啟用預設通用儲存庫,DDD應始終通過聚合根訪問子實體
options.AddDefaultRepositories();
//開啟後,非聚合根實體也支援IRepository注入
//options.AddDefaultRepositories(includeAllEntities: true);
});
}
AddDefaultRepositories()
用於為與DbContext
相關的實體啟用預設通用儲存庫。預設情況下,它僅為聚合根實體啟用通用儲存庫,因為在域驅動設計(DDD)中,子實體應始終通過聚合根進行訪問。如果還想將儲存庫用於其他實體型別,可以將可選的includealentities
引數設定為true
options.AddDefaultRepositories(includeAllEntities: true);
使用此選項,意味著您可以為應用程式的任何實體注入IRepository
服務。
注意:因為從事關聯式資料庫的開發人員習慣於從所有資料庫表中查詢,如果要嚴格應用 DDD 原則,則應始終使用聚合根來訪問子實體。
我們已經瞭解瞭如何註冊DbContext
類,我們可以為DbContext
類中的所有實體注入和使用IRepository
介面。接下來,我們應該首先為實體配置EF Core對映。
3.4 配置實體對映
EF Core是一個物件到關係的對映器,它將實體對映到資料庫表。我們可以通過以下兩種方式配置這些對映的詳細資訊:
- 在實體類上使用資料註釋屬性
- 通過重寫
OnModelCreating
方法在內部使用 Fluent API(推薦)
使用資料註釋屬性會領域層依賴於EF Core,如果這對您來說不是問題,您可以遵循EF Core的文件使用這些屬性。為了解脫依賴,同時也為了保持實體類的純潔度,推薦使用Fluent API方法。
要使用Fluent API方法,可以在DbContext類中重寫OnModelCreating方法,如以下程式碼塊所示:
public class FormsAppDbContext : AbpDbContext<FormsAppDbContext>
{
...
//1.override覆蓋後,依然會呼叫父類的base.OnModelCreating(),因為內建審計日誌和資料過濾
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
2.Fluent API,這裡可以繼續封裝(TODO)
builder.Entity<Form>(b =>
{
b.ToTable("Forms");
b.ConfigureByConvention(); //3.重要,預設配置預定義的Entity或AggregateRoot,無需再配置,剩下的配置就顯得整潔而規範了。
b.Property(x => x.Name)
.HasMaxLength(100)
.IsRequired();
b.HasIndex(x => x.Name);
});
//4.一對多的配置
builder.Entity<Question>(b =>
{
b.ToTable("FormQuestions");
b.ConfigureByConvention();
b.Property(x => x.Title)
.HasMaxLength(200)
.IsRequired();
b.HasOne<Form>() //5.一個問題對應一個表單,一個表單有多個問題。
.WithMany(x => x.Questions)
.HasForeignKey(x => x.FormId)
.IsRequired();
});
}
}
重寫OnModelCreating
方法時,始終呼叫base.OnModelCreating()
,因為該方法內執行預設配置(如稽核日誌和資料過濾器)。然後,使用builder
物件執行配置。
例如,我們可以為本章中定義的表單類配置對映,如下所示:
builder.Entity<Form>(b => {
b.ToTable("Forms");
b.ConfigureByConvention();
b.Property(x => x.Name).HasMaxLength(100) .IsRequired();
b.HasIndex(x => x.Name);
});
在這裡呼叫b.ConfigureByConvention
方法很重要。如果實體派生自ABP的預定義實體或AggregateRoot
類,它將配置實體的基本屬性。剩下的配置程式碼非常乾淨和標準,您可以從EF Core的文件中瞭解所有細節。
下面是另一個配置實體之間關係的示例:
builder.Entity<Question>(b => {
b.ToTable("FormQuestions");
b.ConfigureByConvention();
b.Property(x => x.Title).HasMaxLength(200).IsRequired();
b.HasOne<Form>().WithMany(x => x.Questions).HasForeignKey(x => x.FormId).IsRequired();
});
在這個例子中,我們定義了表單和問題實體之間的關係:一個表單可以有許多問題,而一個問題屬於一個表單。
EF的 Code First Migrations
系統提供了一種高效的方法來增量更新資料庫,使其與實體保持同步。
Code First相比較傳統遷移的好處:
- 高效快速
- 增量更新
- 版本管理
3.5 實現自定義儲存庫
我們在“自定義儲存庫”部分建立了一個IFormRepository
介面。現在,是時候使用EF Core
實現這個儲存庫介面了。
在解決方案的EF Core
整合專案中實現儲存庫,如下所示:
//1.整合自EfCoreRepository,傳入三個泛型引數,繼承了所有標準儲存庫的方法。
public class FormRepository : EfCoreRepository<FormsAppDbContext, Form, Guid>,IFormRepository
{
public FormRepository(IDbContextProvider<FormsAppDbContext> dbContextProvider)
: base(dbContextProvider){ }
public async Task<List<Form>> GetListAsync(string name, bool includeDrafts = false)
{
var dbContext = await GetDbContextAsync();
var query = dbContext.Forms.Where(f => f.Name.Contains(name));
if (!includeDrafts)
{
query = query.Where(f => !f.IsDraft);
}
return await query.ToListAsync();
}
}
該類派生自ABP的EfCoreRepository
類。通過這種方式,我們繼承了所有標準的儲存庫方法。EfCoreRepository
類獲得三個通用引數:DbContext
型別、實體型別和實體類的PK
型別。
FormRepository
還實現了IFormRepository
,它定義了一個GetListAsync
方法,DbContext
例項在這個方法中可以使用EF Core API的所有功能。
關於WhereIf
的提示:
條件過濾是一種廣泛使用的模式,ABP提供了一種很好的WhereIf
擴充套件方法,可以簡化我們的程式碼。
我們可以重寫GetListAsync方法,如下程式碼塊所示:
var dbContext = await GetDbContextAsync();
return await dbContext.Forms
.Where(f => f.Name.Contains(name))
.WhereIf(!includeDrafts, f => !f.IsDraft)
.ToListAsync();
因為我們有DbContext
例項,所以可以使用它執行結構化查詢語言(SQL)命令或儲存過程。下面是執行“刪除所有表單”命令:
public async Task DeleteAllDraftsAsync()
{
var dbContext = await GetDbContextAsync();
//執行SQL查詢
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM Forms WHERE IsDraft = 1");
}
執行儲存過程和函式,請參考EF的核心文件學習如何執行儲存過程和函式。
一旦實現了IFormRepository
,就可以注入並使用它,而不是IRepository<Form,Guid>
,如下所示:
1)自定義儲存庫的呼叫
public class FormService : ITransientDependency
{
private readonly IFormRepository _formRepository;//自定義倉儲庫
public FormService(IFormRepository formRepository)
{
_formRepository = formRepository;
}
public async Task<List<Form>> GetFormsAsync(string name)
{
return await _formRepository.GetListAsync(name, includeDrafts: true);
}
}
FormService
類使用IFormRepository
的自定義GetListAsync
方法。即使為表單實現了自定義儲存庫類,仍然可以為該實體注入並使用預設的通用儲存庫(例如,IRepository<Form,Guid>
),尤其是剛開始不熟悉,可以從通用儲存庫上手,等熟悉後就可以使用自定義儲存庫。
2)自定義儲存庫的配置
如果重寫EfCoreRepository
類中的基方法並,可能會出現一個潛在問題:使用通用儲存庫的服務將繼續使用非重寫方法。要防止這種情況,請在向DI註冊DbContext
時使用AddRepository
方法,如下所示:
context.Services.AddAbpDbContext<FormsAppDbContext>(options =>
{
options.AddDefaultRepositories();
//實現倉儲庫後,建議進行注入
options.AddRepository<Form, FormRepository>();
});
通過這種配置,AddRepository
方法將通用儲存庫重定向到自定義儲存庫類。
3.7 資料載入
如果您的實體具有指向其他實體的導航屬性或具有其他實體的集合,則在使用主實體時,您經常需要訪問這些相關實體。例如,前面介紹的表單實體有一組問題實體,您可能需要在使用表單物件時訪問這些問題集。
訪問相關實體有多種方式,包括:
- 顯式載入
- 延遲載入
- 即時載入
1)顯式載入
儲存庫提供了EnsureRepropertyLoadedAsync
和EnsureRecollectionLoadedAsync
擴充套件方法,以顯式載入導航屬性或子集合。
例如,我們可以顯式載入表單的問題,如以下程式碼塊所示:
public async Task<IEnumerable<Question>> GetQuestionsAsync(Form form)
{
//
await _formRepository.EnsureCollectionLoadedAsync(form, f => f.Questions);
return form.Questions;
}
如果不用EnsureCollectionLoadedAsync
,Questions
可能是空的,如果已經載入過,不會重複載入,所以多次呼叫對效能沒有影響。
2)延遲載入
延遲載入是EF Core
的一項功能,它在您首次訪問相關屬性和集合時載入它們。預設情況下不啟用延遲載入。如果要為DbContext
啟用它,請執行以下步驟:
- 在 EF Core 層中安裝
Microsoft.EntityFrameworkCore.Proxies
- 配置時使用
UseLazyLoadingProxies
方法
Configure<AbpDbContextOptions>(options =>
{
options.PreConfigure<FormsAppDbContext>(opts =>
{
opts.DbContextOptions.UseLazyLoadingProxies();
});
options.UseSqlServer();
});
- 確保導航屬性和集合屬性在實體中是
virtual
public class Form : BasicAggregateRoot<Guid>
{
...
public virtual ICollection<Question> Questions { get; set; }
public virtual ICollection<FormManager> Owners { get; set; }
}
當您啟用延遲載入時,您無需再使用顯式載入。
延遲載入是一個被討論過的ORM
概念。一些開發人員發現它很實用,而其他人則建議不要使用它。我之所以不使用它,是因為它有一些潛在的問題,比如:
- 無法使用非同步
延遲載入不能使用非同步程式設計,無法使用async/await
模式訪問屬性。因此,它會阻止呼叫執行緒,這對於吞吐量和可伸縮性來說是一種糟糕的做法。
1+N
效能問題
如果在使用foreach
迴圈之前沒有預先載入相關資料,則可能會出現1+N
載入問題。1+N
載入意味著通過單個資料庫操作1次(比如,從資料庫中查詢實體列表),然後執行一個迴圈來訪問這些實體的導航屬性(或集合)。在這種情況下,它會延遲載入每個迴圈內的相關屬性(N=第一次資料庫操作中查詢的實體數)。因此,進行1+N
資料庫呼叫,會顯著降低應用程式效能。
- 斷言和程式碼優化問題
因為您可能不容易看到相關資料何時從資料庫載入。我建議採用一種更可控的方法,儘可能使用即時載入。
3)即時載入
顧名思義,即時載入是在首先查詢主實體時載入相關資料的一種方式。假設您已經建立了一個自定義儲存庫,以便在從資料庫獲取表單物件時載入相關問題,如下所示:
- 在
EF Core
層,在自定義倉儲庫中使用EF Core API
public async Task<Form> GetWithQuestions(Guid formId)
{
var dbContext = await GetDbContextAsync();
return await dbContext.Forms
.Include(f => f.Questions)
.SingleAsync(f => f.Id == formId);
}
自定義儲存庫方法,可以使用完整的EF Core API
。但是,如果您使用的是ABP
的儲存庫,並且不想在應用程式層依賴EF Core
,那麼就不能使用EF Core
的Include
擴充套件方法(用於快速載入相關資料)。
假如你不想在應用層依賴
EF Core API
該怎麼辦?
在本例中,您有兩個選項:
1)IRepository.WithDetailsAsync
IRepository
的WithDetailsSync
方法通過包含給定的屬性或集合來返回IQueryable
例項,如下所示:
public async Task EagerLoadDemoAsync(Guid formId)
{
var queryable = await _formRepository.WithDetailsAsync(f => f.Questions);
var query = queryable.Where(f => f.Id == formId);
var form = await _asyncExecuter.FirstOrDefaultAsync(query);
foreach (var question in form.Questions)
{
//...
}
}
WithDetailsAsync(f=>f.Questions)
返回IQueryable<Form>
,其中包含form.Questions
,因此我們可以安全地迴圈表單。IAsyncQueryableExecuter
在本章的“通用儲存庫”部分進行了介紹。如果需要,WithDetailsSync
方法可以獲取多個表示式以包含多個屬性。如果需要巢狀包含(EF Core中的ThenClude
擴充套件方法),則不能使用WithDetailsAsync
。
2)聚合模式
聚合模式將在第10章DDD——領域層中詳細介紹。可以簡單地理解:一個聚合被認為是一個單一的單元,它與所有子集合一起作為單個單元進行讀取和儲存。這意味著您在載入Form
時總是載入相關Questions
。
ABP很好地支援聚合模式,並允許您在全域性點為實體配置即時載入。我們可以在模組類的ConfigureServices
方法中編寫以下配置(在解決方案的EntityFrameworkCore
專案中):
Configure<AbpEntityOptions>(options =>
{
options.Entity<Form>(orderOptions =>
{
//全域性點為實體配置預載入
orderOptions.DefaultWithDetailsFunc = query => query
.Include(f => f.Questions)
.Include(f => f.Owners);
});
});
建議包括所有子集合。如上所示配置DefaultWithDetailsFunc
方法後,將發生以下情況
- 預設情況下,返回單個實體(如
GetAsync
)的儲存庫方法將載入相關實體,除非通過在方法呼叫中將includeDetails
引數指定為false
來明確禁用該行為。 - 返回多個實體(如
GetListAsync
)的儲存庫方法將允許相關實體的即時載入,而預設情況下它們不會即時載入。
下面是一些例子,獲取包含子集合的單一表單,如下所示:
//獲取一個包含子集合的表單
var form = await _formRepository.GetAsync(formId);
//獲取一個沒有子集合的表單
var form = await _formRepository.GetAsync(formId, includeDetails: false);
//獲取沒有子集合的表單列表
var forms = await _formRepository.GetListAsync(f => f.Name.StartsWith("A"));
//獲取包含子集合的表單列表
var forms = await _formRepository.GetListAsync(f => f.Name.StartsWith("A"), includeDetails: true);
聚合模式在大多數情況下簡化了應用程式程式碼,而在需要效能優化的情況下,您可以進行微調。請注意,如果真正實現聚合模式,則不會使用導航屬性(指向其他聚合),我們將在第10章DDD——領域層中再次回到這個主題。
瞭解UoW
UoW是ABP用來啟動、管理和處理資料庫連線和事務的主要系統。UoW採用環境上下文模式(Ambient Context pattern)設計。這意味著,當我們建立一個新的UoW時,它會建立一個作用域上下文,該上下文中共享所有資料庫操作=。UoW中完成的所有操作都會一起提交(成功時)或回滾(異常時)。
配置UoW選項
在ASP.NET Core
中,預設設定下,HTTP請求被視為一個UoW。ABP在請求開始時啟動UoW,如果請求成功完成,則將更改儲存到資料庫中。如果請求因異常而失敗,它將回滾。
ABP根據HTTP請求型別確定資料庫事務使用情況。HTTP GET
請求不會建立資料庫事務。UoW仍然可以工作,但在這種情況下不使用資料庫事務。如果您沒有對所有其他HTTP請求型別(POST
, PUT
, DELETE
和其他)進行配置,則它們將使用資料庫事務。
HTTP請求 | 是否建立事務 |
---|---|
GET | 不建立事務 |
PUT | 建立事務 |
POST | 建立事務 |
最好不要在GET請求中更改資料庫。如果在一個GET
請求中進行了多個寫操作,但請求以某種方式失敗,那麼資料庫狀態可能會處於不一致的狀態,因為ABP不會為GET
請求建立資料庫事務。在這種情況下,可以使用AbpUnitOfWorkDefaultOptions
為GET
請求啟用事務,也可以手動控制UoW。
為GET啟用請求事務的配置:
在模組(在資料庫整合專案中)的ConfigureServices
方法中使用AbpUnitOfWorkDefaultOptions
,如下所示:
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpUnitOfWorkDefaultOptions>(options =>
{
options.TransactionBehavior = UnitOfWorkTransactionBehavior.Enabled;
options.Timeout = 300000; // 5 minutes
options.IsolationLevel = IsolationLevel.Serializable;
});
}
TransactionBehavior的三個值:
Auto
(預設):自動使用事務(為非GET
HTTP
請求啟用事務)Enabled
:始終使用事務,即使對於HTTP
GET
請求Disabled
: 從不使用事務
Auto
是預設值,對於大多數應用推薦使用。IsolationLevel
僅對關聯式資料庫有效。如果未指定,ABP將使用基礎提供程式的預設值。最後,Timeout
選項允許將事務的預設超時值設定為毫秒,如果UoW操作未在給定的超時值內完成,將引發超時異常。
以上,我們學習瞭如何在全域性配置UOW預設選項,也可以為單個UoW手動配置這些值。
手動控制UoW
對於web應用,一般很少需要手動控制UoW。但是,對於後臺作業或非web應用程式,您可能需要自己建立UoW作用域。
使用特性
建立UoW作用域的一種方法是在方法上使用[UnitOfWork]
屬性,如下所示:
[UnitOfWork(isTransactional: true)]
public async Task DoItAsync()
{
await _formRepository.InsertAsync(new Form() { ... });
await _formRepository.InsertAsync(new Form() { ... });
}
如果周圍的UoW已經就位,那麼UnitOfWork
特性將被忽略。否則,ABP會在進入DoItAsync
方法之前啟動一個新的事務UoW,並在不引發異常的情況下提交事務。如果該方法引發異常,事務將回滾。
使用注入服務
如果要精細控制UoW,可以注入並使用IUnitOfWorkManager
服務,如以下程式碼塊所示:
public async Task DoItAsync()
{
using (var uow = _unitOfWorkManager.Begin(requiresNew: true,isTransactional: true, timeout: 15000))
{
await _formRepository.InsertAsync(new Form() { });
await _formRepository.InsertAsync(new Form() { });
await uow.CompleteAsync();
}
}
在本例中,我們將啟動一個新的事務性UoW作用域,timeout
引數的值為15秒。使用這種用法(requiresNew: true
),ABP總是啟動一個新的UoW,即使周圍已經有一個UoW。如果一切正常,會呼叫uow.CompleteAsync()
方法。如果要回滾當前事務,請使用uow.RollbackAsync()
方法。
如前所述,UoW使用環境作用域。您可以使用IUnitOfWorkManager.Current
訪問此範圍內的任何位置的當前UoW。如果沒有正在進行的UoW,則可以為null
。
下面的程式碼段將SaveChangesAsync
方法與IUnitOfWorkManager.Current
屬性一起使用:
await _unitOfWorkManager.Current.SaveChangesAsync();
我們將所有掛起的更改儲存到資料庫中。但是,如果這是事務性UoW,那麼如果回滾UoW或在UoW範圍內引發任何異常,這些更改也會回滾。
小結 & 思考
- 小結:ABP 框架可以與任何資料庫系統一起工作,同時它提供了與EF Core和MongoDB的內建整合包。
- 思考:假如你不想在應用層依賴EF Core API,或者用的是ABP倉儲庫該怎麼辦?