WEBOPIXEL

Laravel8でCMSを作るチュートリアル(4) ユーザーと投稿の関連付け(多対一:HasMany)[2020版]

Laravelロゴ

Posted: 2020.12.29 / Category: PHP / Tag: 

Laravel8を使用して、コーポレートサイトでよくありそうなのお知らせを管理するCMSを作るチュートリアルの4回目です。
今回はUserとPostの関連付け(リレーション)をしてどのユーザーが投稿したのかをわかるようにしてみましょう。

Sponsored Link

作る機能

  1. 投稿一覧&詳細ページ
  2. 管理画面へのログイン機能
  3. 投稿管理(CRUD)機能
  4. ユーザーと投稿の関連付け(多対一:HasMany)
  5. 投稿のタグ分け(多対多:ManyToMany)

テーブルの編集

postsテーブルにuser_idというカラムを追加します。

database/migrations/XXXX_XX_XX_XXXXXX_create_posts_table.php

Schema::create('posts', function (Blueprint $table) {
	$table->id();
	$table->string('title');
	$table->text('body')->nullable();
	$table->boolean('is_public')->default(true)->comment('公開・非公開');
	$table->dateTime('published_at')->default(DB::raw('CURRENT_TIMESTAMP'))->comment('公開日');
	$table->foreignId('user_id')->constrained();
	$table->timestamps();
});

追加したのは8行目です。
$table->id()で設定している場合はforeignIdを使用します。Laravelの規約通りなら、constrainedに引数は必要ありませんが、関連するテーブルがusersではない場合引数に指定します。

ファクトリーファイルにもuser_idを追加しましょう。

database/factories/PostFactory.php

return [
	'title' => $this->faker->realText(rand(20,50)),
	'body' => $this->faker->realText(rand(100,200)),
	'is_public' => $this->faker->boolean(90),
	'published_at' => $random_date,
	'user_id' => $this->faker->numberBetween(1,3),
	'created_at' => $random_date,
	'updated_at' => $random_date
];

次のコマンドでDBを再構築します。

$ artisan migrate:refresh --seed

モデルの設定

リレーションの設定はモデルで行います。
user_idがあるPostモデルではbelongsToを設定します。

app/Models/Post.php

class Post extends Model
{
	// ...

	public function user()
    {
        return $this->belongsTo(User::class);
    }
}

カラム名がリレーション先の「モデル_id」なら上記の通りで問題ありませんが、もしそれ以外のカラム名を関連付ける場合はbelongsToの第二引数に指定します。

今回は表示しませんが、Userモデルで設定する場合は複数のPostを持つのでhasManyになります。

app/Models/User.php

class User extends Model
{
	// ...

	public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

ビューの設定

PostからUserにアクセスできるようになりました。
ビューで表示してみましょう。
ユーザー名を表示したい場合は$post->user->nameのように指定します。

resources/views/back/posts/index.blade.php

<thead>
	<tr>
		// ...

		<th scope="col">編集者</th>
	</tr>
</thead>
<tbody>
@foreach($posts as $post)
	<tr>
		// ...
		
		<td>{{ $post->user->name }}</td>
	</tr>
@endforeach

編集画面にも表示してみましょう。

resources/views/back/posts/edit.blade.php

<table class="table">
	<tr>
		<th>編集者</th>
		<td>{{ $post->user->name }}</td>
	</tr>
	<tr>
		<th>登録日時</th>
		<td>{{ $post->created_at }}</td>
	</tr>
	<tr>
		<th>編集日時</th>
		<td>{{ $post->updated_at }}</td>
	</tr>
</table>

N+1問題

モデルを関連付けすることは簡単にできることがわかったと思います。
ただこのままだと問題があります。
一覧ページのクエリを確認するとレコードの数だけクエリが発行されているのがわかると思います。

クエリは必要になったときに発行される仕組みなので、このような問題が起こります。
この問題を解消するにはEager Loadingという機能を使います。
コントローラーを次のように編集してください。

app/Http/Controllers/Back/PostController.php

$posts = Post::with('user')->latest('id')->paginate(20);

再度一覧ページを表示するとクエリの数が少なくなっているのがわかると思います。

savingイベントでuser_idを保存する

表示することはできたので、今度は保存時にログインユーザーのidをuser_idに保存するという処理をします。
この処理はコントローラーに書いてもいいのですが、Laravelにはさまざまなイベントの間に処理を挟むことができます。
今回はデータの登録と更新時に同じ処理を行いたいのでsavingというイベントに処理を追加します。
このイベントの書き方もいろいろとあるのですが、今回は単純な処理なのでモデルに直接記述します。

app/Models/Post.php

protected static function boot()
{
	parent::boot();

	// 保存時user_idをログインユーザーに設定
	self::saving(function($post) {
		$post->user_id = \Auth::id();
	});
}

データの更新をしてuser_idにログインユーザーのidが登録されるか確認してみてください。

シーダーの修正

先ほどのsavingイベントでログイン情報を読み込むような処理をすると、シーダーのコマンド実行時にもイベントが実行されてしまいエラーになります。(コマンド実行時はログイン情報が取れない為)
Event::fakeForでイベントを実行しないように修正しましょう。

database/seeders/PostSeeder.php

  \Event::fakeFor(function () {
	Post::factory()->count(50)->create();
});

これで多対一(HasMany)で投稿(Post)とユーザー(User)の関連付けをすることができました。
次回は多対多(ManyToMany)のリレーションでタギング機能を作っていきます。

ソースコードはGitHubに置いてます。

LaravelMiniCMS2020

COMMENTS

wan 2021-01-14 01:13 

ありがとうございます。1点質問です。

>モデルイベントでログイン情報を読み込むような処理をすると、シーダー実行にもイベントが実行されてしまいうまくいきません。

⇨こちらよくわかりませんでした。モデルイベントでログイン情報を読み込むとは、
追加したbootのことでしょうか。

また、以下をファクトリーに追加して実行したところエラーが出ました。
>$this->faker->numberBetween(1,3),
前の章でユーザーを2名追加した状態で本章に取り組んでいるため、
(1,2)とした方が親切かなと思いました。

webOpixel 2021-01-20 21:29 

コメントありがとうございます。

> こちらよくわかりませんでした。モデルイベントでログイン情報を読み込むとは、
追加したbootのことでしょうか。

わかり難くすみません。
おっしゃる通りbootのことのことです。
(この部分修正しました。)

> 前の章でユーザーを2名追加した状態で本章に取り組んでいるため、
(1,2)とした方が親切かなと思いました。

途中でユーザーを追加していたことを失念してました。
前章の方を修正しました。

LEAVE A REPLY

コードを書く場合は<pre>で囲んでください。