ABP框架之——數據訪問基礎架構(下)

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

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)顯式加載

存儲庫提供了EnsureRepropertyLoadedAsyncEnsureRecollectionLoadedAsync擴展方法,以顯式加載導航屬性或子集合。

例如,我們可以顯式加載表單的問題,如以下代碼塊所示:

public async Task<IEnumerable<Question>> GetQuestionsAsync(Form form)
{
	//
    await _formRepository.EnsureCollectionLoadedAsync(form, f => f.Questions);
    return form.Questions;
}

如果不用EnsureCollectionLoadedAsyncQuestions可能是空的,如果已經加載過,不會重複加載,所以多次調用對性能沒有影響。

2)延遲加載

延遲加載是EF Core的一項功能,它在您首次訪問相關屬性和集合時加載它們。默認情況下不啓用延遲加載。如果要爲DbContext啓用它,請執行以下步驟:

  1. 在 EF Core 層中安裝Microsoft.EntityFrameworkCore.Proxies
  2. 配置時使用 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 CoreInclude 擴展方法(用於快速加載相關數據)。

假如你不想在應用層依賴EF Core API該怎麼辦?

在本例中,您有兩個選項:

1)IRepository.WithDetailsAsync

IRepositoryWithDetailsSync方法通過包含給定的屬性或集合來返回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請求類型(POSTPUTDELETE和其他)進行配置,則它們將使用數據庫事務

HTTP請求 是否創建事務
GET 不創建事務
PUT 創建事務
POST 創建事務

最好不要在GET請求中更改數據庫。如果在一個GET請求中進行了多個寫操作,但請求以某種方式失敗,那麼數據庫狀態可能會處於不一致的狀態,因爲ABP不會爲GET請求創建數據庫事務。在這種情況下,可以使用AbpUnitOfWorkDefaultOptionsGET請求啓用事務,也可以手動控制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倉儲庫該怎麼辦?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章