Laravelで外部キー制約があるテーブルデータを削除したい
結論
2つ方法があります。
前提
テーブル構成
例えば下記のようなテーブルがあったとします。
teams
テーブルがあり、それに紐づく形でmembers
テーブルがある(親:teams
子:members
)members
のteam_id
はteams
のid
を参照している外部キーfk__team_id__members_id
は外部キーの制約名
したいこと
teams
テーブルのデータを削除する。とにかく削除ができればいい。
書いたこと
TeamsController.php
...略 public function deleteTeam($id) { $this->teams->deleteTeam($id); }
TeamsService.php
...略 public function deleteTeam($id){ Teams::where('id', '=', $id)->delete(); }
これでいけるだろって思ってました。
実行結果
エラー
Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails (`DB名`.`teams`, CONSTRAINT `外部キー名` FOREIGN KEY (`team_id`) REFERENCES `teams` (`id`)) (SQL: delete from `DB名`.`` where `id` = xxx)
なんか外部キーが問題で削除できないと。えぇ.....
(この時は外部キー設定されてたことと、その制約内容知らなかった)
原因
実はこのテーブル、こうなっていました。
fk__team_id__members_id
の制約内容がON DELETE RESTRICT
テーブルの参照整合性を保つために、親テーブルの削除ができないようになっていたんですね。
補足
RESTRICT
は下記の場合に設定されます。
ON DELETE RESTRICT
と明示的にRESTRICT
を設定した時ON DELETE NO ACTION
と明示的にNO ACTION
を設定した時ON DELETE
を明示的に何も設定しなかった時
設定した覚えがないのにRESTRICT
になっているのは、そういう仕様だからみたいですね。
対処法
マイグレーションでDBスキーマを変更
方法
問題のfk__team_id__members_id
の設定を変えればOK。
ON DELETE RESTRICT
をON DELETE CASCADE
に変えることで、親テーブルの削除に合わせて子テーブルも削除されます。
では、具体的にどうするか。順序としては下記の通りです。
1. 既存の外部キー制約削除のマイグレーションを生成する
下記のコマンドを流します。
$ php artisan make:migration 生成するファイル名
そうすると、日付_生成するファイル名.php
がdatabase/migrations
に作成されます。
生成された日付_生成するファイル名.php
を下記のように編集します。
...略 class 生成するファイル名(←すでに記述されている) extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('members', function (Blueprint $table) { $table->dropForeign('fk__team_id__members_id'); }); } }
これで既存の外部キーfk__team_id__members_id
を削除します。
2. 新しい外部キー制約追加のマイグレーションを生成
もう一度下記のコマンドを流します。
$ php artisan make:migration 生成するファイル名2
そうすると、1.のように日付_生成するファイル名2.php
がdatabase/migrations
に作成されます。
生成された日付_生成するファイル名2.php
を下記のように編集します。
...略
class 生成するファイル名2 extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::statement(
'ALTER TABLE members ADD CONSTRAINT 新しい外部キー名
FOREIGN KEY (team_id) REFERENCES teams (id)
ON DELETE CASCADE ON APDATE CASCADE'
);
}
}
これで新しい外部キーを作り、その外部キーに対して削除時の挙動を明示します。
補足
さりげなく書いているON APDATE
は更新時の挙動を指定する句です。
ON DELETE CASCADE
と同じように、ON UPDATE CASCADE
は親テーブルの更新に合わせて子テーブルも更新されます。
3. マイグレーション実行
下記のコマンドでマイグレーションを実行します。
$ php artisan migrate
これでOKです。
マイグレーションは生成されたファイル順で実行されます。
なので、既存の外部キー削除→新しい外部キー作成の順で生成しましょう。
順序逆だとエラーになると思います。(試してないですが)
メリット/デメリット
2つ方法を紹介しているので、各方法のメリット・デメリットを一応書きます。
あくまで私が感じたことですが・・・。
○ 削除実行部分をスマートに記述できる
× スキーマ変更で何か不具合が起きる可能性ある
親テーブルデータ削除前に子テーブルデータを削除
方法
親テーブルに紐づいている子テーブルのデータを先に削除してから親テーブルを削除するだけです。
こんな感じのメソッド作って、
MembersService.php
...略
public function deleteMemberByTeamId($teamId)
{
Members::where('team_id', '=', $teamId)->delete();
}
teams
削除の記述前に追加。
TeamsController.php
...略 public function deleteTeam($id) { $this->members->deleteMemberByTeamId($id); $this->teams->deleteTeam($id); }
メリット/デメリット
これも主観です。
○ スキーマ変更する必要ない
× 親テーブルに紐づく子テーブルが複数ある場合、全テーブル書く必要があるので冗長
× なんかかっこ悪い
余談
- 2つ紹介しましたが、とりあえず上手くいった方法です。これでもできたけどなんか冗長な気もするから、多分もっとスマートで正しい書き方あるんだろうな。Laravelかじりたてなので許してください。むしろ正しい方法あったらぜひ教えてください。
- controllerとserviceで定義するメソッド名って同じでいいのかな。よくわかんない。
- なんとなんと、laravel5.1です!(古くてすみません。)まあ今のバージョンでもきっと同じエラー起こると思うから・・・。