Skip to content

第六章:資料存取基礎設施 (Entity Framework Core & MongoDB)

6.1 引言:DDD 與資料存取

在領域驅動設計 (DDD) 中,領域層 (Domain Layer) 應該與具體的資料存取技術 (如 SQL Server, MongoDB) 無關。然而,在實作上,我們必須透過 基礎設施層 (Infrastructure Layer) 來將領域物件持久化。

ABP Framework 透過 Repository 模式Unit of Work (UoW) 模式,完美地隔離了領域邏輯與資料存取細節。


6.2 Entity Framework Core 整合

EF Core 是 ABP 預設且支援最完整的 ORM。

1. AbpDbContext

所有的 DbContext 都應繼承自 AbpDbContext<T>。它提供了以下增強功能:

  • 自動處理 審計屬性 (CreationTime, CreatorId, etc.)。
  • 自動處理 軟刪除 (IsDeleted)。
  • 自動發布 實體變更事件
  • 整合 Unit of Work
csharp
[ConnectionStringName("Default")]
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
{
    public DbSet<Book> Books { get; set; }

    public BookStoreDbContext(DbContextOptions<BookStoreDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        // 設定模組的資料表 (如 Identity, Permission Management)
        builder.ConfigurePermissionManagement();
        builder.ConfigureSettingManagement();
        // ... 其他模組

        // 設定應用程式的實體
        builder.Entity<Book>(b =>
        {
            b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema);
            b.ConfigureByConvention(); // 自動設定標準屬性

            b.Property(x => x.Name).IsRequired().HasMaxLength(128);
            b.HasIndex(x => x.Name);
        });
    }
}

2. 資料庫遷移 (Migrations)

ABP 專案通常包含一個 DbMigrator 主控台應用程式。

  • 用途:在部署時執行,確保資料庫 Schema 是最新的,並執行 Data Seeder。
  • 運作方式:它會解析所有模組的 DbContext,並依序執行遷移。

最佳實踐

  • 不要 在應用程式啟動時 (Web 專案) 自動執行 Database.Migrate(),這在多實例部署時會導致併發問題。
  • 總是使用 DbMigrator 或 CI/CD Pipeline 來執行遷移。

6.3 Entity Framework Core 共用實體類型 (Shared Entity Types)

ABP V10 引入了對 EF Core 共用實體類型的支援。這允許您在執行時動態設定 Repository 的實體名稱,適用於多租戶資料隔離或資料封存等場景。

1. 設定實體

DbContextOnModelCreating 中,使用 ConfigureByConvention 時可以指定是否為共用類型:

csharp
builder.Entity<Book>(b =>
{
    b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema);
    b.ConfigureByConvention();
    // 雖然標準實體不需要額外設定,但此功能主要用於動態表名
});

2. 使用 IRepository 設定實體名稱

您可以在使用 Repository 之前,透過 SetEntityName 方法指定要操作的表名或集合名。

csharp
public class ArchiveAppService : ApplicationService
{
    private readonly IRepository<Book, Guid> _bookRepository;

    public ArchiveAppService(IRepository<Book, Guid> bookRepository)
    {
        _bookRepository = bookRepository;
    }

    public async Task ArchiveOldBooksAsync()
    {
        // 設定 Repository 操作 "Books_Archive_2024" 資料表
        (_bookRepository as ISupportEntityName)?.SetEntityName("Books_Archive_2024");

        var oldBooks = await _bookRepository.GetListAsync(b => b.CreationTime < DateTime.Now.AddYears(-1));
        // ... 處理邏輯
    }
}

注意:此功能依賴於底層 Provider 的支援。


6.3 Repository 模式詳解

Repository 是存取聚合根 (Aggregate Root) 的唯一入口。

1. 通用 Repository (Generic Repository)

ABP 為每個聚合根自動註冊了 IRepository<TEntity, TKey>

csharp
public class BookAppService : ApplicationService
{
    private readonly IRepository<Book, Guid> _bookRepository;

    public BookAppService(IRepository<Book, Guid> bookRepository)
    {
        _bookRepository = bookRepository;
    }

    public async Task DoSomething()
    {
        // 標準 CRUD
        var book = await _bookRepository.GetAsync(id);
        await _bookRepository.InsertAsync(newBook);
        await _bookRepository.DeleteAsync(id);

        // LINQ 查詢 (需引用 Volo.Abp.Domain.Repositories)
        var queryable = await _bookRepository.GetQueryableAsync();
        var books = await queryable.Where(b => b.Price > 100).ToListAsync();
    }
}

2. 自訂 Repository (Custom Repository)

當通用 Repository 無法滿足需求 (例如複雜的 SQL 查詢、Stored Procedure 或效能優化) 時,我們需要建立自訂 Repository。

步驟 1:定義介面 (Domain Layer)

csharp
public interface IBookRepository : IRepository<Book, Guid>
{
    Task<List<Book>> GetListByAuthorAsync(string author);
}

步驟 2:實作介面 (Infrastructure Layer)

csharp
public class BookRepository : EfCoreRepository<BookStoreDbContext, Book, Guid>, IBookRepository
{
    public BookRepository(IDbContextProvider<BookStoreDbContext> dbContextProvider)
        : base(dbContextProvider)
    {
    }

