跳到主要内容

Unit of Work パターン (DDD)

Unit of Work(ユニット・オブ・ワーク)パターンは、ビジネストランザクションの一貫性を保つためのデザインパターンです。特にドメイン駆動設計(DDD)において、集約(Aggregate)の整合性を維持するために重要な役割を果たします。

概要

Unit of Workは、一連の操作(データの変更)を一つのトランザクションとして扱います。「すべての変更が成功するか、すべて失敗するか」を保証することで、データの不整合を防ぎます。

DDDの文脈では、Repository パターンと組み合わせて使用されることが一般的です。Repositoryは集約の取得や追加を担当し、Unit of Workはその変更をデータベースにコミットする責任を持ちます。

.NET (Entity Framework Core) における位置づけ

.NETの標準的なORMである Entity Framework Core (EF Core)DbContext は、実は Unit of Work パターンと Repository パターンの概念を内包しています。

  • DbSet<T>: Repository の役割に近い
  • DbContext: Unit of Work の役割(SaveChanges でコミット)

しかし、DDDのアーキテクチャを厳密に適用する場合、インフラストラクチャ層(EF Core)への依存をドメイン層から排除するために、明示的な抽象化(インターフェース)を導入することがよくあります。

実装例

以下に、DDDスタイルでの Unit of Work と Repository の実装例を示します。

1. インターフェース定義 (Domain Layer)

ドメイン層では、永続化の詳細は知らず、契約(インターフェース)のみを定義します。

// IUnitOfWork.cs
public interface IUnitOfWork
{
// 変更を確定するメソッド
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}

// IRepository.cs (Generic or Specific)
public interface IOrderRepository
{
IUnitOfWork UnitOfWork { get; } // リポジトリが属するUoWへの参照を持つこともある
Task<Order> FindByIdAsync(int id);
void Add(Order order);
// UpdateやDeleteは追跡されているエンティティを変更するだけなので、
// 明示的なメソッドがない場合もあるが、わかりやすさのために用意することもある。
}

2. 実装 (Infrastructure Layer)

インフラストラクチャ層で EF Core を使って実装します。

// OrderRepository.cs
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _context;

public OrderRepository(AppDbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}

// DbContextがIUnitOfWorkを実装しているとみなす、またはアダプターを噛ませる
public IUnitOfWork UnitOfWork => _context;

public void Add(Order order)
{
_context.Orders.Add(order);
}

public async Task<Order> FindByIdAsync(int id)
{
return await _context.Orders.FindAsync(id);
}
}

// AppDbContext.cs
// DbContextにIUnitOfWorkを実装させる
public class AppDbContext : DbContext, IUnitOfWork
{
public DbSet<Order> Orders { get; set; }

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

// SaveChangesAsyncはDbContextに既に存在するが、インターフェースに合わせて実装を確認
// 必要に応じてドメインイベントのディスパッチなどをここに挟む
}

3. アプリケーションサービスでの使用 (Application Layer)

アプリケーション層(ユースケース)では、リポジトリを通じてドメインオブジェクトを操作し、最後に Unit of Work でコミットします。

public class CreateOrderCommandHandler
{
private readonly IOrderRepository _orderRepository;

public CreateOrderCommandHandler(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}

public async Task Handle(CreateOrderCommand command, CancellationToken cancellationToken)
{
// 1. ドメインロジックの実行(集約の生成)
var order = new Order(command.CustomerId);
order.AddItem(command.ProductId, command.Quantity);

// 2. リポジトリへの追加(まだDBには保存されない)
_orderRepository.Add(order);

// 3. Unit of Workによるコミット(ここでトランザクションが確定)
await _orderRepository.UnitOfWork.SaveChangesAsync(cancellationToken);
}
}

メリットと注意点

メリット

  • 整合性の保証: 複数のリポジトリにまたがる変更を1つのトランザクションでアトミックに処理できます。
  • 関心の分離: ビジネスロジック(ドメイン層)と永続化の詳細(インフラ層)を切り離せます。
  • テスト容易性: IUnitOfWorkIRepository をモックすることで、DBなしで単体テストが可能になります。

注意点

  • 過剰な抽象化: EF Core自体が既にUnit of Workパターンを実装しているため、小規模なプロジェクトではわざわざ IUnitOfWork インターフェースを作らず、直接 DbContext を使用する方がシンプルな場合もあります(YAGNI原則)。
  • 集約の境界: 1つのトランザクションで複数の集約を更新することは、集約の設計原則(1トランザクション1集約)と競合する場合があります。整合性が必要な場合は、ドメインイベントと結果整合性の利用も検討してください。