🎻
[Symfony][Doctrine] 外部キーの付け替えを伴うマイグレーションスクリプトの書き方
例
例えば、以下のようなエンティティ構造の変更を考えます。
-
Before
-
店舗が商品を持っている
-
-
After
-
店舗にスタッフが所属していて、スタッフごとに担当の商品を持っている
-

エンティティのコードとしては以下のようなイメージです。
Before
class Shop
{
/**
* @ORM\OneToMany(targetEntity=Item::class, mappedBy="shop")
*/
private $items;
}
class Item
{
/**
* @ORM\ManyToOne(targetEntity=Shop::class, inversedBy="items")
* @ORM\JoinColumn(nullable=false)
*/
private $shop;
}
After
class Shop
{
/**
* @ORM\OneToMany(targetEntity=Staff::class, mappedBy="shop")
*/
private $staves;
}
class Staff
{
/**
* @ORM\ManyToOne(targetEntity=Shop::class, inversedBy="staves")
* @ORM\JoinColumn(nullable=false)
*/
private $shop;
/**
* @ORM\OneToMany(targetEntity=Item::class, mappedBy="staff")
*/
private $items;
/**
* @ORM\Column(type="string", length=255)
*/
private $name;
}
class Item
{
/**
* @ORM\ManyToOne(targetEntity=Staff::class, inversedBy="items")
* @ORM\JoinColumn(nullable=false)
*/
private $staff;
}
マイグレーションスクリプトを自動生成してみる
上記のコード変更を行った場合のマイグレーションについて考えてみましょう。
まずは doctrine:migrations:diff コマンドでマイグレーションスクリプトを自動生成してみます。
bin/console doctrime:migrations:diff
すると以下のようなスクリプトが生成されます。
public function up(Schema $schema) : void
{
$this->addSql('CREATE TABLE staff (id INT AUTO_INCREMENT NOT NULL, shop_id INT NOT NULL, name VARCHAR(255) NOT NULL, INDEX IDX_11111111111111 (shop_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE staff ADD CONSTRAINT FK_11111111111111 FOREIGN KEY (shop_id) REFERENCES shop (id)');
$this->addSql('ALTER TABLE item DROP FOREIGN KEY FK_22222222222222');
$this->addSql('DROP INDEX IDX_22222222222222 ON item');
$this->addSql('ALTER TABLE item CHANGE shop_id staff_id INT NOT NULL');
$this->addSql('ALTER TABLE item ADD CONSTRAINT FK_33333333333333 FOREIGN KEY (staff_id) REFERENCES staff (id)');
$this->addSql('CREATE INDEX IDX_33333333333333 ON item (staff_id)');
}
public function down(Schema $schema) : void
{
$this->addSql('ALTER TABLE item DROP FOREIGN KEY FK_33333333333333');
$this->addSql('DROP TABLE staff');
$this->addSql('DROP INDEX IDX_33333333333333 ON item');
$this->addSql('ALTER TABLE item CHANGE staff_id shop_id INT NOT NULL');
$this->addSql('ALTER TABLE item ADD CONSTRAINT FK_22222222222222 FOREIGN KEY (shop_id) REFERENCES shop (id)');
$this->addSql('CREATE INDEX IDX_22222222222222 ON item (shop_id)');
}
up() の内容を見てみると、以下のようなことをしています。
-
staffテーブルを作成 -
staffテーブルのshop_idカラムにshopテーブルへの外部キー制約を追加 -
itemテーブルのshop_idカラムのshopテーブルへの外部キー制約を削除 -
itemテーブルのshop_idカラムのインデックスを削除 -
itemテーブルのshop_idカラムをstaff_idに名前変更 -
itemテーブルのstaff_idカラムにstaffテーブルへの外部キー制約を追加 -
itemテーブルのstaff_idカラムにインデックスを追加
down()はこの逆ですね。
さて、このマイグレーションスクリプトは期待どおりの内容になっているでしょうか?
よく考えてみれば分かりますが、残念ながらこのままではデータが空の状態でもない限り正常にマイグレーションできません😓
staff_id カラムにはもともと shop_id のつもりで入れたIDは入っているので、そこに新しく作った staff テーブルへの外部キー制約を追加しようとしても以下のエラーになるはずです。
SQLSTATE[HY000]: General error: 1215 Cannot add foreign key constraint
マイグレーションスクリプトを書き換える
というわけで、ちゃんと自力でマイグレーションスクリプトを書き換えてあげましょう💪
up() については、以下のような手順を踏めば正常にマイグレーションできるはずです。
-
staffテーブルを作成して、shop_idカラムにインデックスとshopテーブルへの外部キー制約を追加 -
itemテーブルにstaff_idカラムを追加して、インデックスを作成 (まだ外部キー制約は追加しない) -
shopテーブルを巡回して、各shopに対して必要なだけstaffを作成- このとき、もともと
shopが持っていたitemを何らかの条件に応じて対応するstaffに適切に紐づける - 巡回を終えると、
staffテーブルに必要なレコードがすべて挿入済みで、各レコードのshop_idには適切にshopのIDが入っており、かつstaffがitemからも適切に参照されている状態になる
- このとき、もともと
-
itemテーブルのstaff_idカラムにstaffテーブルへの外部キー制約を追加 (データが整理できたのでここで外部キー制約を追加する) -
itemテーブルのshop_idカラムのインデックスと外部キー制約を削除して、shop_idカラム自体も削除
これをコードにすると、以下のようになるでしょう。
public function up(Schema $schema) : void
{
// staff テーブルを作成して、shop_id カラムにインデックスと shop テーブルへの外部キー制約を追加
$this->addSql('CREATE TABLE staff (id INT AUTO_INCREMENT NOT NULL, shop_id INT NOT NULL, name VARCHAR(255) NOT NULL, INDEX IDX_11111111111111 (shop_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE staff ADD CONSTRAINT FK_11111111111111 FOREIGN KEY (shop_id) REFERENCES shop (id)');
// item テーブルに staff_id カラムを追加して、インデックスを作成(まだ外部キー制約は追加しない)
$this->addSql('ALTER TABLE item ADD staff_id INT NOT NULL');
$this->addSql('CREATE INDEX IDX_33333333333333 ON item (staff_id)');
// shop テーブルを巡回して、各 shop に対して必要なだけ staff を作成
$shops = $this->connection->query('SELECT id FROM shop')->fetchAll();
foreach ($shops as $shop) {
// shop ごとに作成すべき staff の情報を何かしら一覧などにしておくイメージ.
foreach ($this->getStavesForShop($shop['id']) as $staff) {
$this->addSql('INSERT INTO staff (shop_id, name) VALUES (:shop_id, :name)', [
'shop_id' => $shop['id'],
'name' => $staff['name'],
]);
}
// shop がもともと持っていた item それぞれについてどの staff に持たせるかを何かしらのロジックで決めるイメージ.
$items = $this->connection->query('SELECT i.id FROM item i LEFT JOIN shop s ON i.shop_id = s.id WHERE s.id = :shop_id', [
'shop_id' => $shop['id'],
])->fetchAll();
foreach ($items as $item) {
$this->addSql('UPDATE item SET staff_id = :staff_id WHERE id = :item_id', [
'item_id' => $item['id'],
'staff_id' => $this->getStaffForItem($item['id']),
]);
}
}
// item テーブルの staff_id カラムに staff テーブルへの外部キー制約を追加(データが整理できたのでここで外部キー制約を追加する)
$this->addSql('ALTER TABLE item ADD CONSTRAINT FK_33333333333333 FOREIGN KEY (staff_id) REFERENCES staff (id)');
// item テーブルの shop_id カラムのインデックスと外部キーを削除して、shop_id カラム自体も削除
$this->addSql('DROP INDEX IDX_22222222222222 ON item');
$this->addSql('ALTER TABLE item DROP FOREIGN KEY FK_22222222222222');
$this->addSql('ALTER TABLE item DROP shop_id');
}
private function getStavesForShop($shopId)
{
// スタッフの情報を配列で返す
}
private function getStaffForItem($itemId)
{
// スタッフの情報を返す
}
これで、既存データを適切に付け替えつつスキーマ変更ができるはずです👍
down() でも逆のことをすればいいだけ
同様に、 down() のほうもコードに落とし込むと以下のようなものになるでしょう。
public function donw(Schema $schema) : void
{
// item テーブルに shop_id カラムを追加して、インデックスを作成 (まだ外部キー制約は追加しない)
$this->addSql('ALTER TABLE item ADD shop_id INT NOT NULL');
$this->addSql('CREATE INDEX IDX_22222222222222 ON item (shop_id)');
// item テーブルの shop_id に、対応する shop の id をセット
$this->addSql('UPDATE item a LEFT JOIN staff s ON a.staff_id = s.id SET a.shop_id = s.shop_id');
// item テーブルの shop_id に外部キー制約を追加(ここで追加)
$this->addSql('ALTER TABLE item ADD CONSTRAINT FK_22222222222222 FOREIGN KEY (shop_id) REFERENCES shop (id)');
// item テーブルの staff_id カラムのインデックスと外部キー制約を削除して、staff_id カラム自体も削除
$this->addSql('DROP INDEX IDX_33333333333333 ON item');
$this->addSql('ALTER TABLE item DROP FOREIGN KEY FK_33333333333333');
$this->addSql('ALTER TABLE item DROP staff_id');
// staff テーブルを削除
$this->addSql('DROP TABLE staff');
}
Discussion