Doctrine ORMでリレーションを定義すると、PostgreSQL用のforeign key(外部キー制約)は NOT DEFERRABLE INITIALY IMMEDIATE となります。
Doctrine ORMを使っているPHPアプリケーションのみで使うデータベースを作っている場合は特に問題にはならないでしょう。すべてのデータベースアクセスはDoctrine ORMを使って行うので、foreign keyのオプションを気にする必要はないからです。(PostgreSQLの便利機能は使わない前提)
では、他のシステムとデータベース全体や一部テーブルを共有する場合など、データベース設計としてdeferrableなforeign keyがほしいときはどうすればいいか?
Doctrine ORMのDDL管理の仕組み
(ORM) マッピング定義(annotation,attribute,yaml,php設定ファイル等でPHPのクラスにマッピング定義をつける)
↓
(ORM) MappingDriverが各形式のマッピング定義を読み込み、連想配列化
↓
(ORM/DBAL) SchemaToolが連想配列化されたマッピング定義を使って、Schemaを作る
↓
(DBAL) 各データベースごと(MySQL,PostgreSQL,SQLite等)のPlatformがSchemaをSQLに変換
Schemaの段階では、テーブルやカラム・リレーションの情報はDoctrine DBALによって抽象化されており、どのデータベースプラットフォームにも使えるような設定しか持っていません。
一部、オプションだけが各データベースプラットフォーム固有の情報を保持できます。大抵のPHPアプリケーションではマッピング定義の時点で利用するプラットフォームが決まっているため、データベースプラットフォーム固有情報はoptionsやcolumnDefinitionsのようなオプション項目に開発者が埋め込んだものです。
開発者が便利な doctrine:schema:create や doctrine:schema:update 、あるいは doctrine:migrations:diff コマンドを呼び出したとき、Doctrine ORMは内部でこの仕組を使ってSQLを生成したり、実行したりしています。
DoctrineでNOT DEFERRABLEなforeign key制約を作っているのはどこか
DEFERRABLE にしても NOT DEFERRABLE にしてもPostgreSQLの概念なのでPostgreSQLのPlatformクラスにあります。
https://github.com/doctrine/dbal/blob/22de295f10edbe00df74f517612f1fbd711131e2/src/Platforms/PostgreSQLPlatform.php#L488-L514
Platformのコードを見ると deferrable deffered というオプションをForeignKeyに持たせれば良さそうです。
が、SchemaToolのほうを見るとSchemaToolで作られるForeignKeyにはオプションを渡す記述がありません。(渡されるオプションは ['onDelete' => '...'] のみ)
https://github.com/doctrine/orm/blob/5283e1441cc020d526d4842c6bfa21c1a500886d/lib/Doctrine/ORM/Tools/SchemaTool.php#L291-L299
DoctrineのEventシステムを使ってSchemaのオプションを更新する
そこで、DoctrineのEventSubscriberを使って、オプションを更新します。
使えるイベントは2つあります。1つのテーブルだけを更新したいのかすべてのテーブルに適用したいのかによって使い分けると良いでしょう。
| イベント名 | タイミング | 
|---|---|
| Doctrine\ORM\Tool\ToolEvents::postGenerateSchemaTable | 個別のテーブル用のスキーマを組み立て終わった後、テーブル1つについて発火 | 
| Doctrine\ORM\Tools\ToolEvents::postGenerateSchema | 全テーブルのスキーマを組み立て終わった後、スキーマ全体について1回だけ発火 | 
このイベントに対するDoctrineイベントサブスクライバを書き、ForeignKeyConstraintの deferrable と deferred をtrueにセットすることで、 DEFERRABLE INITIALLY DEFERRED としてDoctrineに認識させることができます(schema:updateやmigrations:diffで無駄なdiffが出ません)。下記に全テーブルに適用するサブスクライバの例を示します。
<?php
declare(strict_types=1);
namespace App\Service\Doctrine;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
use Doctrine\ORM\Tools\ToolEvents;
/**
 * foreign keyをdefferable initially deferredにする
 */
class ForeignKeyDeferrableSchemaSubscriber implements EventSubscriber
{
    /**
     * @var array|bool[]
     */
    private array $myForeignKeyOptions = [
        'deferrable' => true,
        'deferred' => true,
    ];
    public function getSubscribedEvents(): iterable
    {
        return [
            ToolEvents::postGenerateSchema,
        ];
    }
    public function postGenerateSchema(GenerateSchemaEventArgs $args): void
    {
        $schema = $args->getSchema();
        foreach ($schema->getTables() as $table) {
            foreach ($table->getForeignKeys() as $foreignKey) {
                $table->removeForeignKey($foreignKey->getName());
                $table->addForeignKeyConstraint(
                    $schema->getTable($foreignKey->getForeignTableName()),
                    $foreignKey->getLocalColumns(),
                    $foreignKey->getForeignColumns(),
                    array_merge($foreignKey->getOptions(), $this->myForeignKeyOptions),
                );
            }
        }
    }
}
感想
DoctrineはリアルワールドのデータベースであってもPHPエンジニアがORMで扱えるような機能が整っているなぁと再認識しました。
データベースをきちんと管理したいDBAさんがいるようなプロダクトにもピッタリのORMだと思います!
