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だと思います!