ブログ

Blog

Laravelで一括更新画面を作成する

Laravelで一括更新画面を作成する

はじめに

DBのCRUDを行う画面は一覧表示画面や新規登録画面など色々ある中で、一番ややこしいのが「複数件のデータを一括して更新する画面」ではないかと私は感じております。更新は登録と異なり更新時にその対象レコードが存在しているかのチェックが必要だったり、一件と異なり複数件だとバリデーションエラーをどう出すか、DBへの反映時もinsertやdeleteと異なり一発SQLで済ますには工夫が必要です。そんな一括更新画面をできる限り楽に作る方法を、例として複数台の車情報を一括更新する画面を作りながら紹介できればと思います。

前提

Laravel 11.x
PHP 8.2
MySQL 8.0

参考までにWindows10, VirtualBox6.0, Ubuntu20.04, Docker 24.0を使用しています。

carsテーブル

物理名 id license_no expiration odometer user_id
内容 ID ナンバー 車検満了日 総走行距離 使用者ID
BIGINT VARCHAR(4) DATE INT BIGINT

Car Model


<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Car extends Model
{
    protected function casts(): array
    {
        return [
            'expiration' => 'date',
        ];
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

実装

一覧表示

更新可能な項目を/で表示し、name属性に配列形式を利用します。


<table>
  <thead>
    <tr>
      <th>ナンバー</th>
      <th>車検満了日</th>
      <th>総走行距離</th>
      <th>使用者</th>
    </tr>
  </thead>
  <tbody>
    @foreach($cars as $car)
      <tr>
        <td>
          {{ $car->license_no }}
          <input type="hidden" name="id[]" value="{{ $car->id }}">
        </td>
        <td>
          <input type="date" name="expiration[{{ $car->id }}]" value="{{ $car->expiration->format('Y/m/d') }}">
        </td>
        <td>
          <input type="number" name="odometer[{{ $car->id }}]" value="{{ $car->odometer }}">
        </td>
        <td>
          <select name="user_id[{{ $car->id }}]">
            @foreach($users as $user)
              <option value="{{ $user->id }}" @selected($user->id === $car->user_id)>
                {{ $user->name }}
              </option>
            @endforeach
          </select>
        </td>
      </tr>
    @endforeach
  </tbody>
</table>

バリデーション

FormRequestで配列形式のバリデーションを指定し、withValidatorで一括存在チェックを行います。


class BulkUpdateFormRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'id'             => 'required|array',
            'expiration.*'   => 'required|date',
            'odometer.*'     => 'required|numeric',
            'user_id.*'      => 'required|numeric',
        ];
    }

    public function withValidator($validator)
    {
        $validator->after(function ($validator) {
            if ($validator->errors()->any()) {
                return;
            }
            $exist_user_ids = User::whereIn('id', collect($this->input('user_id'))->unique())
                                  ->pluck('id');
            foreach ($this->input('user_id') as $car_id => $user_id) {
                if (!$exist_user_ids->contains($user_id)) {
                    $validator->errors()->add('user_id.'.$car_id, 'エラーメッセージ');
                }
            }
        });
    }
}

Bladeではold('odometer.'.$car->id, $car->odometer)@error('odometer.'.$car->id)でエラー表示・元入力を保持できます。

更新処理

Controllerでトランザクション内にまとめて更新します。変更のあったモデルのみ保存されます。


public function bulkUpdate(BulkUpdateFormRequest $request): RedirectResponse
{
    \DB::transaction(function () use ($request) {
        $cars = Car::whereIn('id', $request->input('id'))->get();
        foreach ($cars as $car) {
            $car->expiration = $request->input('expiration.'.$car->id);
            $car->odometer   = $request->input('odometer.'.$car->id);
            $car->user_id    = $request->input('user_id.'.$car->id);
            $car->save();
        }
    });
    return redirect(...);
}

大量更新が必要な場合はMySQLのELT/FIELDを使って一括UPDATEも可能ですが、可読性・SQLインジェクション対策・isDirtyチェックなど考慮が必要です。

最後に

配列形式のname属性とCollectionを組み合わせることで、少ないコード量で一括更新機能を実装できます。今後もLaravelの機能を使い倒していきたいですね。

株式会社BTMではエンジニアの採用をしております。ご興味がある方は是非こちらをご覧ください。

  • SNS
  • 投稿日
  • カテゴリー

    BTM Useful