【Laravel8】Eloquentのupsertメソッドで複数レコードのupdateとinsertをよしなに行う

業務で、複数のデータについて、
・既存のデータがDBにあれば更新
・既存のデータがDBになければレコード追加
を行いたいという場面がありました。

そこで`update`か`insert`のどちらを行うかよしなに判断して`Bulk insert`を行ってくれるという、すごく便利な`Eloquent`の`upsert`メソッドの存在を知ったので、備忘録を書いておきます。

## upsertメソッドとは

Laravel8から実装された機能で、**1つのクエリで複数のアップサート**を実行できるメソッドです。

**※アップサート（`upsert`）とは**
update + insertのことで、データの更新(update)とデータの挿入(insert)の両方の機能を併せ持つ。
対象のレコードがあればそのレコードを更新し、レコードがなければレコードの新規追加をするという場合で使う。

https://readouble.com/laravel/8.x/ja/eloquent.html#:~:text=%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E5%A0%B4%E5%90%88%E3%80%81-,upsert%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89,-%E3%81%AFcreated_at%E3%81%A8

Laravel7までにも`updateOrCreate`メソッドや`firstOrCreate`メソッドという一つのレコードをアップサートするメソッドはありましたが、`upsert`メソッドにより複数レコードを`Bulk insert`できるようになったようです。

## 使い方

`usersテーブル`に複数のデータを`upsert`する例を書いてみます。

前提として、DBの`usersテーブル`には
**・id：1、name：taro**
**・id：2、name：hanako**
の2つのレコードが既に存在するとします。

これに対して以下の通り`upsert`メソッドを実行します。

```php
User::upsert([
  ['id' => 1, 'name' => 'taro', 'age' => 20],      // update
  ['id' => 2, 'name' => 'jiro', 'age' => 22],      // update
  ['id' => null, 'name' => 'taro', 'age' => 24]    // insert
], ['id'], ['name', 'age']);
```

結果は、第2引数に渡した`id`が一致するレコードがあった1つ目と2つ目は`update`となり、3つ目は`insert`になります。

### 構文

`upsert`メソッドは3つの引数を持ちます。

- 第1引数
`update`または`insert`を行いたい値を配列で渡す。

- 第2引数
レコードを一意に識別するカラム（プライマリーキーかユニークキー）を配列で渡す。

- 第3引数（省略可）
`update`が行われる際に、更新する必要があるカラムを配列で渡す。
省略すると全カラム更新される。

## 注意点
色んな引数を渡して動作確認をしてみて、気を付けるべきと思ったことを挙げておきます。

#### 第2引数に指定して意味があるのはプライマリーキーもしくはユニークキーのみ
上記の例で第2引数に`['id', 'name']`を渡したとします。

すると2つ目のデータ`['id' => 2, 'name' => 'jiro', 'age' => 22]`については、既存のDBのデータは`id：2、name：hanako`であり`name`が一致しないので`insert`になる…と思いきや、`update`が実行されました。

Readoubleにも
> 2番目の引数は、関連付けられたテーブル内のレコードを一意に識別するカラムをリストします。

と書いてあるので、第2引数に渡せるのは一意に識別できるカラムに限るようです。

#### 第2引数には第1引数に渡しているカラムを渡す
先ほどの例で、以下のように第1引数には`id`を渡さず、第2引数に`id`を渡したとします。

```php
User::upsert([
  ['name' => 'taro', 'age' => 20],
  ['name' => 'jiro', 'age' => 22],
  ['name' => 'taro', 'age' => 24] 
], ['id'], ['name', 'age']);
```

この場合は`id`が一致するカラムがあったとしても全て`insert`になりました。
なので第2引数に指定するカラムは、必ず第1引数にも渡しておかないといけません。

## created_atとupdated_atは自動で設定される
モデルのタイムスタンプが有効になっている場合、`created_at`と`updated_at`のカラムは自動的に値を入れてくれます。

第1引数には`created_at`と`updated_at`について何も書かなくても、

-  `update` ：`updated_at`のみ更新
-  `insert` ：`created_at`と`updated_at`は処理をしたときの時刻

としてくれます。

普通の `insert`メソッドを使って`bulk insert`する場合は、`created_at`と`updated_at`は自動入力されず明示的に含める必要があるので、これは便利だなと思いました。

## 【おまけ】実装例
実際のコードの書き方の例も残しておきたいと思います。

これは`upsert`だけでなく通常の`bulk insert`にも言えることですが、DBを更新するための配列を`foreach`や`for`文を回して作る際は、以下のように書くとスッキリします。

```php
// POSTされたデータ配列 $postedAnswers をupsertする場合

$upsertAnswers = [];

foreach ($postedAnswers as $answer) {
  $upsertAnswers [] = [
    'id' => $answer['id'] ?? null,
    'body' => $answer['body'],
    'type' => $answer['type']
  ];
}

// idが一致するレコードがあれば更新、無ければレコード作成する
Answer::upsert($upsertSelections, ['id']);
```

私ははじめ`array_push`を使って配列に要素を追加していっていましたが、先輩にレビューいただいて上記の書き方を知ったのでこれもメモでした！



## 参考記事

https://readouble.com/laravel/8.x/ja/eloquent.html#:~:text=%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E5%A0%B4%E5%90%88%E3%80%81-,upsert%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89,-%E3%81%AFcreated_at%E3%81%A8

https://qiita.com/uchiii1/items/bb8043719f15e40db35b#eloquent%E3%81%AEupsert%E3%81%A3%E3%81%A6

https://zenn.dev/y640/articles/9f66d6abfeecf6#%E4%BD%BF%E3%81%84%E6%96%B9