LoginSignup
0
0

More than 1 year has passed since last update.

Doctrine ORMでPostgreSQLのforeign keyをDEFERRABLE INITIALLY DEFERREDにする

Posted at

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:createdoctrine: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の deferrabledeferred を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だと思います!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0