男爵が書く

DDD、オブジェクト指向、技術書の感想など

PofEAAで考える値オブジェクトの永続化あれこれ

この記事はドメイン駆動設計#1 Advent Calendar 2019 - Qiitaの3日目の記事です。

エリック・エヴァンス氏の『ドメイン駆動設計』に端を発したDDDの設計哲学では、システムが同一性を認識しなければならないエンティティと、同一性を認識しなくて良い値オブジェクトを区別して設計することが重要であるとされています。例としてよく使われるのが「貨幣オブジェクト」です。

// 貨幣
class Money {
  String code; // 通貨コード
  Double value; // いくらかを表す数値
}

疑似コードなので雰囲気で読んでください。このMoneyクラスを使って「100円」というインスタンスを2つ作ったとします。システムはこれらのインスタンスが同一の貨幣を指しているのか、それとも偶然値が一致している(等価である)だけなのかをどのように判別すれば良いでしょうか? ここで「判別しなくて良い」と言えるシステムなら、貨幣オブジェクトを値オブジェクトとして扱うことが出来ます。極端な例として、日本中の貨幣にIDを振って管理する「貨幣流通管理システム」のようなものであれば、そうはいかないでしょう。

貨幣のようなものは、道端にポンと置かれていることはあまりありません。道端に落ちている貨幣は、誰のものかわからなくなります。システム内でも同様で、永続化対象となる値オブジェクトは何らかのエンティティの中に入っているはずです。IDを持ったエンティティの中に入っていなければ、永続化した値オブジェクトを再度取り出す手がかりが無くなってしまうためです。

// 財布
class Wallet {
  String id; // 財布のID
  Money money; // 財布の中身
}

では、このエンティティ内の値オブジェクトはどのようにデーターベースに永続化されるのでしょうか? オブジェクトをデーターベース、特にリレーショナル・データベースに永続化する際には、オブジェクトの形とテーブル構造を必ずしも一致させられないという問題があります(インピーダンスミスマッチ)。Walletのような複合的なオブジェクトを保存するには、何らかの実装方針を選択しなければなりません。

これついてはマーティン・ファウラー氏が『エンタープライズアプリケーションアーキテクチャパターン』(PofEAA)の中で答えてくれています。『ドメイン駆動設計』の出版より少し前の本ですが、値オブジェクトというアイデア自体はオブジェクト指向界隈にそれ以前からあったものなのです。PofEAAは実装上の課題一つに対していくつかの解決策を挙げ、それぞれどのような長所・短所があるのかを解説するというスタイルで書かれています。値オブジェクトの永続化に関しても、3つのデザインパターンが紹介されています。

小さなオブジェクトを一つだけ

まず上記に挙げたようにWalletオブジェクトの中に一つだけMoneyオブジェクトが入っているようなケースでは、Embedded Valueパターンが有力な候補になるでしょう。

www.martinfowler.com

Embedded Valueパターンは、親となるエンティティを永続化するテーブルに、値オブジェクトの各プロパティを分解して入れてしまうという方法です。例えば、walletsテーブルは以下のような定義になります。

CREATE TABLE wallets (
  id VARCHAR(32) PRIMARY KEY COMMENT '財布のID',
  money_code CHAR(3) COMMENT '通貨コード',
  money_value FLOAT COMMENT 'いくらかを表す数値'
);

永続化する際にはWalletオブジェクトの各プロパティと、Moneyオブジェクトの各プロパティをそれぞれ対応するカラムに入れます。そして復元する際には、まずMoneyオブジェクトのプロパティを取り出して組み立て、次にWalletオブジェクトのプロパティを取り出して、先に作っておいたMoneyオブジェクトの参照を持たせれば、元通りのWalletオブジェクトが手に入ります。この辺りの具体的な実装方法は、DDD界隈ではリポジトリの話題として扱われます。PofEAAの中でも様々なパターンが紹介されているので、気になる方はぜひ読んでみてください。

Embedded Valueのメリットは、エンティティの中にある小さな値オブジェクトのために個別のテーブルを作らなくて良くなる点にあります。また、エンティティを取得する際にテーブルのJOINが不要になるため、パフォーマンスにもいい影響を与えるでしょう。

複雑な、もしくは複数のオブジェクト

