🧑‍🤝‍🧑

Laravel 8.42 の目玉機能 One of Many を試す

2021/05/20に公開

はじめに

Laravel 8.42 の目玉機能(たぶん)を試してみます。

これはどういう場面で使えるかと言うと、例えば、管理者がユーザーの一覧を表示しつつ、
各ユーザーが投稿した最新のブログのタイトルも一緒に表示したい時などに使えます。

という事で、今回はその機能を作成して行きます。

ちなみに、users テーブルに1つ項目を作って、各ユーザーの最新のブログのタイトルをそこに保存すれば、ある意味それでも実現できたりしますが、それ自体がちょっと手間だったり、そもそも動的な条件によって、何を最新とするか変わる場合などは(例えば、状況によっては「非公開」の記事も最新記事に含めたり等)、その方法だと難しくなったりします。

そこで今回の One of Many を使ってやります。いわゆる N+1 問題も解決できます。

参考1:Laravel News
参考2:GitHub Add one-of-many relationship (inner join)
参考3:本家ドキュメント Has One Of Many

データの準備

データの準備は、前回の記事と同じになりますが、再度掲載しておきます。

以下のコマンドで、ブログ用のモデル、マイグレーション、ファクトリファイルを作ります。

php artisan make:model Post -mf

マイグレーションは、以下のように編集します。

    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id');
            $table->string('title');
            $table->text('body');
            $table->timestamps();
        });
    }

PostFactoryは、以下のような感じで。

    public function definition()
    {
        return [
            'title' => $this->faker->realText(10),
            'body' => $this->faker->realText(30),
        ];
    }

DatabaseSeederは、以下のように変更します。

use App\Models\Post;
use App\Models\User;

    public function run()
    {
        User::factory(10)->create()->each(function ($user) {
            Post::factory(random_int(0, 3))->create(['user_id' => $user]);
        });
    }

上記のSeederで、各ユーザーに対して、0~3件のブログ投稿を登録しています。
0件の場合は、まだブログを作成していない、という事ですね。

以下でマイグレーションとシーディングを実行します

php artisan migrate --seed

ファイル編集

Models/User に以下を追加します。

    public function latestPost()
    {
        return $this->hasOne(Post::class)->latestOfMany();
    }

メソッド名はもちろん任意ですが、今回は上記のようにしました。

latestOfMany() は、デフォルトでプライマリキー(id)の最大のデータを取得するようになっています(created_at とかではない)。その反対の oldestOfMany() もあります。

web.php を以下のようにします。

<?php

use App\Models\User;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {

    $users = User::with('latestPost')->get();

    return view('welcome', compact('users'));
});

with()を使って Eagerロードしています。

views/welcome.php は以下とします。

    <ul>
        @foreach($users as $user)
        <li>
            {{ $user->name }} /

            {{ optional($user->latestPost)->title }}
        </li>
        @endforeach
    </ul>

これをブラウザでアクセスした時、下記のようなイメージになります。

ちょっと分かりにくいですが、/ の右側が、最新のブログのタイトルです。

空白になっているのは、そのユーザーがまだブログを書いていない為です。
この時、NULLのプロパティにアクセスしようとして、「Trying to get property of non-object」というエラーを回避する為に、optional() ヘルパー関数を間に挟みました。

ちなみに、この一覧表示で発行されるSQLは、以下の2つのみです。

select * from `users`
select * from `posts` inner join (select MAX(id) as id, `posts`.`user_id` from `posts` group by `posts`.`user_id`) as `latestPost` on `latestPost`.`id` = `posts`.`id` and `latestPost`.`user_id` = `posts`.`user_id` where `posts`.`user_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

更に詳しい情報は、本家ドキュメント Has One Of Many などを参照して下さい。

雑感

いつかお世話になる機能かも知れません。

おかしな箇所等ありましたら、コメント下さい。

Discussion