ありあけこういち’s diary

SaaS企業Webエンジニア|HSK6級|福岡ラバー|木管楽器|ITについて、明快な読み物を目指しています。

リポジトリについて

はじめに

ドメイン駆動設計におけるリポジトリについてまとめたいと思います。

リポジトリの概要

ドメインオブジェクトが永続化ストレージ(データベースやファイルシステムなど)にアクセスするためのインターフェースを提供するオブジェクトであり、エンティティや値オブジェクト、集約などのドメインオブジェクトの取得や格納を担当します。
なお、リポジトリの一番大きい特徴は永続化の隠蔽です。よって、リポジトリを使用する側がCSVだろうがDBだろうがファイルだろうが知ったこっちゃ無い状態にするべきだと思ってます。

以下は引用です。

オブジェクトを繰り返し利用するには、何らかのデータストアにオブジェクトのデータを永続化(保存)し、再構築(復元)する必要があります。リポジトリはデータを永続化し再構築するといった処理を抽象的に扱うためのオブジェクトです。 -- 成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.144). Kindle 版.

PHPコード例

リポジトリの概念をPHPコードで起こすと下記のようになります。
商品(以下Productオブジェクト)を取得、登録、更新、削除するリポジトリ処理を実装しています。

<?php
declare(strict_types=1);

namespace App\Product;
use App\Product\ValueObject\Price;
use App\Product\ValueObject\ProductId;
use App\Product\ValueObject\ProductName;
use Exception;
use PDO;

class ProductRepository
{
    private const NONE_PRODUCT = 0;
    private PDO $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function findById(ProductId $id): Product
    {
        $sql = "SELECT * FROM products WHERE id = :id";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute(['id' => (string)$id]);

        if ($stmt->rowCount() === self::NONE_PRODUCT) {
            throw new Exception('商品が存在していません。');
        }
        $product = $stmt->fetch(PDO::FETCH_ASSOC);        

        $productId = new ProductId($product['id']);
        $productName = new ProductName($product['name']);
        $productPrice = new Price($product['price']);

        return new Product($productId, $productName, $productPrice);
    }

    public function save(Product $product): void
    {
        $sql = "INSERT INTO products (id, name, price) VALUES (:id, :name, :price)
               ON DUPLICATE KEY UPDATE name = :name, price = :price";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            'id' => (string)$product->getProductId(),
            'name' => (string)$product->getProductName(),
            'price' => $product->getPrice()->toInt()
            ]);
    }

    public function delete(productId $productId): void
    {
        $sql = "DELETE FROM products WHERE id = :id";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            'id' => (string)$productId
        ]);

        if ($stmt->rowCount() === self::NONE_PRODUCT) {
            throw new Exception('IDが' . (string)$productId . 'の商品を削除できませんでした。');
        }
    }
}

$productId = new ProductId('5819f3ad1c4b6');
$pdo = new PDO($dsn, $user, $password);
$productRepository = new ProductRepository($pdo);

//使用例
//商品取得
$product = $productRepository->findById($productId);

//商品作成
$newProductId = new ProductId('5819f3ad1c0ce');
$newProductName = new ProductName('name2');
$newProductPrice = new Price(120);

$newProduct = new Product($newProductId, $newProductName, $newProductPrice);
$productRepository->save($newProduct);

//商品更新
$updatedProduct = new Product($newProductId, new ProductName('name3'), new Price(200));
$productRepository->save($updatedProduct);

//商品削除
$productRepository->delete($newProductId);
?>

この例では、ProductRepositoryクラスがProductエンティティを永続化するためのメソッドを提供しています。

  • findByIdメソッドでは、指定されたIDのProductオブジェクトをデータベースから取得し、Productオブジェクトを返します。
  • saveメソッドでは、与えられたProductオブジェクトをデータベースに保存します。同じIDのProductオブジェクトが存在している場合は更新します。
  • deleteメソッドでは、指定されたIDのProductオブジェクトをデータベースから削除します。