さきほどのWalletクラスの定義を見ておかしなところに気付いたでしょうか? そう、この財布には貨幣を一つしか入れられないのです。それでは困るので、貨幣をいくつでも入れられるようにしてみましょう。

// 財布
class Wallet {
  String id; // 財布のID
  List<Money> monies; // 財布の中身
}

moneyは不可算名詞ですが、複数であることがわかりやすいようmoniesとしました。ともあれ、これで100円玉、1000円札、1ドル札といった様々な貨幣を財布に入れられるようになりました。しかし、この形のオブジェクトはEmbedded Valueパターンによるテーブル定義では対応できそうにありません。そこで、次のデザインパターンであるDependent Mappingパターンの登場です。

www.martinfowler.com

このパターンを使える条件は「親オブジェクトと子オブジェクトがあり、子オブジェクトが常に親オブジェクトと共に永続化・復元される。子オブジェクトは一つの親にのみ所属する」というものです。これはエンティティと、その中にある値オブジェクトの関係性と合致します。*1

Dependent Mappingでは、子オブジェクトを親オブジェクトとは別のテーブルに永続化します。その際、Moneyオブジェクト用のテーブルには、親のWalletオブジェクトのレコードに対する外部キー制約を張っておきます。また、値オブジェクトには本来IDがありませんが、テーブル設計の作法としては便宜上のプライマリキーを決めておくと良いでしょう。ここではWalletのIDと「財布の中での連番」を複合プライマリキーとして設定しています。

CREATE TABLE wallets (
  id VARCHAR(32) PRIMARY KEY COMMENT '財布のID'
);

CREATE TABLE monies (
  wallet_id  VARCHAR(32) COMMENT '財布のID', -- Moneyオブジェクトのプロパティではない
  no INT(11) COMMENT '財布の中での連番', -- Moneyオブジェクトのプロパティではない
  code CHAR(3) COMMENT '通貨コード',
  value FLOAT COMMENT 'いくらかを表す数値',
  PRIMARY KEY (wallet_id, no),
  FOREIGN KEY (wallet_id) REFERENCES wallets (id) 
);

永続化の際にはまず、WalletのプロパティのうちMoneyオブジェクト以外のもの(この場合はidのみ)を持つレコードをwalletsテーブルに挿入します。その後、Moneyオブジェクト一つにつき一つのレコードをmoniesテーブルに挿入していきます。Moneyオブジェクトの財布の中での連番はその際に採番します。

復元はまず、moniesテーブルに対してwallet_idカラムをキーとしつつnoカラムでソートした状態で検索し、Moneyオブジェクトのリストを作ります。その後、walletsテーブルに対してidカラムをキーとして検索してWalletオブジェクトを作り、さきほど作ったMoneyオブジェクトのリストの参照を渡せば元通りのWalletオブジェクトが手に入ります。

このパターンで難しいのは更新の処理です。値オブジェクトについて勉強された方は、「値オブジェクトは不変(イミュータブル)である」ということを理解されていると思います。値オブジェクトに変更があった場合、元のオブジェクトが変更されるのではなく、新しいオブジェクトが作られます。そして元のオブジェクトは捨てられます。ではこの性質をデータベース上でどう扱えば良いでしょうか? そもそも値オブジェクトにはIDがないため、既存のレコードを更新することなどできるのでしょうか? また例えば、財布の中身が「100円、1000円、1ドル」から「500円、100円、2ユーロ」に変化した場合、追加された値オブジェクトと削除された値オブジェクトの差分をどのように検出すれば良いでしょうか?

PofEAAに書かれている実装方針はシンプルで、全ての値オブジェクトのレコードをDELETEし、再度INSERTし直せば良いというものです。値オブジェクト自身の性質と同じく、元のレコードを破棄して、新しいレコードを作るということです。通常、リレーショナル・データベースのレコードは他のテーブルからの関連(リレーション)を考慮しなければならないので、安易に削除することができません。ですがDependent Mappingで値オブジェクトを永続化するテーブルは、どのテーブルからも関連を繋がれていないはずです。リレーションは一般的にプライマリキーに対して向けられますが、値オブジェクトのレコードのプライマリキーはレコード挿入時に便宜的に作られた意味のない値だからです。

