Laravelの外部キー制約でCASCADEを設定するときに使える新しいメソッド

外部キー制約を試していたら、Laravel の新しいメソッドを見つけました。

# 要約

- 外部キー制約の `ON DELETE` に `CASCADE` を設定するとき
  - これまでは `onDelete('cascade')` と書いてきた
  - だけど、最近は `cascadeOnDelete()` とも書けるよ

- 外部キー制約の `ON UPDATE` に `CASCADE` を設定するとき
  - これまでは `onUpdate('cascade')` と書いてきた
  - だけど、Laravel 7.20.0 から `cascadeOnUpdate()` とも書けるようになったよ（[CHANGELOG](https://github.com/laravel/framework/blob/7.x/CHANGELOG-7.x.md#v7200-2020-07-14)）

もうこれ以上説明する必要は無さそうですが、それだとボリュームが寂しいので、だらだらと書いていきます。

# 環境

- Laravel 7.20.0
- PHP 7.4.7
- MySQL 5.7.29

# 前提

以下の2テーブルが存在するとします。

- `memos`
- `keywords`
  - 外部キー制約により、 `memo_id` は `memos.id` と紐付いています

<img width="489" alt="ER図.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141299/fd879464-6184-4ae3-f042-cfa548be4cf4.png">


# そもそもCASCADEとは

良い記事が多数あるので、ここでは MySQL の [公式マニュアル](https://dev.mysql.com/doc/refman/5.6/ja/create-table-foreign-keys.html) を引用しておきます。

> CASCADE: 親テーブルの行を削除または更新し、子テーブル内の一致する行を自動的に削除または更新します。ON DELETE CASCADE と ON UPDATE CASCADE の両方がサポートされています。

~~ふむ、これだけじゃ、よくわからん~~

# マイグレーションファイル

`ON DELETE` と `ON UPDATE` のそれぞれに `CASCADE` を設定します。

`memos` のマイグレーションファイルは、通常どおりです。

```php
class CreateMemosTable extends Migration
{
    public function up()
    {
        Schema::create('memos', function (Blueprint $table) {
            $table->id();
            $table->string('title')->nullable(false);
            $table->text('body')->nullable(false);
            $table->boolean('is_published')->default(false);
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('memos');
    }
}
```

`keywords` のマイグレーションファイルです。

`onDelete('cascade')` や `onUpdate('cascade')` と [記述](https://readouble.com/laravel/7.x/ja/migrations.html#foreign-key-constraints) するかわりに、今回は `cascadeOnDelete()` と `cascadeOnUpdate()` を記述します。


```php
class CreateKeywordsTable extends Migration
{
    public function up()
    {
        Schema::create('keywords', function (Blueprint $table) {
            $table->id();

            $table->foreignId('memo_id')
                ->constrained()
                ->cascadeOnDelete()  // ON DELETE で CASCADE
                ->cascadeOnUpdate(); // ON UPDATE で CASCADE

            $table->string('name');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('keywords');
    }
}
```

# 実際にやってみた

これだけだと味気ないので、実際にやってみました。

## ON DELETE
### 削除前

外部キー制約により、 `keywords.memo_id = 1` のレコードは、 `memos.id = 1` のレコードに紐付いています。

`memos`

|id|title|body|is_published|created_at|updated_at|
|---|-----|----|------------|----------|----------|
|1|タイトル1|本文1|1|2020-07-18 17:42:21.0|2020-07-18 17:42:21.0|
|2|タイトル2|本文2|1|2020-07-18 17:42:21.0|2020-07-18 17:42:21.0|

`keywords`

|id|memo_id|name|created_at|updated_at|
|---|-------|----|----------|----------|
|1|1|キーワード1|2020-07-18 17:42:22.0|2020-07-18 17:42:22.0|
|2|2|キーワード2|2020-07-18 17:42:22.0|2020-07-18 17:42:22.0|

### 削除実行

`memos.id = 1` のレコードを削除します。

```
App\Memo::find(1)->delete();
```

### 削除後

`memos.id = 1` のレコードが削除されました。

`memos`

|id|title|body|is_published|created_at|updated_at|
|---|-----|----|------------|----------|----------|
|2|タイトル2|本文2|1|2020-07-18 17:42:21.0|2020-07-18 17:42:21.0|

また、外部キー制約により `keywords.memos_id = 1` のレコードが削除されました。

`keywords`

|id|memo_id|name|created_at|updated_at|
|---|-------|----|----------|----------|
|2|2|キーワード2|2020-07-18 17:42:22.0|2020-07-18 17:42:22.0|


## ON UPDATE
### 更新前

外部キー制約により、 `keywords.memo_id = 1` のレコードは、 `memos.id = 1` のレコードに紐付いています。

`memos`

|id|title|body|is_published|created_at|updated_at|
|---|-----|----|------------|----------|----------|
|1|タイトル1|本文1|1|2020-07-18 17:39:26.0|2020-07-18 17:39:26.0|
|2|タイトル2|本文2|1|2020-07-18 17:39:26.0|2020-07-18 17:39:26.0|

`keywords`

|id|memo_id|name|created_at|updated_at|
|---|-------|----|----------|----------|
|1|1|キーワード1|2020-07-18 17:39:27.0|2020-07-18 17:39:27.0|
|2|2|キーワード2|2020-07-18 17:39:27.0|2020-07-18 17:39:27.0|

### 更新処理

`memos.id = 1` のレコードについて、 `id` を `1` から `3` へ更新します。

```
$model = App\Memo::find(1);
$model->id = 3;
$model->save();
```

### 更新後

`memos.id` が `1` から `3` へ更新されました。

`memos`

|id|title|body|is_published|created_at|updated_at|
|---|-----|----|------------|----------|----------|
|2|タイトル2|本文2|1|2020-07-18 17:39:26.0|2020-07-18 17:39:26.0|
|3|タイトル1|本文1|1|2020-07-18 17:39:26.0|2020-07-18 17:40:37.0|

また、外部キー制約により `keywords.memo_id` も `1` から `3` へ更新されました。

`keywords`

|id|memo_id|name|created_at|updated_at|
|---|-------|----|----------|----------|
|1|3|キーワード1|2020-07-18 17:39:27.0|2020-07-18 17:39:27.0|
|2|2|キーワード2|2020-07-18 17:39:27.0|2020-07-18 17:39:27.0|

# 補足

- `cascadeOnDelete()`
  - Laravel 7.0.0 以降で使用できる
    - 2019/11/09 の [コミット](https://github.com/laravel/framework/commit/ec966c21c4062c8ded5d8add4db91cab61a591dc#diff-316160024910a0fae3a16198eac04e67) で追加された

- `cascadeOnUpdate()`
  - Laravel 7.20.0 以降で使用できる
    - 2020/07/13 の [プルリクエスト](https://github.com/laravel/framework/pull/33522) で追加された

# 感想

今までどおり `onDelete('cascade')` と記述しても問題ないと思いますが、 `cascadeOnDelete()` のほうが若干タイプ数も少ないですし[^1]、IDE がサジェストしてくれるので `'cascade'` のスペルを心配しなくて済みます。

[^1]: 前者は19文字なのに対して、後者は15文字。**4文字** 少ない。

そもそも取り上げようと思ったきっかけは、外部キー制約を使ってみよう[^2] と思って書いていたら、IDE のサジェスト機能に偶然教えられたからでした。

そこから `cascadeOnDelete()` が追加された経緯を調べていたら、 **4日前** に出た最新バージョンで `cascadeOnUpdate()` が追加されていたことに気づきました。

いわゆる **偶然の産物** ってやつですね。

[^2]: ちなみに、私の **案件ガチャ運** が悪いのかもしれませんが、外部キー制約を使っている案件を一度も担当したことがありません。[『SQL アンチパターン』](https://www.amazon.co.jp/SQL%E3%82%A2%E3%83%B3%E3%83%81%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3-Bill-Karwin/dp/4873115892) とか読むと、「外部キー制約を使わないなんて頭おかしい」と言わんばかりに推奨されているのは知っていますけど。一度提案してみたら、「データを投入したり削除するときに考えないといけないことが増えるからイヤだ」と一蹴されましたし（それも分からなくもない）。たまに「テストコード書くのは常識となり...」みたいなことを言われる方を見かけますが、「どこの世界線の話なのかな」とか思っちゃいますね。そうやって、自分の置かれた環境が「常識」だと思うのはお止めになったほうが良いと思う...（というか、「脚注にポエムを書くな」という話ですが）