CakePHP-ja tumblr

Nov 12 2009

エンティティ的なDB設計を実装する

CakePHP の O/R マッパーはいささか大仰なものであり、多少なりとも SQL 言語を理解している向きにとっては大味に映る。それはともかく CakePHP でエンティティ的な DB 設計を実装する方法を考えてみた。

まず、entities というテーブルがあり、モデル名は Entity とする。これが全てのテーブルの全てのレコードの実体となるデータを格納するテーブルだ。

CREATE TABLE `entities` (
  `updated_id` int(20) NOT NULL auto_increment,
  `foreign_uuid` char(36) NOT NULL,
  `modified` timestamp NOT NULL default CURRENT_TIMESTAMP,
  `type` varchar(255) NOT NULL,
  `body` longtext NOT NULL,
  PRIMARY KEY  (`updated_id`),
  UNIQUE KEY `foreign_uuid` (`foreign_uuid`),
  KEY `class_name` (`type`)
) ENGINE=InnoDB AUTO_INCREMENT=5114 DEFAULT CHARSET=utf8;

こんなところだろうか。

foreign_uuid は entities テーブルに保存されたレコード、つまりエンティティがどのテーブルのレコードに依存するデータであるかを指し示す。
さらに、type はエンティティのクラス名を指し示すのだが、リソース的な視点から合理的なデータ構造は「インスタンスの中から必要なデータだけを抽出した配列をシリアライズしたもの」であると考えられる。
気になるなら圧縮でもすればよいが、それも同じベクトル上にある対策の一つでしかないので割愛する。

上記のデータを格納するのが body である。
updated_id はデータのシークエンスを保証するだけに過ぎないし、modified は更新日時を記すだけである。

少し横道にそれるが、foreign_uuid となっているのは「全てのテーブルの全てのレコードに対するエンティティを一つのテーブルに保存する」と言う要件を満たすため「全てのレコードで通じる固有性」を UUID で実装した結果である。
CakePHP で UUID を実装するのはとりわけ簡単で、最低限カラム名が uuid となっていれば機械的に解釈してくれる。もしくは String::uuid() がその都度 UUID を返してくれる。

無論、それらの UUID はまだ使用されていない事が保証されたデータではないのだが、entities テーブルに全ての uuid が格納されているのでその辺りの固有性確保はさほど問題ではない。

次に、エンティティの基底クラスとなる EntityModel を実装する。
詳しい実装内容については割愛するが、前述のシリアライズされたデータの再利用性を高めるのであれば最低限 Factory メソッドと Flyweight メソッド程度は揃えておきたい。

DB から抽出したデータを Factory メソッドでエンティティクラスのインスタンスとして再生成し、生データ自体は foreign_uuid でいつでも参照できるようクラスの静的なメンバ変数に格納しておく。
再利用したい場合は Flyweight メソッドで生データから再生成してやればよい。

それらの格納した生データの新規性が損なわれた場合に備えるために init メソッドがあっても良いだろう。init メソッドは新規性云々だけにではなく、例えば「あるカテゴリに属するレコードを抽出する」場合の”あるカテゴリ”を複数回にわたって問い合わせるような場合において必要不可欠なメソッドである。

そうでなければ生データを保持し続ける Factory メソッドに抽出したデータの抽出条件を解釈させなければならず、そんな手間をかけるくらいなら init で全部削除してしまっても良い制御シーケンスを実装すべきだ。

また、連続したデータを扱う以上 Iterator メソッドもあったほうが良い。単純に保持しているデータを Flyweight メソッドで呼び続け、最後までいったら false でも返してやればよい。

ここまで読んだ賢明な仮想読者諸兄ならばお気づきであろうとおり、これは「ハッシュテーブルに複数のインデックスを付与する」実装である。つまり、インデックスの数だけテーブルを増やせば良く、一つのテーブルに対してうんうん唸ってインデックスをひねり出してやる必要はない。
その点この実装は富豪的にリソースを消費するため冗長だと言われればそれまでだが、仕様変更に伴うインデックスの修正も気にする必要はない。新たにテーブルを作ればいいのだ。

ここでひとつだけ留意すべき点がある。複数のテーブルと複数のエンティティがあったとしても、一度抽出したデータを静的に保持するエンティティクラスは大元の基底クラスだけに留めておくべきである。
継承したそれぞれのエンティティはあくまでも入れ物に過ぎない。どのデータがどのエンティティに収まるべきかは基底クラスだけがコントロールできるようにすべきである。そうでない場合リソースのコントロールが極めて煩雑になる可能性が高い。

この点を解決するためだけに基底クラスは基底クラスでなければならないというわけである。

次に、これらのエンティティ的な実装での副産物的効果を考えてみた。

まず考えられるのがモジュラー実装である。
任意のタイミングで任意の処理を走らせる、イベントドリブン的な実装を実現しやすい。

エンティティクラスにイベントのトリガとなるメソッドを用意しておけば良いからである。
例えば、データが抽出された回数を記録しておきたいような場合、Factory メソッドに設置したトリガにフックしたメソッドで記録すれば良い。これだけでインデックスとエンティティに対するロジックをそれぞれから分離することができる。つまり、インデックスの変更についてエンティティを考慮する場面が少なくなるのである。

継承すべきではないメソッドが多数ありクラス間の継承関係をより実際的な使い方をする辺りを合理的な考えだと僕は気に入って使っているのだが、ここまで作ると CakePHP の O/R マッパーが逆に邪魔で仕方なくなってくる。邪魔だからエンティティを採用したのか、エンティティを採用したから邪魔になったのか、その辺りは仮想読者諸兄の想像にお任せするとして、本件はここまでとする。

Page 1 of 1