Laravel Livewire の public プロパティの注意点(その2)
前書き
前回、Livewire での public プロパティの注意点を書きましたが、その第2弾です。
Livewire を習い始めると、ビールと同じノリで、「とりあえず public プロパティで」となりがちですが、それを見直そうというシリーズです(今回で最後ですが)。
今回の結論としては、「Eloquent Collection を public プロパティに設定するのは、そうするのが妥当でない限り(or 特別な意図が無い限り)辞めておきましょう。」
という話です。
本題
では、何故そうするのは宜しくないのか?私の中では理由は2つ位ありますが、当ブログでは、その内の1つを紹介させていただきます。
理由としては、(本人はそれと気づかずに)SQL が無駄に発行される(又は発行されがち)になるからです。若しくは、それを回避しようとした処理が微妙な処理になったりします。
以下、検証用サンプルとして、ブログ投稿の一覧ですが、プルダウンで「1:公開中」と「2:下書き」を切り換えて表示できるというサンプルです。デフォルトは、「1:公開中」です。(マジックナンバーですが、その辺は気にせずに…)
<?php
namespace App\Http\Livewire;
use App\Models\Post;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Component;
class PostList extends Component
{
public Collection $posts;
public int $status = 1;
public function mount()
{
$this->getPosts();
}
public function updatedStatus()
{
$this->getPosts();
}
public function getPosts()
{
// status は、1 or 2 に限定
abort_unless(in_array($this->status, [1, 2], true), 403);
$this->posts = Post::query()
->where('status', $this->status)
->get();
}
public function render()
{
return view('livewire.post-list');
}
}
<div>
<select wire:model="status">
<option value="1">公開中</option>
<option value="2">下書き</option>
</select>
<ul>
@foreach($posts as $post)
<li wire:key="post-{{ $post->id }}">
{{ $post->title }}
</li>
@endforeach
</ul>
</div>
プルダウンでデフォルトの「1:公開中」から「2:下書き」に切り換えると、updatedStatus() メソッドが呼ばれ、下書きの一覧が表示されるようになります。ただ、その時、SQL 的には以下のような2つの SQL が実行されます。
select * from `posts` where `posts`.`id` in (4, 5, 6, 7, 8, 12, 15, 17, 18, 19)
select * from `posts` where `status` = 2
1つ目は、「1:公開中」の投稿データを取得する為の SQL です。(連番部分は、もちろん登録状況により異なります)
2つ目が、「2:下書き」の投稿データを取得する為の SQL です。
ご覧の通り、下書きの一覧を表示するだけなのに、公開中のデータ分も SQL が走っています。完全に1つ目は、無駄に終わりますね。
もちろん、「2:下書き」から「1:公開中」に切り換えた際も逆の順序で2つの SQL が走り、下書き分の SQL が無駄に終わります。
これは、Livewire のバグではなく、そういう仕様(仕組み)です。
ハイドレーションという過程において、前回のコンポーネントの状態を復元する為のものです。
Livewire は、ステートレスで通信を行う為、この機能が無いと、Livewire 自体が成り立ちません。
ではどうするかと言うと、
- そもそもプロパティ化せず、render 内に書いてしまう。
- プロパティ化する場合は、posts は、private/protected プロパティにして、view() の第2引数で明示的に渡す(明示的に渡さずともテンプレート内で、$this->posts の形で参照できます)。
という感じになります。
この位であれば、render 内に書いても問題無いレベルですね。
class PostList extends Component
{
public int $status = 1;
public function render()
{
abort_unless(in_array($this->status, [1, 2], true), 403);
return view('livewire.post-list', [
'posts' => Post::query()
->where('status', $this->status)
->get()
]);
}
}
これで、無駄な SQL が走るのは回避できます。
ところで
そもそも、ページネーションは、public プロパティに設定するのはサポートされていないですよね。元々、ページネーションは性質的に public プロパティで状態管理するものではない、という事だと思います。
また、上記の public $status も注意が必要ですね。今回はユーザーに値の変更を許すのでいいのですが、そうでも無い時に public $status = 1 とかやっていて、入力チェックも無い場合、前回の記事の通り、ユーザーに値を変更されてしまいます。
Discussion