今回はPDO*1を使用してデータベースにアクセスしていますが、LaravelのElloquentモデルなどでも同様に実装することができます。

ドメイン駆動設計とクリーンアークテクチャでのリポジトリの概念について

筆者はドメイン駆動設計でのリポジトリとクリーンアークテクチャでのリポジトリは、どちらも同様の永続化を担当するオブジェクトなので、コード上でも同じのように書いて差し支えないと考えています。
下記は引用です。

Repository は Interface Adapter レイヤーにある GateWays にあたります。 リポジトリパターンで知られており、特定のモデルのデータ永続化についてを抽象化したオブジェクトです。 今回の「ユーザ登録」ではユーザというモデルの永続化が必要になると思います。 -- 引用元:実装クリーンアーキテクチャ

クリーンアーキテクチャでのリポジトリJavaコード例(引用)

public class UserRepository : IUserRepository {
  public User FindByUserName(string username) {
    using (var con = new MySqlConnection(Config.ConnectionString)) {
      con.Open();
      using (var com = con.CreateCommand()) {
        com.CommandText = "SELECT * FROM t_user WHERE username = @username";
        com.Parameters.Add(new MySqlParameter("@username", username));
        var reader = com.ExecuteReader();
        if (reader.Read()) {
          var id = reader["id"] as string;
          return new User(
            id,
            username
          );
        } else {
          return null;
        }
      }
    }
  }

  public void Save(User user) {
    using (var con = new MySqlConnection(Config.ConnectionString)) {
      con.Open();

      bool isExist;
      using (var com = con.CreateCommand()) {
        com.CommandText = "SELECT * FROM t_user WHERE id = @id";
        com.Parameters.Add(new MySqlParameter("@id", user.Id.Value));
        var reader = com.ExecuteReader();
        isExist = reader.Read();
      }

      using (var command = con.CreateCommand()) {
        command.CommandText = isExist
          ? "UPDATE t_user SET username = @username WHERE id = @id"
          : "INSERT INTO t_user VALUES(@id, @username)";
        command.Parameters.Add(new MySqlParameter("@id", user.Id.Value));
        command.Parameters.Add(new MySqlParameter("@username", user.UserName));
        command.ExecuteNonQuery();
      }
    }
  }
}

-- コードの引用元:実装クリーンアーキテクチャ

引用元のJavaコードではクリーンアーキテクチャでのリポジトリの実装であり、ドメイン駆動設計のリポジトリと機能と同様モデルのデータ永続化を担当しています。
そのため、筆者はドメイン駆動設計でのリポジトリとクリーンアーキテクチャでのリポジトリはコード上では違いがないと考えています。

リポジトリとDAOの違い

筆者は、リポジトリとDAO(Data Access Object)は、データアクセスに関わるロジックをカプセル化している点について似た役割をもっていて、確かコード上ではリポジトリとDAOが同様の実装がされることもあるが、目的の違う異なる概念であると考えています。

筆者の理解をまとめると下記です。

  • リポジトリ:アプリケーションコードが、ドメインオブジェクトを操作するのが目的のオブジェクト。
  • DAO:データアクセスのメカニズムをカプセル化していて、データアクセスに目的のオブジェクトであり、基本的にRDBMSに最適化されたインターフェースを持っている。

以下は引用です。

リポジトリは、データベースを操作するため、従来のDAO(データアクセスオブジェクト)と似ているかもしれません。しかし、DAOがデータ中心指向であるのに対して、リポジトリオブジェクト指向アプローチである点で異なります。 -- 実践DDD本 第12章「リポジトリ」~集約の永続化管理を担当~

DAOというのは リポジトリより抽象度の低い、RDBMSに最適化されたI/F(insert, update, delete, selectなどSQLに近いI/F)を持っているはずです。 -- Scalaコードでわかった気になるDDD

上述のように、リポジトリとDAOは目的が異なります。

参考記事、書籍

*1:PHP Data Objectsで、PHPでデータベースにアクセスするためのデータベースアクセス抽象化層(Database Access Abstraction Layer)のことです。