複合一意制約を使ってupsertを実行する

以前upsertメソッドを以下の記事にて紹介させていただきましたが、今回は複合一意制約を使ったupsert実行処理の紹介になります。upsertメソッドってなんだっけ？という方は以下の記事をチェックしていただければと思います。

> Eloquentの upsert メソッドでは、対象となるレコードが存在する場合はupdateを、存在しない場合は新規insertするという挙動を実現

https://qiita.com/mattsershaw/items/b9ac073cbedca1c347ca

## upsertメソッドの復習

upsertメソッドですが、migrationファイルなどで検索対象となるカラムをprimaryキーもしくはuniqueキーで設定しないとupdateされず全てinsertとなってしまいます。
例えば以下のようなテーブルがある場合、student_idがuniqueキーで設定されているため、upsertメソッドではテーブル内で同じstudent_idが存在する場合はそのレコードを更新、存在しなければ新規追加する処理となります。
```php
    public function up()
    {
        Schema::create('student_courses', function (Blueprint $table) {
            $table->id();
            $table->foreignId('student_id')->constrained()->unique()->onDelete('cascade');
            $table->foreignId('course_id')->constrained()->onDelete('cascade');
            $table->date('date_enrolled');
            $table->string('grade')->nullable();
        });
    }
```
upsertはeloquentで以下のようになります。
```php
StudentCourse::upsert($arrayStudentCourses, ['student_id']);
```

しかしこの例の場合、course_idがあるということで授業コースがいくつかあると想定できますが、student_idがuniqueキーのため2つ目の授業コースを登録することができません。

## 複数カラムでuniqueとしたい

そこで考えたのが、student_idとcourse_idを結合させたカラム(student_id_course_id_concat)を作り、それをuniqueキーとすればやりたかったことの実現は可能になります。
```php
    public function up()
    {
        Schema::create('student_courses', function (Blueprint $table) {
            $table->id();
            $table->foreignId('student_id')->constrained()->onDelete('cascade'); // 1の場合
            $table->foreignId('course_id')->constrained()->onDelete('cascade'); // 100の場合
            $table->string('student_id_course_id_concat')->unique(); // 結合させて1100とする
            $table->date('date_enrolled');
            $table->string('grade')->nullable();
        });
    }
```
しかし、カラムのデータを足しただけのカラムを増やすのは効率的とは言えませんし、格納するタイミングや、エラーが起きた場合の処理などリスクがあるためできる限り避けたいです。そんな時に使えるのが複合一意制約です。
## 複数カラムでuniqueとする書き方
```php
    Schema::create('student_courses', function (Blueprint $table) {
            $table->id();
            $table->foreignId('student_id')->constrained()->onDelete('cascade');
            $table->foreignId('course_id')->constrained()->onDelete('cascade');
            $table->date('date_enrolled');
            $table->string('grade')->nullable();
            $table->unique(['student_id', 'course_id']); // 複合一意制約の書き方
    });
```
この書き方ですとuniqueキーを複数カラムに跨がせる形で指定することができ、student_idとcourse_idを合わせて配列に入れています。student_idとcourse_idが組み合わせでuniqueとなっているため、それぞれ同じレコードが存在する場合は更新され、どちらか片方だけ同じレコードが存在する場合はuniqueではないため新規追加されます。つまり

```php
// テーブル内にstudentId = 2, courseId = 2のレコードが存在している場合
studentId = 1, courseId = 1 // insert
studentId = 2, courseId = 1 // insert
studentId = 2, courseId = 2 // update
```

といった判定となります。よって同じstudent_idでも異なるcourseが登録できますし、異なるstudent_idで同じcourseが登録できます。両idどちらもテーブルに存在しない場合は新規登録されますし、どちらも存在する場合は更新されます。
## 複合カラムに名前を付けることもできる
以下のように組み合わせに名前をつけることもできます。
```php
// student_id_course_id_concatで名前付け
$table->unique(['student_id', 'course_id'], 'student_id_course_id_concat');
```
upsertのコードは以下のようになります。気を付けることは第二引数には組み合わせの名前ではなく、カラム名をそれぞれ指定しないとupsertとならないことです。
```php
// ok!
StudentCourse::upsert($arrayStudentCourses, ['student_id', 'course_id']);
// ng
StudentCourse::upsert($arrayStudentCourses, ['student_id_course_id_concat']);
```
以上になります！使う機会があった場合はこのようにシンプルに書いていきましょう！

## 参考リンク
- [Laravel 9.x マイグレーション](https://readouble.com/laravel/9.x/ja/migrations.html)
- [Laravelのマイグレーションで複数カラムにunique制約を設定する方法](https://loop-never-ends.com/laravel-unique-columns/)