ここではエンティティが値オブジェクトのコレクションを持っているケースを取り上げましたが、値オブジェクトのプロパティがたくさんある場合にもこのパターンが効果を発揮します。値オブジェクトが一つだけであっても、親のエンティティのテーブルの肥大化を避けるために、値オブジェクトのテーブルを分離するというのは理に適った選択になると思います。

複雑なオブジェクトツリー

最後のデザインパターンは、Serialized LOBパターンです。

martinfowler.com

Serialized LOBでは値オブジェクトやそのコレクションを、文字列やバイト列にシリアライズして一つのカラムに保存します。最近のリレーショナル・データベースではJSONデータ型が使えることが多いので、ここではMoneyオブジェクトのリストをJSON形式の文字列にシリアライズしてみましょう。

[
  {"code": "JPY", "value": 500},
  {"code": "JPY", "value": 100},
  {"code": "EUR", "value": 2}
]

テーブル定義は以下のようになります。

CREATE TABLE wallets (
  id VARCHAR(32) PRIMARY KEY COMMENT '財布のID',
  monies JSON COMMENT  '通貨のコレクション'
);

これまでのテーブル定義に比べて、とても単純になりました。またDependent Mappingのように更新処理の実装方法に悩まされることもありません。さらには「値オブジェクトのコレクションを持つ値オブジェクトのコレクション」など、複雑なオブジェクトツリーを永続化する場合でも実装コードの複雑さは変わりません。それほどのメリットがあるなら、常にSerialized LOBで実装すればいいのではないかと思ってしまいます。

しかし、このパターンにはそれらのメリットを相殺しかねないほど顕著なデメリットがいくつかあります。その一つはデータ構造の変更が困難になることです。例えば、Moneyオブジェクトに「発行年」を表すプロパティを追加したいとします。

class Money {
  String code; // 通貨コード
  Double value; // いくらかを表す数値
  Date publishedAt; // 発行年
}

walletsテーブルに既にいくらかのレコードが挿入されていた場合、moniesカラムをどう処理すれば良いでしょうか? JSONデータ型の中身の構造を通常のALTER文で変更することはできません。かといって既存のレコードをそのままにしておくと、古いMoneyオブジェクトを復元しようとした時にデシリアライズに失敗してしまいます。これを修正するには、古い形式のレコードから新しい形式のレコードに書き換えるための何らかのプログラムを記述する必要があるでしょう。

他にも以下のようなデメリットがあります。

  • シリアライズされたデータの一部を検索条件とする場合、パフォーマンスチューニングの難易度が上がる*2
  • 運用中のイレギュラーな対応として手入力でレコードを修正する場合、書き間違えのリスクが上がる

もっと詳しく知りたい方には『失敗から学ぶ RDBの正しい歩き方』の「JSONの甘い罠」の章を読んでいただくのがお薦めです。 gihyo.jp

いずれにしてもメリット・デメリットを足し引きして、妥当性をしっかり検討した上で選択しなければならないパターンだと言えるでしょう。個人的には、列挙型を表現する値オブジェクトのコレクションなど将来的にも構造が変わりそうにない場合には、Serialized LOBのメリットを享受しても良いのではないかと思っています。

まとめ

この記事ではDDDにおける値オブジェクトの永続化の実装方法として、PofEAAからデザインパターンを3つご紹介しました。これらのパターンの名前を知らなかった方も、このどれかに似た形でこれまで実装されていたのではないでしょうか。デザインパターンの真価は開発者の間に共通の語彙を作り、先人が蓄積したメリット・デメリットの知識を下敷きに設計を発展させられるところにあると思います。いつか実装方法を検討する際に、これらのパターンの名前を使ってみていただけると幸いです。

また、この記事では話を簡単にするため、集約とリポジトリついては深入りしませんでした。筆者の去年のアドベントカレンダーの記事を合わせて読んでいただくと、理解の助けになるかもしれません。

dnskimox.hateblo.jp

明日は集約のエンティティこと@pictinyさんの記事です。

参考文献

www.shoeisha.co.jp

www.shoeisha.co.jp

*1:厳密に言えば、値オブジェクトの参照を複数の親オブジェクトで共有することも有りえます。Flyweightパターン(GoF)はそれによってメモリ効率を良くする例です。

*2:パフォーマンスチューニングが必ずしも不可能になるわけではありませんが、MySQLのGenerated Columnsのような特殊な機能を使ってINDEXを作成する必要があるでしょう。