    public async Task<List<Book>> GetListByAuthorAsync(string author)
    {
        var dbSet = await GetDbSetAsync();
        return await dbSet
            .Where(b => b.Author == author)
            .ToListAsync();
    }
}

6.4 Unit of Work (UoW) 與交易管理

ABP 的 UoW 系統是全自動的。

1. 預設行為

  • Controller / AppService 方法:預設為一個 UoW。方法開始時開啟交易,方法結束時 (若無例外) 提交交易。
  • Repository 方法:參與當前的 UoW。

2. 手動控制

使用 [UnitOfWork] 屬性來改變預設行為。

csharp
public class MyService : ITransientDependency
{
    // 關閉交易 (適用於唯讀查詢,提升效能)
    [UnitOfWork(IsDisabled = true)]
    public virtual async Task<List<Book>> GetBooksAsync()
    {
        // ...
    }

    // 強制開啟獨立交易
    [UnitOfWork(IsTransactional = true, IsolationLevel = IsolationLevel.Serializable)]
    public virtual async Task UpdateCriticalDataAsync()
    {
        // ...
    }
}

3. IUnitOfWorkManager

在極少數情況下,您可能需要在程式碼中手動控制 UoW 範圍。

csharp
using (var uow = _unitOfWorkManager.Begin(requiresNew: true))
{
    // 這裡的變更在獨立交易中
    await _repo.InsertAsync(book);
    await uow.CompleteAsync();
}

6.5 MongoDB 整合

ABP 對 MongoDB 的支援與 EF Core 非常相似,這得益於 Repository 模式的抽象。

1. 設定 MongoDbContext

csharp
[ConnectionStringName("Default")]
public class BookStoreMongoDbContext : AbpMongoDbContext
{
    public IMongoCollection<Book> Books => Collection<Book>();

    protected override void CreateModel(IMongoModelBuilder modelBuilder)
    {
        base.CreateModel(modelBuilder);

        modelBuilder.Entity<Book>(b =>
        {
            b.CollectionName = "Books";
        });
    }
}

2. 模組依賴

BookStoreEntityFrameworkCoreModule (或改名為 InfrastructureModule) 中,將依賴從 AbpEntityFrameworkCoreModule 改為 AbpMongoDbModule

注意:雖然可以混合使用 EF Core 和 MongoDB (例如模組 A 用 SQL,模組 B 用 Mongo),但同一個交易 (UoW) 無法跨越不同的資料庫技術


6.6 效能優化技巧

  1. IQueryable vs List

    • GetListAsync() 會直接查詢資料庫並回傳 List (記憶體中)。
    • GetQueryableAsync() 回傳 IQueryable,允許您繼續串接 Where, OrderBy, Select,直到呼叫 ToListAsync() 時才執行 SQL。盡量使用 IQueryable 以減少資料傳輸。
  2. AsNoTracking

    • 對於唯讀查詢,使用 AsNoTracking() 可以避開 EF Core 的變更追蹤,顯著提升效能。
  3. 投影 (Projection)

    • 不要總是查詢整個 Entity。使用 .Select(b => new BookDto { ... }) 只查詢需要的欄位。

6.7 習題

概念題(易)⭐

習題 1:解釋 Repository Pattern 的核心概念與 UoW(工作單元)的關係。

請說明:

  • Repository Pattern 的目的
  • UoW 的作用
  • 兩者如何協同工作
  • 在 ABP 中的實作方式

習題 2:EF Core 與 MongoDB 在 ABP 中各自的適用場景?

請說明:

  • EF Core 的優勢與適用場景
  • MongoDB 的優勢與適用場景
  • 如何在同一個專案中混合使用
  • 跨資料庫事務的限制

計算/練習題(中)⭐⭐

習題 3:設計 Book 與 Author 的一對多關係,使用 EF Core Fluent API 配置。

要求:

  • 定義 Book 和 Author 實體
  • 配置一對多關係
  • 實作級聯刪除
  • 提供完整的 DbContext 配置

習題 4:實作自訂 Repository 方法 GetByAuthorAsync,並進行效能優化(避免 N+1)。

要求:

  • 實作自訂 Repository
  • 使用 Include 預載入關聯資料
  • 使用投影優化查詢
  • 提供效能對比數據

實作題(較難)⭐⭐⭐

習題 5:實作一個 BookRepository 包含以下方法:

  • GetTopSellingBooksAsync
  • GetBooksByPriceRangeAsync
  • GetBooksWithAuthorsAsync(避免 N+1)
  • SearchBooksAsync(支援全文搜尋)

習題 6:為 BookRepository 編寫整合測試,使用 Testcontainers 啟動真實 SQL Server。

要求:

  • 配置 Testcontainers
  • 編寫完整的整合測試
  • 測試所有 Repository 方法
  • 驗證資料一致性

習題解答:請參考 content/solutions/ch06-solutions.md


6.8 總結

本章介紹了 ABP 強大的資料存取基礎設施。

  • AbpDbContext 簡化了 EF Core 的設定。
  • Repository 隔離了資料存取邏輯。
  • UoW 自動化了交易管理。

掌握這些工具,您將能寫出高效、安全且易於測試的資料存取程式碼。下一章,我們將探討 橫切關注點 (Cross-Cutting Concerns),包括驗證、授權與審計日誌。


參考資源

Released under the MIT License.