🎻
[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