doctrineを利用していて、イケてないと思えるポイントを書き出してみた。
なおdoctrineをそこまで熟知しているわけではないので、もしかしたら解決策がある可能性はある。
設計に関する課題
エンティティクラスが何も継承していない
何も継承していないすっぴんのPHPクラスで定義しているので、共通の処理が一切ない。
必要であれば、自前のベースクラスを定義して継承させる設計が求められる。
例えばこの記事のように。
abstract class BaseEntity {
protected ?int $id = null;
public function getId(): ?int {
return $this->id;
}
}
class User extends BaseEntity {
private string $name;
public function getName(): string {
return $this->name;
}
public function setName(string $name): static {
$this->name = $name;
return $this;
}
}
テーブルの個々の列に対してGetter/Setterが必要
getter/setterだけで数百行になることは珍しくない。
列の名前や型はdoctrine向けのテーブル定義から取得できるはずなので、そもそもこれらは要らないもののはずだ。
getterだけ、setterだけを外から見えないようにしたいニーズは、無くはないが稀だ。
保存時に別のテーブルに影響を与えようとした場合の処理が煩雑
prePersist
や preUpdate
といった仕組みはあるが、ほぼ自身のデータの更新に限られる。
別のテーブルに影響を与えようとした場合、そのテーブルだけでなく、DBへの保存全体に対するフックを用意し、全テーブルの中からそのテーブルに対して変更が来た場合に、別のテーブルのデータを呼び出して変更する処理が必要になる。
クエリの書き方が冗長
バックエンドエンジニアは「データベース読み書きエンジニア」と揶揄されている程度にはデータベースとのやり取りが多い。
にも関わらず、doctrineはとにかく記述量が多い。
getter/setterの件もそうだが、基本的にデータを問い合わせる際の記述は冗長だ。
例えば名前が一致するユーザーがいるかどうかを取得しようとした場合、以下のようなクエリを書く必要がある。
$result = $entityManager->getRepository(Order::class)
->createQueryBuilder('o')
->select('1')
->andWhere('o.name = :name')
->setParameter('name', $name)
->getQuery()->setMaxResults(1)->getOneOrNullResult();
$exists = !is_null($result);
特に where
に関わる部分が冗長で、 AはA'、A'にはA''が入るのような書き方が煩雑だ。
where
とandWhere
があるのもトラップに近い。
以下のような構文でできるようになっていて欲しいものだ。
$exists = $entityManager->getRepository(Order::class)
->where('name', $name)
->exists();
パフォーマンスや動作の課題
メモリーにくわえ込んで管理する
エンティティをpersistするが、その時点では記録しないし、何ならpersistした後でもflushするまで変更が可能だ。
課題はいくつかある。
- 1つ目は「persistとflushを分ける意味があまり無い」ということ
- persistとflushをわざわざ分けることで余計な関心事が増えてしまっている。これはtransactionで囲むことで全体が対象であることが分かるし、flushを適宜挟むのは分かりづらさを助長している側面がある
- 2つ目は「PHP側で余計なメモリを食ってしまう」ということ
- transactionしておけばDB側でより良くメモリ管理をしてくれるところを、PHP側でも余計なメモリ管理コストがかかる
- 二重管理になるだけで意味があるとは考えづらい
- 3つ目はエンティティがロードされていないなどの場合に「あれが無い」「これが無い」とエラーを吐く点
- 頼んでもいないのに勝手にエンティティを管理して、不必要にエラーを出して止めてしまう
- この問題に対処するためには、余計なエンティティをDBからロードするなどの対処が必要になる
N+1問題に対応する方法がほぼ無い
エンティティで別のテーブルにリレーションを書くと、勝手にロードされてしまうことがある。ここまではよくあることだが、問題は複数エンティティ分をまとめてロードする方法がほぼ無いことだ。
それに対処するためには、自前で必要なエンティティのIDを集めて別途エンティティを取得し、そのエンティティを関連元のエンティティに関連付けなければならない。
そのため、N+1対応をしようとした場合、通常の数倍のコードを書くことになる。
雑感
おそらく古めの設計だとこういうこともあるだろう、と感じられる。
doctrineは、私が触ってきたORMの中では、不必要に学習コストが高く感じられた。
記述量や考慮事項が多い当該パッケージを今から採用するメリットはあまり感じられなかった。