Modelのテーブルのレコードをクエリで更新した際に、Modelのデータ状態で気を付けて欲しいこと(備忘録)
どーも、最近はまた寒くなってきて京都では少し雪が降ってきています。
寒いのは苦手なので、早く春が来て欲しいですね。
今回は以前少しハマってしまった事について忘れないように記事としてまとめたいと思います。
ハマった内容としてはModelのテーブルのレコードをクエリで更新した後に、Modelのプロパティを参照しても更新前のデータを持っていたというものでした。
では詳しく見ていきましょう。
検証環境
- PHP: 8.1.9
- Laravel: 9.44.0
やりたい事
今回はUserモデルのnameをクエリを使用して更新し、モデルからnameを取得してレスポンスで返したいと思います。
準備
ModelはLaravelをインストールした際に作成されていたUserを使用したいと思います。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}
次にContorollerです。
<?php
namespace App\Http\Controllers;
use App\Http\Requests\PutUser;
use App\Models\User;
use App\Services\UserUpdateService;
use Illuminate\Http\JsonResponse;
class UserController extends Controller
{
/**
* @param UserUpdateService $userUpdateService
*/
public function __construct(
readonly private UserUpdateService $userUpdateService
) {
}
/**
* @param User $user
* @param PutUser $request
* @return JsonResponse
*/
public function update(User $user, PutUser $request): JsonResponse
{
$validated = $request->validated();
$this->userUpdateService->update(
$user,
$validated
);
return response()->json(
[
'message' => '名前を' . $user->name . 'に更新しました。'
]
);
}
}
次にサービスです。
<?php
namespace App\Services;
use App\Models\User;
class UserUpdateService
{
/**
* @param User $user
* @param array $payload
* @return void
*/
public function update(User $user, array $payload)
{
User::query()
->where('id', $user->id)
->update(
$payload
);
}
}
次にテストコードです。
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Testing\TestResponse;
use Tests\TestCase;
class TestUserUpdate extends TestCase
{
/**
* @testdox レスポンスが期待通りに返ってくる
*/
public function testUserUpdate(): void
{
$user = User::factory()->create(
[
'name' => 'test_taro'
]
);
$response = $this->putUser(
$user,
[
'name' => 'test_hana'
]
);
$response->assertJson(
[
'message' => '名前をtest_hanaに更新しました。'
]
);
}
/**
* @param User $user
* @param array $payload
* @return TestResponse
*/
private function putUser(User $user, array $payload): TestResponse
{
return $this->put(
$user->id,
$payload
);
}
}
検証
このテストコードを実際に実行してみた結果は以下です。
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'名前をtest_hanaに更新しました。'
+'名前をtest_taroに更新しました。'
更新したはずなのですが、test_taroがレスポンスで返ってきました。
ではControllerに1行だけ追加してみます。
<?php
namespace App\Http\Controllers;
use App\Http\Requests\PutUser;
use App\Models\User;
use App\Services\UserUpdateService;
use Illuminate\Http\JsonResponse;
class UserController extends Controller
{
/**
* @param UserUpdateService $userUpdateService
*/
public function __construct(
readonly private UserUpdateService $userUpdateService
) {
}
/**
* @param User $user
* @param PutUser $request
* @return JsonResponse
*/
public function update(User $user, PutUser $request): JsonResponse
{
$validated = $request->validated();
$this->userUpdateService->update(
$user,
$validated
);
// 以下追加分
$user->refresh();
return response()->json(
[
'message' => '名前を' . $user->name . 'に更新しました。'
]
);
}
}
修正前のコードはBuilderから発行されるクエリによりデータベース上のレコードの値は更新されているのですが、それがUserのModelには反映されていない状態になっています。
ですので、$user->refresh()を行う前は $user->name は test_taro になってしまいます。
それを$user->refresh()の実行により、データベースから取得したデータで既存のインスタンスを最新の状態に再構築してくれます。
つまり $user->name が test_hana になり、最新の状態に再構築された状態になります。
この状態で再テストを行いますと正常にテストが通るようになりました。
Serviceで行っていた更新の処理を$user->update($payload)で更新する処理に変更する修正でも問題ないですが、実際のコードはより複雑だったため、Illuminate\Database\Eloquent\Model::refresh() を使って解決しました。
まとめ
今回は比較的に簡単なコードですので、この様な不具合を修正する事は非常に簡単です。
しかし、関わっていたプロジェクトが大きく不具合の原因を探すのも苦労する様な場面で、今回の題材にしたことが原因で不具合が発生していました。
皆さんもModelのデータが自分の意図している内容と違っている場面に遭遇した時は、今回の記事を思い出していただけると幸いです。 読んでいただきありがとうございました。