Doctrine や Eloquent や CakePHP はいかにして差分更新を実現しているか

Doctrine や Eloquent や CakePHP などの ORM でDBからフェッチしたエンティティの一部の属性だけ変更して保存したとき、テーブルの行全体が更新されるわけではなく、変更した一部の属性だけが更新されますが、それがどう実装されているか気になったので調べたメモ。

Eloquent

Laravel の Eloquent はエンティティ(モデル)が POPO ではないので、エンティティ自身にいろいろ情報が詰め込まれています。

モデルの HasAttributes トレイトで、

フェッチしたときの元の値を保持していて、

元の値との比較で更新すべき属性のリストを得ます。

CakePHP

Laravel の Eloquent と同じく、エンティティが POPO ではないのでエンティティ自身にいろいろ情報が詰め込まれています。

(実際に試してはいないんですけど)、EntityTrait トレイトで DB からフェッチしてきてから変更や追加されたプロパティの一覧を持っていて、

更新時に Entity から変化のあったプロパティだけ取り出して SQL を作ります。

Doctrine

Symfony などで使われる Doctrine はエンティティが POPO なので Eloquent や CakePHP のようにエンティティにいろいろ詰め込むことは出来ないはずですが?

EntityManager の中の UnitOfWork で、

$originalEntityData という、DBからフェッチした元の Entity の値を保持していて、

保存時に、フェッチしたときの元の値と Entity の値を比較して更新するセットを導出しています。

ので、 Entity 自体は POPO のままで、比較による差分での部分更新を実現していました。なかなか面白いですね、POPO なエンティティを扱う ORM は同じような実装になっているものなのでしょうか。

zend-db

zend-db の TableGateway や RowGateway は見た感じ差分更新のようなことは行われていなさそうです。RowGateway をフェッチして一部の属性を変更して save すると変更していない属性も含めて全部更新されそうです(試していない)。

さいごに

差分更新が出来ないと特定の状況ですごく不自然な動きになるように思います。

例えば、ユーザーというエンティティがあって、ユーザーの一部の属性だけ(氏名だけ、とか、メールアドレスだけ、とか)編集するフォームがあって、次のように処理していたとします。

  • Repository からユーザーのエンティティを取得
  • リクエストから値を取り出してエンティティの属性に反映
  • Repository でエンティティを DB に保存

ユーザーの氏名だけ変更数フォームと、メールアドレスだけ変更するフォームがあって、[A] は氏名のみを編集するリクエスト、[B] はメールアドレスを編集するリクエストです。この2つのリクエストが次のような順番で処理されると・・

  • [A] Repository からユーザーのエンティティを取得
  • [A] リクエストからメールアドレスを取り出してエンティティの属性に反映
  • [B] Repository からユーザーのエンティティを取得
  • [B] リクエストから氏名を取り出してエンティティの属性に反映
  • [A] Repository でエンティティを DB に保存
  • [B] Repository でエンティティを DB に保存

最後の段で差分更新が行われていないと [A] によるメールアドレスの変更は [B] による氏名の変更によって上書きされるので残りません。ですが [B] としては氏名だけ変更するフォームで操作しただけなので [A] に問い詰められても氏名しか変更してないので知らんがなです。

差分更新ができれば [A] によるメールアドレスの変更も [B] による氏名の変更も両方残ります。

DBからフェッチした時点で FOR UPDATE なロックしとけば大丈夫ですけど、これだけのために FOR UPDATE は過剰? でもないか??

あるいは、フォームにあわせた特定の属性だけ更新するメソッドをリポジトリに設けたり、

$this->userRepo->updateEmail($user->id, $user->email);

うーん、ログインユーザーの権限によって更新できる属性が異なる、などという仕様だったりすると「特定の属性だけ編集するフォーム」の「特定」が可変になるので破綻します。

では、更新する属性をリポジトリのメソッドで指定してみたり、

$this->userRepo->save($user, ['email']);

うーん、ありかな?