0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Node.js】リファクタリングを通して学ぶドメイン駆動設計 Part5: リポジトリの実装

Last updated at Posted at 2025-12-05

はじめに

この記事は、以下の記事の続きです。

今回はユースケースの作成について説明します。

開発環境

開発環境は以下の通りです。

  • Windows11
  • Docker Engine 27.0.3
  • Docker Compose 2
  • PostgreSQL 18.1
  • Node.js 24.11.0
  • npm 11.6.2
  • TypeScript 5.9.3
  • Express 5.1.0
  • Prisma 6.18.0
  • Zod 4.1.12

インフラ層での実装

Part3でリポジトリのインターフェースを定義しました。今回は、そのインターフェースをPrismaを使って具体的に実装します。

domain/repositories/IUserRepository.ts
export interface IUserRepository {
  save(user: User): Promise<User>;
  findById(id: UserId): Promise<User | null>;
  findAll(): Promise<User[]>;
  update(user: User): Promise<User>;
  delete(id: UserId): Promise<void>;
  existsByEmail(email: Email): Promise<boolean>;
}

インフラ層の役割

インフラ層は、技術的な実装の詳細を担当する層です。データベース、外部API、ファイルシステムなど、具体的な技術に依存する処理をここに配置します。

ドメイン層で定義したインターフェースを、この層で具体的に実装することで、ドメイン層を技術的な詳細から保護します。

リポジトリの実装

Prismaを使ってリポジトリを実装します。

infrastructure/prisma/PrismaUserRepository.ts
import { PrismaClient } from '@prisma/client';

export class PrismaUserRepository implements IUserRepository {
  constructor(private readonly prisma: PrismaClient) {}

  async save(user: User): Promise<User> {
    const data = await this.prisma.user.create({
      data: {
        email: user.getEmail().getValue(),
        name: user.getName().getValue(),
      },
    });

    return User.reconstruct(
      new UserId(data.id),
      new Email(data.email),
      new UserName(data.name)
    );
  }

  async findById(id: UserId): Promise<User | null> {
    const data = await this.prisma.user.findUnique({
      where: { id: id.getValue() },
    });

    if (!data) {
      return null;
    }

    return User.reconstruct(
      new UserId(data.id),
      new Email(data.email),
      new UserName(data.name)
    );
  }

  async findAll(): Promise<User[]> {
    const users = await this.prisma.user.findMany();

    return users.map(data =>
      User.reconstruct(
        new UserId(data.id),
        new Email(data.email),
        new UserName(data.name)
      )
    );
  }

  async update(user: User): Promise<User> {
    const data = await this.prisma.user.update({
      where: { id: user.getId().getValue() },
      data: {
        email: user.getEmail().getValue(),
        name: user.getName().getValue(),
      },
    });

    return User.reconstruct(
      new UserId(data.id),
      new Email(data.email),
      new UserName(data.name)
    );
  }

  async delete(id: UserId): Promise<void> {
    await this.prisma.user.delete({
      where: { id: id.getValue() },
    });
  }

  async existsByEmail(email: Email): Promise<boolean> {
    const count = await this.prisma.user.count({
      where: { email: email.getValue() },
    });

    return count > 0;
  }
}

ポイント1. ドメインオブジェクトとDBモデルの変換

リポジトリの重要な役割の1つは、ドメインオブジェクトデータベースモデルの変換です。

保存時: ドメインオブジェクト → DBモデル

async save(user: User): Promise<User> {
  // ドメインオブジェクトから値を取り出す
  const data = await this.prisma.user.create({
    data: {
      email: user.getEmail().getValue(),  // Email値オブジェクト → string
      name: user.getName().getValue(),    // UserName値オブジェクト → string
    },
  });
  
  // ...
}

ドメインオブジェクトは値オブジェクトを持っていますが、データベースにはプリミティブ型で保存します。

取得時: DBモデル → ドメインオブジェクト

async findById(id: UserId): Promise<User | null> {
  const data = await this.prisma.user.findUnique({
    where: { id: id.getValue() },
  });

  if (!data) {
    return null;
  }

  // DBから取得したプリミティブ型をドメインオブジェクトに変換
  return User.reconstruct(
    new UserId(data.id),        // number → UserId値オブジェクト
    new Email(data.email),      // string → Email値オブジェクト
    new UserName(data.name)     // string → UserName値オブジェクト
  );
}

データベースから取得したプリミティブ型のデータを、値オブジェクトに変換してエンティティを再構築します。

なぜ変換が必要なのか:

  • ドメイン層では型安全な値オブジェクトを使いたい
  • データベースはプリミティブ型で保存する
  • この変換はインフラ層の責務

ポイント2. Prismaへの依存はこの層だけ

元のコードでは、コントローラーから直接Prismaを呼び出していました。

// Before: Prismaへの依存がコントローラーに漏れている
app.post("/users", async (req, res) => {
  const user = await prisma.user.create({
    data: { email, name },
  });
  res.status(201).json(user);
});

リポジトリパターンを使うことで、Prismaへの依存をインフラ層に閉じ込められます。

// After: Prismaへの依存はリポジトリだけ
export class PrismaUserRepository implements IUserRepository {
  constructor(private readonly prisma: PrismaClient) {}
  // Prismaを使った実装
}

メリット:

  • 将来的にPrismaから別のORM(TypeORM、Drizzleなど)に変更しやすい
  • リポジトリの実装を差し替えるだけで、他の層に影響を与えない
  • ドメイン層とアプリケーション層がデータベースの詳細を知らなくて済む

ポイント3. エラーハンドリング

リポジトリでは、データベース固有のエラーをドメイン固有のエラーに変換することもできます。

async save(user: User): Promise<User> {
  try {
    const data = await this.prisma.user.create({
      data: {
        email: user.getEmail().getValue(),
        name: user.getName().getValue(),
      },
    });

    return User.reconstruct(
      new UserId(data.id),
      new Email(data.email),
      new UserName(data.name)
    );
  } catch (error) {
    // Prisma固有のエラーをドメインのエラーに変換
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      if (error.code === 'P2002') {
        throw new Error('Email already exists');
      }
    }
    throw error;
  }
}

ただし、今回のサンプルでは簡潔さのためエラーハンドリングは省略しています。

ポイント4. 一貫性のあるインターフェース

リポジトリのメソッドは、常にドメインオブジェクトを受け取り、ドメインオブジェクトを返します。

// 受け取る: ドメインオブジェクト
async save(user: User): Promise<User>
async findById(id: UserId): Promise<User | null>
async update(user: User): Promise<User>
async delete(id: UserId): Promise<void>

// プリミティブ型は受け取らない(NG)
async findById(id: number): Promise<User | null>  // NG

メリット:

  • 使う側(ユースケース)が扱いやすい
  • 型安全性が保たれる
  • ドメインの概念が一貫している

まとめ

今回は、DDDにおけるリポジトリ(Repository) の実装について説明しました。

  • ポイント
    • リポジトリはドメインオブジェクトとDBモデルを変換する
    • データベース固有の技術への依存をインフラ層に閉じ込める
    • インターフェースを実装することで、実装を差し替え可能にする
    • ドメインオブジェクトを受け取り、ドメインオブジェクトを返す
  • メリット
    • データベースの実装詳細がドメイン層に漏れない
    • ORMの変更が容易
    • テスト用のモックを作りやすい
    • ドメイン層とアプリケーション層がシンプルになる

次回は、コントローラーの実装について説明します。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?