⛱️

Laravel Livewire の public プロパティの注意点(その2)

2022/03/12に公開

前書き

前回、Livewire での public プロパティの注意点を書きましたが、その第2弾です。
https://zenn.dev/nshiro/articles/157c198cc46353

Livewire を習い始めると、ビールと同じノリで、「とりあえず public プロパティで」となりがちですが、それを見直そうというシリーズです(今回で最後ですが)。

今回の結論としては、「Eloquent Collection を public プロパティに設定するのは、そうするのが妥当でない限り(or 特別な意図が無い限り)辞めておきましょう。」
という話です。

本題

では、何故そうするのは宜しくないのか?私の中では理由は2つ位ありますが、当ブログでは、その内の1つを紹介させていただきます。

理由としては、(本人はそれと気づかずに)SQL が無駄に発行される(又は発行されがち)になるからです。若しくは、それを回避しようとした処理が微妙な処理になったりします。

以下、検証用サンプルとして、ブログ投稿の一覧ですが、プルダウンで「1:公開中」と「2:下書き」を切り換えて表示できるというサンプルです。デフォルトは、「1:公開中」です。(マジックナンバーですが、その辺は気にせずに…)

Livewire/PostList.php
<?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');
    }
}
post-list.blade.php
<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