Doctrine でハマったことまとめ

yositani2002 です。

この記事は [Symfony Advent Calender 2016](http://qiita.com/advent-calendar/2016/symfony) の17日目の記事です。

Symfony 2.x 系以降の Doctrine2 をいじって、過去に躓いたことを遡ってまとめてました。
深掘りできてなかったり、非常に主観的な内容もあるかもしれません。

## 既存の不具合について

本投稿を書いていて知ったのですが、 Doctrine の本家サイトに既知の制約・問題がまとまっているページが有りました。

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/limitations-and-known-issues.html#limitations-and-known-issues


## DoctrineBundleの機能として、shardingについて

sharding は master/slave の設定との同時使用できないようです。
https://github.com/doctrine/DoctrineBundle/blob/master/DependencyInjection/DoctrineExtension.php#L283


master/slave はフェイルオーバーなどの機能はなく、トランザクションを利用しない参照のみ slave に送られるようです。


## One-To-One Bidirectional で繋がるテーブル同士を一つのキーで繋げられない

One-To-One Bidirectional で繋がるテーブル A と B, B と C をキーひとつでつなげることはできない。
真ん中に入るテーブルに同じ値を持つカラムを持たせる必要があるようです。

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html#onetoone

## デフォルト値を設定する場合は、 Entity のプロパティ値に値を入れておく。

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/faq.html#how-can-i-add-default-values-to-a-column

<pre>
options={"default":0}
</pre>

でデフォルト値が設定されるというStackOverflowの回答もありますが、サポートされていない方法と思われます。
http://stackoverflow.com/questions/3376881/default-value-in-doctrine

## QueryBuilder で where 句にプレースホルダーを使わない時の指定方法

嵌ったのは自分だけかもしれませんが、QueryBuilderを使って、Where句のパラメータをプレースホルダーとして設定しない場合の設定方法は、expr()->literal()を使用する。
<pre>
$qb->where($qb->expr()->eq('a.enable', $qb->expr()->literal(true)))
</pre>

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/query-builder.html#binding-parameters-to-your-query

## DQL が対応していない書き方がある

QueryBuilder および DQL は、 SQL の完全な再現は出来ず、生の SQL を書いた方がいい場合もあるようです。
とくに UPDATE や DELETE でテーブルを join して一括で指定するような SQL には対応していないようです。

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html#update-queries

http://stackoverflow.com/questions/37080116/doctrine-bulk-dql-update-with-join

## PostgreSQLの場合、persist()の段階でEntityのIDを付与する

以下、わかりやすくまとまっていました。
http://qiita.com/chihiro-adachi/items/4dac99731817485d51ac
persist() の段階で ID が生成されます。

## Entity がデータベースに保存される順番は、persist() した順番ではない。

データベースへの保存は、 persist() した順番で行われるされるかとおもいきや、以下のメソッドで並びを決めるようです。
https://github.com/doctrine/doctrine2/blob/master/lib/Doctrine/ORM/UnitOfWork.php#L1113
https://github.com/doctrine/doctrine2/blob/master/lib/Doctrine/ORM/Internal/CommitOrderCalculator.php

順番に保存される必要がある場合は、逐次 flush() しましょう。

さらに、保存されるクラスの順番によって、persist() で一度付与された Entity の ID が null に設定されることがあります。
リレーションを持っている Entity で親にあたる方が先に保存されてしまったときのようです。

https://groups.google.com/forum/#!topic/doctrine-user/o5j8PcYc0DY
ここではlooseだと言われています。


http://stackoverflow.com/questions/32766709/make-doctrine-to-add-entities-in-original-order
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/transactions-and-concurrency.html


## EntityManagerの挙動について

EntityManagerはselectで取得したEntityの情報をすべて保持しています。

ChangeTrackingPolicy が Deferred Implicit (デフォルト) のとき、flush()されたときに、保持しているすべてのEntityについて変更があったかチェックし、変更があった Entity についてはDBに保存します。
そのため、既存の Entity を変更したときには persist() しなくてもよいです。
ChangeTrackingPolicy を Deferred Explicit に設定するとpersist()されたEntityだけ、差分をチェックしてDBに保存するようになります。
http://doctrine-orm.readthedocs.io/projects/doctrine-orm/en/latest/reference/change-tracking-policies.html

同様に、 NativeQuery を使った EntityResult で取得した Entity も、 EntityManager に保存されます。
この状態で、
<pre>
$em->getRepository('MyProject\Domain\User')->find($id);
</pre>
したときに、idに一致するEntityをEntityManagerが保持している場合、SQLを投げずに保持している Entity を返します。
NativeQueryで取得したEntityは、addFieldResult()で設定した値しか保持していないため、プロパティが null になっている可能性があるので注意が必要です。
find() で SQL を投げて Entity を取得し直したい場合は、該当 Entity をEntityManager から detatch() してから find() する必要があります。
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-objects.html#detaching-entities

データを再利用する場合以外は、区切りのいいところでクリアしておくといいようです。
<pre>
$em->clear();
</pre>


## Entity の削除の順番について

Entity a と Entity b がリレーションで繋がっていて、onDeleteの設定がない状態で、aとbを両方とも削除するケースでエラーになりました。

<pre>
$a->setB(null);
$em->persist($a);
$em->remove($b);
$em->remove($a);
$em->flush();
</pre>

「bはaから参照されている」というエラーがでました。
ChangeTrackingPolicy が Deferred Implicit の場合は、変更したEntityをpersist()しても、EntityManagerは無視するだけなので、 $a->setB(null); が無視されます。その状態で $b が先に delete されるのでエラーになるようです。

http://stackoverflow.com/questions/14257004/doctrine2-symfony2-cascading-remove-integrity-constraint-violation-1451

削除系のフラグは onDelete="CASCADE", cascade={"remove"}, orphanRemoval="true" と3つあるのでそれぞれはじめに意識して設計する必要がありますね。

http://stackoverflow.com/questions/27472538/cascade-remove-vs-orphanremoval-true-vs-ondelete-cascade

## ネストしたトランザクションについて

詳しく知らな無いのですが、商用のDBですとネストしたトランザクションに対応したものがあるようです。
MySQL や PostgreSQL ではサポートされていないと思います。
代わりに savepoint がサポートされていますので、そちらを利用することで同様のことが出来るようです。

<pre>Doctrine\DBAL\Connection::setNestTransactionsWithSavepoints()</pre>
http://www.doctrine-project.org/api/dbal/2.3/class-Doctrine.DBAL.Connection.html#_setNestTransactionsWithSavepoints

データベースがサポートしている場合のみ成功します。
といっても @Doctrine\DBAL\Platforms@ の中では DB2（IBM Database2）以外はサポートしているようです。


なかなかサンプルを見つけにくかったのですが、以下が参考になりそうです。
http://cgit.drupalcode.org/doctrine/tree/vendor/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ConnectionTest.php?id=e72638ee91c275b504645deb39378ce152ce5680#n64

beginTransaction() を複数回発行し、rollback()した場合、一つ前のトランザクションはまだ生きている状態となります。

トランザクションの中で、内部でトランザクションを使用しているメソッドを呼びだす場合や、成功するまで繰り返し実行する場合などで使えるかとおもいます。

## APCu のキャッシュクリア

Doctrine のキャッシュの保存先として APCu に保存することができます。 その場合、Doctrine のキャッシュクリアのコマンドが効かず、httpd のプロセス経由で消す必要があります。
これは、コマンドラインのプロセスと httpd のプロセスが別の為のようです。

httpd の restart で手っ取り早く消せますが、専用の Bundle があるので、大規模な環境ですとこちらのほうがいいのかもしれません。
https://github.com/Smart-Core/AcceleratorCacheBundle

http://stackoverflow.com/questions/39981501/how-to-clear-doctrine-apc-cache-for-production

## Entity Cache について

あまり深く追いきれていないのですが、Second Level cache の entity cache を使って難儀した記憶があります。
挙動が不安定（だった）ので、ほとんど変更しないようなマスターテーブルのみ、entity cacheを有効にするか、
そもそも使わない方がいいかと思います。

## SoftDeleteで、disableにして値を取りだしていてハマった

論理削除の拡張を使うと透過的にDQLに削除フラグの条件を差し込んでくれて便利です。
https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html

削除済みのものを参照したい場合は、filter を disable にしてクエリーを投げることで参照できます。
<pre>
$filters = $em->getFilters();
$filters->disable('softdeleteable');
</pre>
QueryBuilder および DQL を使わず、生 SQL を書く場合は手動で書くことになります。
きちっと最初に考えて利用すればいいと思いますが、複雑な生 SQL 書いたりする場合は、後でハマるかも知れません。


## EntityManager をコンストラクターに渡すことについて

汎用的な（特にサードパーティーバンドルの）クラスのコードを書くとき、コンストラクタに EntityManager は指定せず、 ManagerRegistry を渡してそこから EntityManager を取得するといいようです。
http://php-and-symfony.matthiasnoback.nl/2014/05/inject-the-manager-registry-instead-of-the-entity-manager/
EntityManager は複数定義でき、 Entity クラスに対して個別に使用できるようになっているためです。

ただ、リンク先の
<pre>
$entityManager = $managerRegistry->getManagerForClass(get_class($customer));
</pre>

で得られる $entityManager は、 ORM を使用していれば、 EntityManager のインスタンスが返ってきますが、 interface の定義では ObjectManager が返るようになっています。そのため、 EntityManager 特有のメソッドを使用する場合、IDE や scrutinizer などの CI ツールでは未定義のメソッド扱いされます。


------------

まとなりのない内容ですが、以上です。

最後に、なんだかんだ言っても Doctrine および Symfony 便利ですよね。 
ありがとうございました。