こまどブログ

技術のこととか仕事のこととか。

関連テーブルへの操作と、ドメイン駆動設計における集約・リポジトリ

ドメイン駆動設計を意識しながら設計をしている*1ときに、関連テーブルの操作に関して悩んでいたことがあった。人に相談に乗ってもらい、自分でも改めて書籍などを見ながら考え直したところ、自分の集約への理解が全く不十分であったことがわかった。

TL;DR

問い:

ある集約への変更のために、関連テーブルへの操作を実体のテーブルへの操作と別に提供したくなったらどうする?

回答:

集約の単位とは独立に提供したくなるのは集約をちゃんと考えられていないからかもしれないので考え直そう。

前提

「部署」と「社員」という2つの実体*2の間に、「所属」という多対多の関連が存在しているとする。部署は部員として複数の社員を持つことができる一方で、社員は所属先として複数の部署を持つことができる(つまり、兼部を許す)。「所属」を連関エンティティとすることで、以下のようなデータベース設計になっているとする。

f:id:ky_yk_d:20190420111815p:plain

このようなデータの保存形式をとっている部署に対する、以下のようなユースケースをサポートしたい。DB上のデータ操作も併せて記載する。

部署への操作 操作対象テーブル データ操作
名称を変更する 部署テーブル UPDATE 部署名カラムを更新する
新たな社員を配属する 所属テーブル INSERT 新たなレコードを追加する

問い:

以上の操作を提供するために、データベースアクセス手段をどのように提供するべきなのか。

悩みと相談

集約の単位として部署を用いるのであれば、部署リポジトリのメソッド、たとえばsave()で両方をまとめて実行するというのがセオリーだろう。しかし、操作対象のテーブルが異なっており、ユースケースとしても独立しているという理解であったため、あえてドメイン層で束ねる意味があるのだろうか、という疑念が頭から離れない。

先日参加した勉強会の懇親会で、n.sienaさんとkabukawaさんがいらっしゃったので、疑問をぶつけてみたところ、n.sienaさんから「ユースケースに合わせたデータベースアクセス方法を提供するというのは、トランザクションスクリプト的な発想ですね」との言葉をいただいた。

やはり、集約として部署を設定したのであれば、部署リポジトリsave()メソッドの内部で複数のテーブルにアクセスするのが自然であるという話になった。実装するならば、以下のようなコードになるようだ*3

public class DepartmentRepositoryImpl implements DepartmentRepository{
  @Autowired
  DepartmentDao departmentDao;
  @Autowired
  AffiliationDao affiliationDao;
  @Override
  public void save(Department aDepartment){
    // 部署テーブル側の更新
    departmentDao.update(mapper.map(aDepartment, DepartmentEntity.class));
    // 所属テーブル側の更新(挿入)。この辺はORM依存になりそう
    List<AffiliationEntity> affliations = someMethod(aDepartment);
    for (AffiliationEntity e : affliations){
      affiliationDao.updateOrInsert(e);
    }
  }
}

ここまでの話で、「アクセス先のテーブルが別々でもひとつのリポジトリでまとめて処理するのでいいのだ」と納得したので、その場は別の話題に移った。そのまま遅くまでリレーショナルモデルについてなどの話をして、すっきりした気分で帰宅した。

もやもやの正体:集約への無理解

翌朝、ブログを書こうと改めて整理を試みてみると、集約の境界とは異なる単位でデータベースアクセス手段を提供したくなっていたのは、集約の設計に違和感があるということの現れだということに気づいた。

そもそも、ドメイン駆動設計における集約とはトランザクション整合性の境界であり、なんらかの不変条件に服するデータ群を整合性のある状態に保つためのパターンなのである。

整合性の境界の論理的な意味は、「その内部にあるあらゆるものは、どんな操作をするにかかわらず、特定の不変条件のルールに従う」ということだ。この境界の外部にある、あらゆるものの整合性は、集約とは無関係になる。つまり、集約はトランザクション整合性の境界と同義である。*4

したがって、リポジトリは集約全体をまとめて永続化する以外の手段を提供すべきではない。ORMのDaoが提供するような個別のテーブルに対する更新は、集約の一部を独立して更新する手段を提供するものであり、バグを生みかねないのだ。

 DAOやそれに関連するパターンを使って、集約の一部とみなされるようなデータに対する、きめ細やかなCRUD操作も行えるので、これはドメインモデルと組み合わせて使うのを避けたほうがいいパターンだといえる。通常の条件の下では、集約自身にビジネスロジックなどの内部処理を管理させて、それ以外にはもらさないようにしておきたい。*5

集約の単位を考え直す

今回の例であれば、名前と部員を同時に変更するというのを、トランザクション整合性の求められる操作として見るべきである、つまり「ひとつの部署の名前と部員との間に担保すべき不変条件がある」のであれば、上に示したような部署リポジトリの設計・実装になるであろう。

しかし、もし部署の名前と部員との間に担保すべき不変条件がないのであれば、集約というトランザクション整合性の単位に括るのではなく、別々の集約として整理して、結果整合性*6を用いればよいのだろう。

いずれにせよ、「ドメイン駆動設計だからリポジトリで関連テーブルも併せて更新する」というようなものではなくて、モデル自体を吟味することが必要なのだ。

おわりに

『エリック・エヴァンスのドメイン駆動設計』や、実践している人のブログなどを読んで、何となくわかってきている気がしていたが、実際に適用してみようとすると全く理解できていないことを痛感させられる。まだまだこれからだ。

※なお、この記事で挙げた部署と社員の例は、実際の業務とは関係のない架空のものである。

Special Thanks

n.sienaさん、kabukawaさん、長時間相談に乗っていただき、ありがとうございました。

また、今回の相談の現場となったのは、「OCHaCafe#5 - 避けては通れない!認証・認可」の懇親会でした。勉強会本体もとても勉強になりました。ありがとうございました。

実践ドメイン駆動設計 (Object Oriented SELECTION)

実践ドメイン駆動設計 (Object Oriented SELECTION)

*1:「エンティティ」、「リポジトリ」、「集約」等の用語はドメイン駆動設計におけるものとする。

*2:ERモデルにおける「実体=エンティティ」であり、ドメイン駆動設計の「エンティティ」ではない。

*3:なお、ここではORMで2種類のDaoを使ってそれぞれのテーブルにアクセスしている風で記述しているが、特定のORMの正確な構文にはなっていない。

*4:『実践ドメイン駆動設計』340頁)

*5:『実践ドメイン駆動設計』425頁。

*6:『実践ドメイン駆動設計』350頁