読者です 読者をやめる 読者になる 読者になる

DDDにおけるDBアクセス回数とドメインモデル表現のトレードオフについて

DDD

DDDでは「適切なドメインモデリング」が重要になってくる。

このモデリングには明確な答えがなかったりするので、
自分が(チームが)適切だと思ったモデリングで進めていくと思うんだけど、
DBアクセスを伴う仕様では上手く表現できないケースが有る。

具体的に言うと、
自分が正しいと思うドメインモデリングでは、
DBアクセス回数が増えてしまい、
パフォーマンスが気になってしまうことがある。
(これは自分がソーシャルゲームのような瞬間的に高負荷になるサービスに携わっていたからかもしれない)

チャットアプリケーションを例に挙げて説明する。

チャットには「ルーム」があり、ルームには「メンバー」が存在する。
メンバーはルームへの入室、退室ができるので、
ルームにメンバーが存在しない場合もある。

ここで「ルームにメンバーが存在するかどうか」をチェックする実装を考える。

個人的には以下の様なコードを書きたい。

<?php
$roomId = 1;
$room = RoomRepository::getById($roomId);
if($room->isExistMember() === true){
	//メンバーが存在する
}else{
	//メンバーが存在しない
}

ドメイン部分しか書いていないが、
RoomRepository::getById() で Roomエンティティを取得して、
Roomエンティティに実装してある isEmpty() でメンバーの存在を確認するイメージ。
個人的にはこれが自然だと思う。

でも、ルームとメンバーといったデータはDBで管理されている事が多い。

以下の様な roomテーブル と memberテーブル で管理されているだろう。
memberテーブル は roomテーブル への外部キーである room_id を持っている。

create table room (
	id int,
	name varchar(10)
);

create table member (
	id int,
	room_id int,
	name varchar(100)
);

このようなテーブル構造で
さきほどのコードを実装すると、
RoomRepository::getById() で roomテーブル にクエリを発行して、
$room->isExistMember() で Roomエンティティの id をベースに memberテーブル にクエリを発行することになると思う。

実際にコードにすると以下。
ちょっと雑に書いたけど、クエリが2回発行されるというのがイメージできるはず。
イメージできるといいな・・・。

<?php
class RoomRepository {
	public static function getById($id){
		$sql = "select * from room where id = $id";
		return Db::query($sql); //ここでroomエンティティが返ってくるとする。
	}
}

class Room {
	public function isExistMember(){
		$members = MemberRepo::getByRoomId($this->id); //ここでもRoomRepositoryのようにクエリを発行する。
		if(empty($members) === true){
			return false;
		}
		return true;
	}
}

つまり、最初にroomを取得してから、そのroomに紐づくmemberを取得することになると思う。

ただ、ルーム内にメンバーが存在するかどうかであれば、
SQLのEXISTSを利用してDBに(リポジトリに)問い合わせた方がクエリが少なくて済む。
以下の MemberRepo::isExistMemberByRoomId() がその部分。

<?php
$roomId = 1;
if(MemberRepo::isExistMemberByRoomId($roomId) === true){
	//メンバーが存在する
}else{
	//メンバーが存在しない
}

以下の実装も考えられるかな。

<?php
$roomId = 1;
$members = MemberRepo::getByRoomId($roomId);
if(empty($members) === true){
	//メンバーが存在しない
}else{
	//メンバーが存在する
}

サーバサイドのデータはDBに保存されているので、
SQLの問い合わせで解決できてしまうものが多く、
エンティティが貧弱になってしまうような気がする。
言い方を変えると、リポジトリがリッチになってしまう。

リポジトリを利用すれば事足りるので、
エンティティって不要じゃない?
という考え。
でも、それって今まで自分が書いてたプログラムというか、
設計に回帰してる・・・。
DDDじゃないような・・・。

リポジトリドメインレイヤだけど、
基本的にDIPでインターフェースを用意するだけだから、
結局インフラレイヤにドメインロジックが流出してしまう可能性もある。
(まあ、ここはロジックをドメインに寄せることで、ある程度は解決できるんだけど・・・)

ただ、「ルーム内にメンバーが存在するかどうか」を表現するSQL
ドメインロジックの流出になるのか? という疑問もある。
結局「memberテーブルのレコード存在確認」なので、
それをドメインロジックの流出を捉えるとあらゆるDBアクセスがドメインロジックの流出になってしまう。
ドメインロジックの流出については別で記事書こうかな。

ということで、
DBアクセスが多くなるのがちょっと嫌というお話でした。

解決方法として、
roomテーブルにmemberの数を持たせる方法がある。

create table room (
	id int,
	name varchar(10),
	member_count int -- これがmember数
);

こうすればRoomエンティティを取得するだけでメンバーの存在を確認できる(member_countが0かどうかで)。
DDDはドメイン駆動なので、
本来はDBよりもドメインを重視して設計する必要があるんだけど、
やっぱりDBも大事だから、member_countを持たせるような設計を適用するかどうかは
ケース・バイ・ケースな気がする。

どうやって実装しようかは考え中。
いい方法があればいーけど。


--- 追記 ---
ドメイン駆動設計のサンプルを確認してみると、
エンティティ内でリポジトリを利用していなかった。
なので、自分が良いと思った以下の実装は違う気がする。

<?php
$roomId = 1;
$room = RoomRepository::getById($roomId);
if($room->isExistMember() === true){
	//メンバーが存在する
}else{
	//メンバーが存在しない
}

「ルームにメンバーが存在するか 」

「ルームに存在するメンバー数が0か」
ということなので、
以下のようにリポジトリからメンバーを取得して確認するだけでいいのかな。

<?php
$roomId = 1;
$members = MemberRepo::getByRoomId($roomId);
if(empty($members) === true){
	//メンバーが存在しない
}else{
	//メンバーが存在する
}

自分はエンティティに無理やり実装しようとしていただけなのかもしれない。
もっと勉強する必要があるな・・・。
------------


--- 追記2 ---
やっぱり自分の認識が間違っていた。
DDDのサンプルをもう一度確認して学び直す必要がある。
そして、Java の Spring の DI は便利だな・・・。

                          • -