In my head.

感想と考え事とメモ

Laravelで外部キー制約があるテーブルデータを削除したい

結論

2つ方法があります。

前提

テーブル構成

例えば下記のようなテーブルがあったとします。

  • teamsテーブルがあり、それに紐づく形でmembersテーブルがある(親:teams 子:members)
  • membersteam_idteamsidを参照している外部キー
  • fk__team_id__members_idは外部キーの制約名

f:id:smspring0426:20190323195117p:plain

したいこと

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)

なんか外部キーが問題で削除できないと。えぇ.....
(この時は外部キー設定されてたことと、その制約内容知らなかった)

原因

実はこのテーブル、こうなっていました。

f:id:smspring0426:20190323195950p:plain

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。

f:id:smspring0426:20190323200458p:plain

ON DELETE RESTRICTON DELETE CASCADEに変えることで、親テーブルの削除に合わせて子テーブルも削除されます。

では、具体的にどうするか。順序としては下記の通りです。

  1. 既存の外部キー制約削除のマイグレーションを生成
  2. 新しい外部キー制約追加のマイグレーションを生成
  3. マイグレーション実行

1. 既存の外部キー制約削除のマイグレーションを生成する

下記のコマンドを流します。
$ php artisan make:migration 生成するファイル名

そうすると、日付_生成するファイル名.phpdatabase/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.phpdatabase/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です!(古くてすみません。)まあ今のバージョンでもきっと同じエラー起こると思うから・・・。

参考