例えば次のテーブルがあるとします。
create table users
(
user_ulid char(26) not null comment 'クライアント側でID生成を許すためにULID' primary key,
full_name varchar(201) as (concat(last_name, ' ', first_name)) stored comment 'フルネーム',
first_name varchar(100) comment '名前',
last_name varchar(100) comment '苗字',
created_at datetime null comment '作成日時',
updated_at datetime null comment '最終更新日時',
deleted_at datetime null
)
comment 'ユーザー' collate = utf8mb4_unicode_ci;
名前と苗字を持ち、名前と苗字からフルネームを生成するテーブルです。ミドルネームや予期せぬ形式の名前を持つ方もいますので実際のテーブルで名前と苗字で区切ってデータを持つことは珍しいですが、とにかく名前と苗字からフルネームを生成するテーブルです。
このテーブルを Eloquent モデルで表現し、苗字を更新すると例えば次の様になります。
public function update(UserUpdateRequest $request, $userId){
$user = User::findOrFail($userId);
$validated = $request->validated();
$user->last_name = $validated['last_name'];
$user->save();
return \Response::json($user->toArray());
}
このコードは予期せぬ挙動をします。というのもインスタンス上の last_name を更新し、save してもインスタンスの方には full_name が反映されていないのです。このためレスポンスには次の様に変更前の full_name が含まれてしまいます。
full_name: "はっまつ 太郎" first_name: "太郎" last_name: "浜松"
アンチパターンなこの問題への対応が次です。
public function update(UserUpdateRequest $request, $userId){
$user = User::findOrFail($userId);
$validated = $request->validated();
$user->last_name = $validated['last_name'];
// あらかじめ生成カラムの値を作って保存しようとすると次の様にエラーになります。
// SQLSTATE[HY000]: General error: 3105 The value specified for generated column 'full_name' in table 'users' is not allowed.
$user->save();
// 保存した後に生成カラムの値をセットします。
$user->full_name = $user->last_name . ' ' . $user->first_name;
return \Response::json($user->toArray());
}
これでも期待通りに動くのですが、データベースの生成カラムとこのソースコードの二か所において同じ目的で同じ動作をするロジックが組み込まれてしまいます。こうなると保守性が悪くなります。片方を変更した際にもう片方が取り残されたりする危険があり、正しく書き換える際でも二か所を変更する必要があります。
悪くないですが書き方が余分になりやすいパターンが次です。
public function update(UserUpdateRequest $request, $userId){
$user = User::findOrFail($userId);
$validated = $request->validated();
$user->last_name = $validated['last_name'];
$user->save();
// 保存した後に再度ユーザーを見つけます
$user = User::find($userId);
return \Response::json($user->toArray());
}
保存した後にユーザーを再度データベースから取得します。これならばロジックは一か所にまとまったままで保守性も悪くありません。ただし User の取得が少し手間になりやすいです。このコードにおいてはただの find で済んでいますが取得クエリが複雑な場合や水物の場合クエリを違うタイミングで二回走らせるとコードが大きくなりやすいです。
Laravelが用意してくれたベストな方法が次です。
public function update(UserUpdateRequest $request, $userId){
$user = User::findOrFail($userId);
$validated = $request->validated();
$user->last_name = $validated['last_name'];
$user->save();
// refresh メソッドでインスタンスの持つ情報を最新にします
$user->refresh();
return \Response::json($user->toArray());
}
refresh メソッドはデータベースからインスタンスの持つ情報を再取得し、最新のデータを反映するメソッドです。詳しくは次リンクにあります。
Eloquentの準備 10.x (翻訳中)Laravel#モデルのリフレッシュ
もし、元のインスタンスを壊したくないのであれば fresh メソッドが使えます。これは次の様に同じレコードを指す複数のインスタンスを並行して扱いたい場合に便利です。
public function update(UserUpdateRequest $request, $userId){
$user = User::findOrFail($userId);
// fresh メソッドで更新前の User インスタンスを保持
$oldUser = $user->fresh();
// 更新処理
$validated = $request->all();
$user->last_name = $validated['last_name'] ?? '浜松';
$user->save();
// 更新結果を反映
$newUser = $user->fresh();
return \Response::json([
'old' => $oldUser->toArray(),
'new' => $newUser->toArray(),
]);
}