Laravel(バージョン8.0.4を利用)において、「多対多(Many to Many)」のリレーションを持つモデルを構築し、簡単なアプリケーションを作成します。
本記事では下記のような関係を持つbooks/tags/book_tagの3つのテーブルを作成します。booksとtagsは多対多の関係であり、book_tagテーブルは中間テーブルとなります。
作成するアプリケーションは、Book一覧画面・新規作成画面・編集画面、また任意のタグが付けられたBookの一覧画面を実装します。なお本記事ではバリデーションは実装していません。
1. Book一覧の画面です。 2. あらかじめtagsテーブルには喜び/悲しみ/笑いを登録してあり、チェックボックスとしてタグ付けします。 3. 新規登録すると一覧画面に戻り、登録完了のメッセージが表示されます。 4. 編集画面で「編集する」ボタンをクリックすると、編集完了のメッセージが表示されます(5)。 6. すでに2冊登録してある様子です。「笑い」タグをクリックすると紐付けられたBookの一覧(7)が表示されます。今回は単純に登録されたタグのidを/tag/3のようにURLとして利用します。
テーブルの作成
まずBookとTagのモデルとマイグレーションファイルを作成し、下記のように編集します。
1 |
$ php artisan make:model Book --migration |
1 |
$ php artisan make:model Tag --migration |
app/Models/Book.php (※Laravel 8.xではModelsディレクトリが生成されます。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Book extends Model { use HasFactory; protected $fillable = [ 'title', ]; public function tags() { return $this->belongsToMany('App\Models\Tag')->withTimestamps(); } } |
18行目 belongsToManyメソッドで多対多として関連付けしています。withTimestampsメソッドで中間テーブルのタイムスタンプを更新します。
app/Models/Tag.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Tag extends Model { use HasFactory; protected $fillable = [ 'name', ]; public function books() { return $this->belongsToMany('App\Models\Book')->withTimestamps(); } } |
database/migrations/ディレクトリ以下に2つのファイルが生成されるので編集します。
2020_11_02_055438_create_books_table.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateBooksTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('books', function (Blueprint $table) { $table->id(); $table->string('title'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('books'); } } |
2020_11_02_055457_create_tags_table.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<?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->id(); $table->string('name'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('tags'); } } |
次に中間テーブルのマイグレーションファイルを作成します(モデルの作成は不要)。
1 |
$ php artisan make:migration create_book_tag_table |
規約通りに作成するならば、中間テーブルの名前は関係するモデル名(bookとtag)をアルファベット順に並べた「book_tag」となります。もし別のテーブル名にするならば、上述したbelongsToManyメソッドの第2引数に設定します。
2020_11_02_060213_create_book_tag_table.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateBookTagTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('book_tag', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('book_id'); $table->unsignedBigInteger('tag_id'); $table->foreign('book_id')->references('id')->on('books')->onDelete('cascade'); $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('book_tag'); } } |
20/21行目 外部キー制約を設定しています。
マイグレーションを実行します。
1 |
$ php artisan migrate |
今回はTag自体のCRUD機能は作成しませんので、生成されたtagsテーブルに適当なデータを登録しておいて下さい。
コントローラーの作成
次に–resourceを付けてコントローラーファイルを作成します。TagControllerは上述した画面遷移図の7で利用します。
1 |
$ php artisan make:controller BookController --resource |
1 |
$ php artisan make:controller TagController --resource |
web.phpに下記コードを追記し、ルーティングの設定をおこないます。
routes/web.php(※Laravel 8.x ~での記述に注意)
1 2 3 4 5 |
use App\Http\Controllers\BookController; use App\Http\Controllers\TagController; Route::resource('/book', BookController::class); Route::resource('/tag', TagController::class); |
設定されているルート情報を確認します。今回利用するルートをハイライトしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
$ php artisan route:list +--------+-----------+------------------+--------------+---------------------------------------------+------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+-----------+------------------+--------------+---------------------------------------------+------------+ | | GET|HEAD | / | | Closure | web | | | GET|HEAD | api/user | | Closure | api | | | | | | | auth:api | | | GET|HEAD | book | book.index | App\Http\Controllers\BookController@index | web | | | POST | book | book.store | App\Http\Controllers\BookController@store | web | | | GET|HEAD | book/create | book.create | App\Http\Controllers\BookController@create | web | | | GET|HEAD | book/{book} | book.show | App\Http\Controllers\BookController@show | web | | | PUT|PATCH | book/{book} | book.update | App\Http\Controllers\BookController@update | web | | | DELETE | book/{book} | book.destroy | App\Http\Controllers\BookController@destroy | web | | | GET|HEAD | book/{book}/edit | book.edit | App\Http\Controllers\BookController@edit | web | | | GET|HEAD | tag | tag.index | App\Http\Controllers\TagController@index | web | | | POST | tag | tag.store | App\Http\Controllers\TagController@store | web | | | GET|HEAD | tag/create | tag.create | App\Http\Controllers\TagController@create | web | | | GET|HEAD | tag/{tag} | tag.show | App\Http\Controllers\TagController@show | web | | | PUT|PATCH | tag/{tag} | tag.update | App\Http\Controllers\TagController@update | web | | | DELETE | tag/{tag} | tag.destroy | App\Http\Controllers\TagController@destroy | web | | | GET|HEAD | tag/{tag}/edit | tag.edit | App\Http\Controllers\TagController@edit | web | +--------+-----------+------------------+--------------+---------------------------------------------+------------+ |
–resourceを付けることにより作成したコントローラーファイルに各アクションである index() / create() / store() / show() / edit() / update() / destroy() があらかじめ記述されているのが確認できます。下記のように追記します(ハイライト部分)。
app/Http/Controllers/BookController.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Models\Book; use App\Models\Tag; class BookController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { $books = Book::all(); return view('book.index', compact('books')); } /** * Show the form for creating a new resource. * * @return \Illuminate\Http\Response */ public function create() { $tags = Tag::all(); return view('book.create', compact('tags')); } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { $book = Book::create($request->all()); $book->tags()->attach(request()->tags); return redirect()->route('book.index')->with('success', '新規登録完了しました'); } /** * Display the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function show($id) { // } /** * Show the form for editing the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function edit($id) { $book = Book::find($id); $tags = $book->tags->pluck('id')->toArray(); $tagList = Tag::all(); return view('book.edit', compact('book', 'tags', 'tagList')); } /** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param int $id * @return \Illuminate\Http\Response */ public function update(Request $request, $id) { $update = [ 'title' => $request->title, ]; Book::where('id', $id)->update($update); $book = Book::find($id); $book->tags()->sync(request()->tags); return back()->with('success', '編集完了しました'); } /** * Remove the specified resource from storage. * * @param int $id * @return \Illuminate\Http\Response */ public function destroy($id) { $book = Book::find($id); $book->delete(); $book->tags()->detach(); return redirect()->route('book.index')->with('success', '削除完了しました'); } } |
43行目 attachメソッドを利用して、中間テーブルにデータを追加しています。
86行目 syncメソッドを利用して、中間テーブルの更新をおこなっています。
100行目 detachメソッドを利用すると中間テーブルから関連するデータも削除されます。ただし、マイグレーションファイルにて外部キー制約を設定しているため、ここではdetachメソッドがなくても削除されます。
app/Http/Controllers/TagController.php
1 2 3 4 5 6 7 8 9 10 11 |
use App\Models\Tag; ~略 public function show($id) { $tag = Tag::find($id); return view('tag.show', compact('tag')); } ~略 |
ビューの作成
resources/views以下にbookおよびtagディレクトリを用意し、下記のようにファイルを作成します。
Book一覧画面
resources/views/book/index.blade.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<h1>Book一覧画面</h1> <p><a href="{{ route('book.create') }}">新規追加</a></p> @if ($message = Session::get('success')) <p>{{ $message }}</p> @endif <table border="1"> <tr> <th>title</th> <th>tag</th> <th>編集</th> <th>削除</th> </tr> @foreach ($books as $book) <tr> <td>{{ $book->title }}</td> <td> @foreach ($book->tags as $tag) <a href="{{ route('tag.show',$tag->id)}}">{{ $tag->name }}</a> @endforeach </td> <td><a href="{{ route('book.edit',$book->id)}}">編集</a></td> <td> <form action="{{ route('book.destroy', $book->id)}}" method="POST"> @csrf @method('DELETE') <input type="submit" name="" value="削除"> </form> </td> </tr> @endforeach </table> |
Book新規作成画面
resources/views/book/create.blade.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<h1>Book新規作成画面</h1> <form action="{{ route('book.store')}}" method="POST"> @csrf <p>タイトル<input type="text" name="title" value="{{ old('title') }}"></p> <p> @foreach($tags as $tag) <label> <input type="checkbox" name="tags[]" value="{{ $tag->id }}">{{ $tag->name }} </label> @endforeach </p> <input type="submit" value="登録する"> </form> |
編集画面
resources/views/book/edit.blade.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<h1>編集画面</h1> <p><a href="{{ route('book.index') }}">Book一覧画面</a></p> @if ($message = Session::get('success')) <p>{{ $message }}</p> @endif <form action="{{ route('book.update',$book->id)}}" method="POST"> @csrf @method('PUT') <p>タイトル<input type="text" name="title" value="{{ $book->title }}"></p> <p> @foreach ($tagList as $tag) <label class="checkbox"> <input type="checkbox" name="tags[]" value="{{$tag->id}}" @if(in_array($tag->id,$tags)) checked @endif> {{ $tag->name }} </label> @endforeach </p> <input type="submit" value="編集する"> </form> |
resources/views/tag/show.blade.php
1 2 3 4 5 6 7 |
<h1>{{ $tag->name }}タグBook一覧</h1> <ul> @foreach ($tag->books as $book) <li>{{ $book->title }}</li> @endforeach </ul> |
関連ページ
参照ページ
Laravel 8.x Eloquent:リレーション