Repository Pattern(儲存庫模式)
是什麼?
Repository 是一個抽象層,提供類似集合(Collection)的介面來存取 Aggregate Root。它把資料存取的細節隱藏起來,讓 Domain 層不依賴任何基礎設施(資料庫、ORM、API)。
ℹ️DDD 基礎設施抽象
Repository 介面定義在 Domain 層,實作放在 Infrastructure 層。這是 DDD 中實現**依賴反轉原則(DIP)**的關鍵手段 — Domain 定義需求,Infrastructure 負責實現。
核心觀念
- 只為 Aggregate Root 建 Repository:內部 Entity 和 Value Object 透過 Root 存取,不需要獨立的 Repository。例如:有
IOrderRepository但不需要IOrderLineItemRepository,因為 OrderLineItem 是 Order Aggregate 的一部分 - 介面在 Domain 層:
IOrderRepository定義在 Domain 專案,OrderRepository實作在 Infrastructure 專案。這實現了依賴反轉原則(DIP)——Domain 定義「我需要什麼」,Infrastructure 負責「怎麼做到」 - Collection 語意:Repository 的方法像操作集合 —
Add、GetById、Remove,不暴露 SQL 或 ORM 細節。常見方法簽名:Task<Order?> GetById(Guid id)/Task Add(Order order)/Task Remove(Order order) - Unit of Work 搭配:Repository 通常搭配 Unit of Work(UoW)模式,確保一個交易內的所有變更一次提交。在 EF Core 中,
DbContext本身就是 UoW——呼叫SaveChangesAsync()時所有變更一次寫入 - 不要在 Repository 放業務邏輯:Repository 只負責存取,業務規則放在 Entity 和 Domain Service。如果你在 Repository 裡看到
if (order.Status == "VIP")這種判斷,它應該搬到 Domain Model
常見誤區
⚠️常見誤區
誤區一:為每個 Entity 都建一個 Repository。只有 Aggregate Root 才有 Repository。OrderLineItem 是 Order 的一部分,透過 Order Repository 存取。
誤區二:Repository 裡面寫業務邏輯(如 GetVipCustomersWithRecentOrders)。這類複雜查詢應該用專門的 Query Service 或 Read Model 處理。
誤區三:Repository 回傳 DTO 或 ViewModel。Repository 回傳的是 Domain Model(Aggregate Root),DTO 的轉換由 Application Service 或 Mapper 負責。
實作流程
定義 Repository 介面
在 Domain 層定義介面,宣告 Add、GetById、Remove 等方法
實作 Repository
在 Infrastructure 層用 EF Core、Dapper 等實作介面
註冊 DI
在 DI Container 中把介面綁定到實作
Application Service 使用
Application Service 透過介面操作 Repository
搭配 Unit of Work
確保一個交易內的所有 Repository 操作一次提交
程式碼範例
C# 版本
// === Domain 層:定義介面 ===
namespace Domain.Orders
{
public interface IOrderRepository
{
Task<Order?> GetById(Guid id);
Task Add(Order order);
Task Remove(Order order);
}
// 通用 Repository 介面(可選)
public interface IRepository<T> where T : AggregateRoot<Guid>
{
Task<T?> GetById(Guid id);
Task Add(T aggregate);
Task Remove(T aggregate);
}
// Unit of Work
public interface IUnitOfWork
{
Task<int> SaveChanges(CancellationToken cancellationToken = default);
}
}
// === Infrastructure 層:實作 ===
namespace Infrastructure.Persistence
{
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
public OrderRepository(AppDbContext context)
{
_context = context;
}
public async Task<Order?> GetById(Guid id)
{
return await _context.Orders
.Include(o => o.LineItems) // 載入 Aggregate 內部的 Entity
.FirstOrDefaultAsync(o => o.Id == id);
}
public async Task Add(Order order)
{
await _context.Orders.AddAsync(order);
}
public async Task Remove(Order order)
{
_context.Orders.Remove(order);
}
}
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
public UnitOfWork(AppDbContext context) => _context = context;
public async Task<int> SaveChanges(CancellationToken cancellationToken = default)
{
return await _context.SaveChangesAsync(cancellationToken);
}
}
}
// === Application 層:使用 ===
namespace Application.Orders
{
public class PlaceOrderHandler
{
private readonly IOrderRepository _orderRepo;
private readonly IUnitOfWork _unitOfWork;
public PlaceOrderHandler(IOrderRepository orderRepo, IUnitOfWork unitOfWork)
{
_orderRepo = orderRepo;
_unitOfWork = unitOfWork;
}
public async Task Handle(PlaceOrderCommand command)
{
var order = await _orderRepo.GetById(command.OrderId)
?? throw new NotFoundException("Order not found.");
order.Place(); // 業務邏輯在 Entity 裡
await _unitOfWork.SaveChanges();
}
}
}TypeScript 版本
// === Domain 層:定義介面 ===
interface OrderRepository {
getById(id: string): Promise<Order | null>;
add(order: Order): Promise<void>;
remove(order: Order): Promise<void>;
}
// === Infrastructure 層:實作 ===
class PrismaOrderRepository implements OrderRepository {
constructor(private prisma: PrismaClient) {}
async getById(id: string): Promise<Order | null> {
const data = await this.prisma.order.findUnique({
where: { id },
include: { lineItems: true },
});
if (!data) return null;
return OrderMapper.toDomain(data);
}
async add(order: Order): Promise<void> {
const data = OrderMapper.toPersistence(order);
await this.prisma.order.create({ data });
}
async remove(order: Order): Promise<void> {
await this.prisma.order.delete({ where: { id: order.id } });
}
}
// === Application 層:使用 ===
class PlaceOrderHandler {
constructor(private orderRepo: OrderRepository) {}
async handle(command: PlaceOrderCommand): Promise<void> {
const order = await this.orderRepo.getById(command.orderId);
if (!order) throw new Error("Order not found.");
order.place();
await this.orderRepo.add(order); // 或用 UoW
}
}Python 版本
from abc import ABC, abstractmethod
from typing import Optional
from uuid import UUID
# === Domain 層:定義介面 ===
class OrderRepository(ABC):
@abstractmethod
async def get_by_id(self, id: UUID) -> Optional["Order"]:
pass
@abstractmethod
async def add(self, order: "Order") -> None:
pass
@abstractmethod
async def remove(self, order: "Order") -> None:
pass
# === Infrastructure 層:實作 ===
class SqlAlchemyOrderRepository(OrderRepository):
def __init__(self, session: AsyncSession):
self._session = session
async def get_by_id(self, id: UUID) -> Optional["Order"]:
result = await self._session.execute(
select(OrderModel)
.options(selectinload(OrderModel.line_items))
.where(OrderModel.id == id)
)
row = result.scalar_one_or_none()
return OrderMapper.to_domain(row) if row else None
async def add(self, order: "Order") -> None:
model = OrderMapper.to_persistence(order)
self._session.add(model)
async def remove(self, order: "Order") -> None:
model = await self._session.get(OrderModel, order.id)
if model:
await self._session.delete(model)
# === Application 層:使用 ===
class PlaceOrderHandler:
def __init__(self, order_repo: OrderRepository, uow: UnitOfWork):
self._order_repo = order_repo
self._uow = uow
async def handle(self, command: PlaceOrderCommand) -> None:
order = await self._order_repo.get_by_id(command.order_id)
if not order:
raise ValueError("Order not found.")
order.place()
await self._uow.commit()概念圖
此圖展示 Repository 的依賴方向:Application Service 依賴 Domain 層定義的介面(IOrderRepository),Infrastructure 層的實作也指向 Domain 介面。這就是依賴反轉——所有箭頭指向 Domain,Domain 不依賴任何外層。
何時需要包一層 Repository,何時直接用 DbContext
| 情境 | 建議 | 理由 | |------|------|------| | Domain 層不想依賴 EF Core | 包 Repository | 保持 Domain 的純淨性,方便換 ORM | | 小型 CRUD 專案、不會換 ORM | 直接用 DbContext | 減少不必要的抽象層 | | 需要在測試中替換資料存取 | 包 Repository | 方便注入 InMemoryRepository 做測試 | | 讀取端需要複雜查詢 | 讀取用 Dapper 直接查詢,寫入用 Repository | CQRS 分離:寫入走 Domain Model,讀取走扁平 DTO |
實戰補充
💡資深開發者筆記
在 EF Core 專案中,DbContext 本身就是 Unit of Work,DbSet<T> 就是 Repository。有些團隊會爭論「還需要再包一層 Repository 嗎?」答案是:如果你的 Domain 層不想依賴 EF Core,就需要。如果是小專案且不打算換 ORM,直接用 DbContext 也行。
Repository 只處理寫入端的資料存取。讀取端(查詢列表、報表、搜尋)建議用專門的 Query Service 或 Read Model,直接用 Dapper 或 Raw SQL 查詢,不經過 Domain Model。這和 CQRS 的理念一致。
Specification Pattern 可以和 Repository 結合:把查詢條件封裝成 Specification 物件,Repository 接受 Specification 來過濾資料。這樣查詢邏輯可以在 Domain 層表達,又不汙染 Repository 介面。
理解測驗
🤔 為什麼 Repository 介面要定義在 Domain 層?
🤔 哪些物件應該有自己的 Repository?
🤔 Repository 的方法應該長什麼樣子?
重點整理
💡一句話記住
口訣:「介面在 Domain、實作在 Infrastructure、只服務 Aggregate Root」—— Repository 三定律。
| 概念 | 說明 | |------|------| | 介面在 Domain | 定義存取需求,不依賴基礎設施 | | 實作在 Infrastructure | 用 EF Core/Dapper/Prisma 等實現 | | 只為 Aggregate Root | 內部物件透過 Root 存取 | | Collection 語意 | Add、GetById、Remove | | 搭配 Unit of Work | 一個交易的變更一次提交 |