大家好,我是張飛洪,感謝您的閱讀,我會不定期和你分享學習心得,希望我的文章能成爲你成長路上的一塊墊腳石,我們一起精進。
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倉儲庫該怎麼辦?