Repository Pattern(儲存庫模式)

是什麼?

Repository 是一個抽象層,提供類似集合(Collection)的介面來存取 Aggregate Root。它把資料存取的細節隱藏起來,讓 Domain 層不依賴任何基礎設施(資料庫、ORM、API)。

ℹ️DDD 基礎設施抽象

Repository 介面定義在 Domain 層,實作放在 Infrastructure 層。這是 DDD 中實現**依賴反轉原則(DIP)**的關鍵手段 — Domain 定義需求,Infrastructure 負責實現。

核心觀念

常見誤區

⚠️常見誤區

誤區一:為每個 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 負責。

實作流程

1

定義 Repository 介面

在 Domain 層定義介面,宣告 Add、GetById、Remove 等方法

2

實作 Repository

在 Infrastructure 層用 EF Core、Dapper 等實作介面

3

註冊 DI

在 DI Container 中把介面綁定到實作

4

Application Service 使用

Application Service 透過介面操作 Repository

5

搭配 Unit of Work

確保一個交易內的所有 Repository 操作一次提交

程式碼範例

C# 版本

csharp
// === 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 版本

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 版本

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()

概念圖

Application Service
依賴介面
IOrderRepository (Domain)
回傳
OrderRepository (Infrastructure)
實作
Database
Order (Aggregate Root)
Unit of Work

此圖展示 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 | 一個交易的變更一次提交 |

你可能也想看

Context MappingApplication Service

按 ← → 鍵切換課程