変更履歴
2021/09/12 validationのリダイレクト処理を編集しました。
こんにちは!
前回はようやく記事を投稿して表示することができましたね!
今回は記事に対してタグを設定していきます!
そのために、多対多のリレーションを設定していきます!
前回の1対多のリレーションに比べると少し複雑になりますが、多対多のリレーションを覚えると、例えばLINEのグループ機能など、複雑なアプリケーションを作成することができるようになります!
気合い入れてやっていきましょう!🔥🕺🔥
- チュートリアル説明
- プログラミングの準備(エディタ、dockerの用意、環境構築)
- 認証機能を追加してログイン機能を実装する
- ブログのタイトルと記事を登録する
- ブログの記事へタグを登録する(今ここ)
- ブログの一覧表示、編集機能の実装
- ブログの検索機能の実装
- ユーザーへの通知機能の実装
早速、前回紹介したsqldesignerを使って、多対多リレーションのイメージを固めていきます!
それではsqldesignerを開いてください。
で、現在のテーブルをインポートしてテーブル一覧を表示させてください。
はい、今こんな感じですね。
では、早速ブログに対して多対多の関係を持ったタグテーブルを作成していきます!
多対多リレーションでのテーブル設計
はい、まずですが、tagsテーブルを作成します。
まず、タグの名前を保存するフィールドを作成しましょう。
で、タグテーブルについては以上になります。
∑(゚Д゚)!?
と思った方!いい反応です!
これは、なぜかというと、article_idが保存できないからです。
つまり、
こんな感じでデータがあったとします。
article1が、tag1とtag2に紐づいているとします。
こんな感じですね。
この場合、前回の1対多のリレーションみたいに、tagsにarticle_idフィールドがあればarticleに紐付けられますね。
では次に、article2がtag1とtag3に紐づいているとします。
こんな感じになります。
ここでイレギュラーが発生しました。よくみてください。tag1にはarticle1とarticle2が紐づいています。
では、tagsテーブルにarticle_id_1フィールドとarticle_id_2フィールドを作成すれば、今現在は大丈夫でしょう。
ですが、タグを登録するたびにarticle_id_xフィールドをどんどん追加しないといけませんね。
で、逆もしかりで、
この場合、tag1はarticle1とarticle2に紐づいていて、tag2はarticle2とarticle3、tag3はarticle1とarticle2とarticle3に紐づいています。
もう意味わかんなくなってきましたよね。tag1,2,3が紐づいているarticle2はtag_id_1,tag_id_2,tag_id_3というフィールドが存在するのでしょうか?
ここまでみてわかったと思いますが、多対多リレーションの場合、テーブルが一つだとそもそも無理なわけです。
ここで満を持して登場するのが、中間テーブルとなります!!
中間テーブルとは、上の図で表現すると、
この真ん中のやつです。
見てみると、articleのidと、tagのidを持ってますね。
こうすれば、articlesにもtagsにも、リレーション相手のidを保持する必要がないですよね。
さらに、この中間テーブルがあれば、articleがどんなに増えようが、tagが増えようが、問題ないということですね!
さぁ、イメージできまいが、理解できまいが、やっていきます!!
それでは、中間テーブルを作成していきます。
はい、中間テーブルの名前はarticle_tagとします。
この名前は、laravelのルールに則った名前で、多対多リレーションで使用するテーブル名は、リレーションで紐づけるモデル名をアルファベット順で並べたものになります。
では、次にフィールドを作っていきます。
はい、こんな感じですね。tag_idも同じように作成しましょう。
はい、今こんな感じだと思いますので、あとはリレーションを設定しましょう。
はい、完成しました!
よし!それでは実際にテーブルを作成していきます!
tagsテーブルと中間テーブルを作成する
ターミナルで、appコンテナの中に入ってください。
php artisan make:migration create_tags_table
と入力してtagsテーブルを作成するmigrationファイルを作成しましょう。
で、出来上がったファイルを以下のように編集します。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTagsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('tags', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');//<- 追加
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('tags');
}
}
tagsテーブルはnameフィールドだけですよね。
で、
php artisan migrate
でテーブルを作成してください。
こんな感じで作成できたかと思います。
そしたら、次に中間テーブルを作成します。
php artisan make:migration create_article_tag_table
で、作成した中間テーブルのファイルを以下のように編集してください。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateArticleTagTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('article_tag', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('article_id');//<-追加
$table->unsignedBigInteger('tag_id');//<-追加
$table->timestamps();
//↓諸々追加
$table->foreign('article_id')
->references('id')
->on('articles')
->onDelete('cascade');
$table->foreign('tag_id')
->references('id')
->on('tags')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('article_tag');
}
}
はい、こんな感じですね。
ちゃんと外部キー制約も設定して、検索を早くしましょう。
そしたらファイルを実行してテーブルを作成してください。
はい、OKですね!
そしたら、tagのmodelを作成しましょう!
php artisan make:model Tag
で作成して、中身を以下のように編集
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
protected $fillable = [
'name',
];
}
これもarticleモデルを作成したときと同じですね。
そしたら次に、articleからtagを参照できるようにリレーションを設定します。
Article.phpを開いて下さい。
で、以下をコピペしてください。
public function tags()
{
return $this->belongsToMany('App\Tag')->withTimestamps();
}
はい、
belongsToMany
これは多対多リレーションを設定する際に使用する関数です。
withTimestamps()
これは、タグの保存処理をした際などに、中間テーブルのcreated_at、updated_atを自動的に更新する関数になります。
(反対にTagモデルにも、同じように
belongsToMany('App\Article')
を設定すれば、TagモデルからもArticleモデルへリレーションを設定することができます。今回はTagからArticleを参照する実装はしませんので、参考にしてください。)
ということで、これでリレーションの設定は終わりです!
∑(゚Д゚)!?
と思った方!鋭いですね!
article_tagというテーブル名がどこにもないですよね?
そもそもarticle_tagモデルは必要ないのかってことですが、これはlaravelのルールに則っているので大丈夫です。中間テーブルのモデルも必要ありません。sqldesignerで中間テーブルを作成したときに説明したように、article_tagテーブルはlaravelのルールに則って作成しました。このルールに則ってテーブルを作成し、多対多リレーションを設定すると、laravelは自動で、呼び出し元のモデル名とリレーション先のモデル名のアルファベット順で並んだテーブルを探して、その値を参照し、関連付けてくれます!
このようにlaravelのルールに従えば、多くの面倒な処理をカットできるので、極力ルールに従いましょう!
そしたら、リレーションが設定できたので、タグを登録してみましょう。
タグを登録する
それでは、ブログの投稿フォームにタグの投稿フォームを作成しましょう。
new.blade.phpを開いてください。
で、以下のように編集してください。
@extends('layouts.app')
@section('content')
<form action="{{route('article.create')}}" method="post">
@csrf
<input name="title" type="text" value="{{old('title')}}">
<input type="text" name="tag" value="{{old('tag')}}">//<-追加
<textarea name="content" cols="30" rows="10">{{old('content')}}</textarea>
<input type="submit">
</form>
@endsection
そしたら一旦viewを確認しましょう。
ログインしてから記事投稿ページにアクセスしてください。
こんな感じでフォームが追加されてますね。オッケーです。
そしたら、タグをどのようにして複数投稿するかですが、今回は複雑にならないよう","で区切ることとします。
(もちろん、他にも半角スペース、全角スペース、全角の"、"なども考慮して文字列を区切ることができます。","以外にも区切りたいという方は色々調べてみましょう!)
そしたら、適当にフォームに入力して送信してみましょう。
そしたらコントローラで値を確認してみます。
オッケーそうですね。では、これをタグテーブルに保存しつつ、中間テーブルにもidを保存していきます。
では、createアクションを以下のように編集してください。
~~省略
use App\Tag;//<-追加
~~省略
public function create(Request $request)
{
$validator = Validator::make($request->all(), [
'title' => [
'required',
'string',
'max:25',
],
'content' => [
'required',
'string',
'max:4000',
],
//↓追加
'tag' => [
'nullable',
'string',
],
]);
$validator->validate();
$title = $request->get('title');
$content = $request->get('content');
$user_id = Auth::id();
$article = Article::create(
[
'title' => $title,
'content' => $content,
'user_id' => $user_id,
]
);
//↓諸々追加
$input_tag = $request->get('tag');
if (isset($input_tag)) {
$tag_ids = [];
$tags = explode(',', $input_tag);
foreach ($tags as $tag) {
$tag = Tag::updateOrCreate(
[
'name' => $tag,
]
);
$tag_ids[] = $tag->id;
}
$article->tags()->sync($tag_ids);
}
return redirect()->route('article.show', ['id' => $article->id]);
}
はい、まずは忘れずTagモデルをuseしましょう。
で、validatorで、
'tag' => [
'nullable',
'string',
],
これを追加しています。
この意味は、
nullable->null許容
string->文字列
ということになります。
つまり、このパラメータはnull(値なし)オッケーですよ。値があった場合、文字列ですよ。という意味になります。
タグは未入力でもブログを投稿できる想定とするので、このvalidationを設定します。
$input_tag = $request->get('tag');
if (isset($input_tag)) {
$tag_ids = [];
$tags = explode(',', $input_tag);
foreach ($tags as $tag) {
$tag = Tag::updateOrCreate(
[
'name' => $tag,
]
);
$tag_ids[] = $tag->id;
}
$article->tags()->sync($tag_ids);
}
これは、タグの登録と、中間テーブルにidを保存する処理になります。
$input_tag = $request->get('tag');
これは、requestからtagのパラメータを取得する記述です。
if (isset($input_tag)) {
これは、もしタグのパラメータがあった場合(nullではない)という意味です。
$tag_ids = [];
これは、$tag_idsという空配列を作成する記述です。
配列とは、データを複数入れることができる型になります。
$tags = explode(',', $input_tag);
これは、文字列を配列に変換する記述です。
foreach ($tags as $tag) {
これは、"foreach"で配列にあるデータの数だけ、$tagにデータを渡して{}の中の処理を回す記述になります。
$tag = Tag::updateOrCreate(
[
'name' => $tag,
]
);
$tag_ids[] = $tag->id;
updateOrCreateとは、テーブルに
- 存在しないデータは保存する
- 存在するデータはアップデートする
という処理をしてくれる関数です。
つまり、もし、仮にtag1というタグが既に存在している場合、データは作成されず(updated_atは更新されると思います。多分)、tag2というタグが存在していなければ、新しくタグを作成するという処理になります。必然的に同じnameのtagが生まれないということですね。
そして、"$tag_ids[] = $tag->id"は、$tag_idsという空配列に、保存したtagのidを配列に追加する記述になります。
$article->tags()->sync($tag_ids);
これは、中間テーブルに、関連するidを保存する記述になります。
syncは、今までの紐付きを一旦解除し、改めて紐付きをしてくれる関数です。
つまり、中間テーブルが常に最適化されるということですね。
はい、色々と新しいものが出てきましたが、よくわからない部分はご自身で改めて調べてみてください。
では、リクエストを送信してみましょう。
はい、こんな感じで処理が通過すればオッケーです!
早速データが登録されているか、確認してみましょう。
はい、タグは問題なく保存されてますね!
はい、article_tagテーブルも、お互いのidが保存されてますね!OKです!
では、早速タグを表示しましょう!
タグを表示する
show.blade.phpを以下のように編集してください!
@extends('layouts.app')
@section('content')
投稿者: {{$article->user->name}}
<br>
title: {{$article->title}}
<br>
//↓色々追加
tag:
@if ($article->tags()->exists())
@foreach ($article->tags as $tag)
<span style="margin-left: 5px;">{{$tag->name}}</span>
@endforeach
@else
タグの登録はありません。
@endif
<br>
内容:
<br>
{!!nl2br(e($article->content))!!}
@endsection
@if
これは、blade内でif文を記述する場合の書き方になります。
@if ($article->tags()->exists())
で、これは、「もし、$articleに紐づくtagがあったら」という意味になります。
@foreach ($article->tags as $tag)
<span style="margin-left: 5px;">{{$tag->name}}</span>
@endforeach
で、これは、$articleに紐付くtagをforeachで回して、
$tag->nameでtagsテーブルのnameフィールドを出力する処理になります。
style="margin-left: 5px;"は、cssといって、レイアウトを調整する記述になります。
気になる方は調べてみましょう!
@else
これは、直前のifに該当しなかった場合という意味になります。
つまり、紐付いているtagがない場合ということですね。
では、適当にブログを投稿するか、タグを保存したブログのidを指定してshowアクションを実行してみてください。
はい!登録したタグが表示されてますね!
やったぜ!
では、念の為タグを登録しないでブログを投稿してみてください。
はい、ちゃんとif文が機能してますね!
ということでタグの登録の説明は以上になります!
少し複雑だったかと思いますが、設定さえしてしまえばどうにでもできるということですね。
さて、前回の記事と今回の記事で一通りリレーションについてやってきましたが、もしかしたら中には、「いや、xxx_idsフィールド作って、jsonとか文字列で","で区切って保存すればできるじゃん。」などと感じた方もいたでしょう。
結論から言うとそれでもできます。ですが、一つのフィールドに複数のデータ、または同じ属性のフィールドが複製されるような構造は、アンチパターンと言って、なるべく回避するデータベース設計に該当します。
このようにアンチパターンのデータベース設計をしてしまうと、それからのアプリケーションの拡張がどんどんしづらくなっていきます。さらに、すでに稼働しているアプリケーションの場合、直そうにも直せない状況が生まれます。
先人のエンジニアが「この設計じゃアカンかったぁぁ!!!」と嘆いて生まれたアンチパターン(多分)なので、みなさんも極力アンチパターンを避けるよう努力していきましょう!
...なんですが、ご自身が作成するアプリケーションの仕様によっては、アンチパターンを含んでてもいいと思います。アンチパターンて、「できるけど避けた方がいい」的な意味だと私は解釈しているので、実現したいことが実現できれば、いいんじゃないかなと私個人は思っています。
ただし、後々のアプリケーションのことを考えて、少しでも不安になる場合は、なるべくアンチパターンを避けた方がいいのかなぁ〜とも思っています。
まぁこの辺はご自身で色々調べてみて、正解だと思うやり方を探していくしかないですね!私も勉強します!
ということで、今回の記事は以上になります!
これからみなさんが何か作りたいものがある場合、是非この多対多のリレーションを活用してみてください!
ではまた!