第六章:資料存取基礎設施 (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。
[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. 設定實體
在 DbContext 的 OnModelCreating 中,使用 ConfigureByConvention 時可以指定是否為共用類型:
builder.Entity<Book>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema);
b.ConfigureByConvention();
// 雖然標準實體不需要額外設定,但此功能主要用於動態表名
});2. 使用 IRepository 設定實體名稱
您可以在使用 Repository 之前,透過 SetEntityName 方法指定要操作的表名或集合名。
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>。
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)
public interface IBookRepository : IRepository<Book, Guid>
{
Task<List<Book>> GetListByAuthorAsync(string author);
}步驟 2:實作介面 (Infrastructure Layer)
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] 屬性來改變預設行為。
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 範圍。
using (var uow = _unitOfWorkManager.Begin(requiresNew: true))
{
// 這裡的變更在獨立交易中
await _repo.InsertAsync(book);
await uow.CompleteAsync();
}6.5 MongoDB 整合
ABP 對 MongoDB 的支援與 EF Core 非常相似,這得益於 Repository 模式的抽象。
1. 設定 MongoDbContext
[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 效能優化技巧
IQueryable vs List:
GetListAsync()會直接查詢資料庫並回傳List(記憶體中)。GetQueryableAsync()回傳IQueryable,允許您繼續串接Where,OrderBy,Select,直到呼叫ToListAsync()時才執行 SQL。盡量使用 IQueryable 以減少資料傳輸。
AsNoTracking:
- 對於唯讀查詢,使用
AsNoTracking()可以避開 EF Core 的變更追蹤,顯著提升效能。
- 對於唯讀查詢,使用
投影 (Projection):
- 不要總是查詢整個 Entity。使用
.Select(b => new BookDto { ... })只查詢需要的欄位。
- 不要總是查詢整個 Entity。使用
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),包括驗證、授權與審計日誌。
參考資源: