Laravelでテーブル同士を関連付ける

2018年08月21日 00時08分

Laravel で DB の取り扱いについて

動作環境

Laravel5.6
php7.1.3
MySQL5.7.22
Bootstrap4.0.0

今回 DB は MySQL で実装している。

掲示板に関連付けられた、コメントを取り出したいので、その例を取り上げる。

流れとしては、

  1. テーブル、カラム生成(マイグレーション実行)

  2. モデル作成、関連付け(Eloquent モデル使用)

  3. コントローラーに反映

  4. ビューに反映(Blade 使用)

になる。

テーブル・カラムの生成

テーブル生成は、1.SQL を直接打つ方法と2.マイグレーションする方法がある。

Laravel はマイグレーション機能があるから、ドキュメントルートから Artisan コマンドを実行して、マイグレーションファイルを生成する。

マイグレーションについては、公式ドキュメントの日本語訳に概略が載っているので引用する。

マイグレーションとはデータベースのバージョンコントロールのような機能です。アプリケーションデータベースのスキーマの更新をチームで簡単に共有できるようにしてくれます。マイグレーションは基本的に Laravel のスキーマビルダとペアで使い、アプリケーションのデータベーススキーマの作成を楽にしてくれます。もしあなたが今まで、チームメイトに彼らのローカルデータベーススキーマに手作業でカラムを追加するよう依頼したことがあるなら、データベースマイグレーションは、そうした問題を解決してくれます。

Laravel 5.6 データベース:マイグレーション

マイグレーションを行うことで、「DB の実装の度合いを共有できる、スキーマ生成が楽になる。」くらいの理解だ。

マイグレーションファイルはドキュメントルートで artisan コマンドを実行することで生成できる。

php artisan make:migration create_boards_table --create=board

create_board_table が生成したいファイル名。 –create=board がオプション。

今回は、create_board_table を生成した。

–create=board とすることで、「boards_table を生成しますよ。」と Laravel に伝えているので、最初からマイグレーションファイルにある程度の記述がされている。

同じようにコメント用のマイグレーションファイルも生成する。

そして、カラムを function up 内に追記。

app\database\migrations\xxx_xx_xx_xxxxxx_create_boards_table
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateBoardTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('boards', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title')->nullable();
            $table->string('name')->nullable();
            $table->string('content', 2000)->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('boards');
    }
}
app\database\migrations\xxx_xx_xx_xxxxxx_create_comments_table
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateCommentTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('board_id');
            $table->string('name')->nullable();
            $table->string('comments')->nullable();
            $table->timestamps();

            $table->foreign('board_id')->references('id')->on('boards');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('comments');
    }
}

コメントテーブルには、board_id(親要素の id)と合わせて外部キー制約をつける。

Laravel は賢いので、boards_table の id はインクリメントかつアンサインドで実装している。

そのため、外部キーである board_id も同様にアンサインドにしておかないとマイグレーション実装時に外部キー制約にひっかかってエラーを吐くので、要注意。

最後に artisan コマンドで migrate を行う。

php artisan migrate

うまくいっていれば、先ほど生成したファイルのマイグレートが実行される。

これでテーブル生成は完了。

モデルの生成・関連付け

Laravel は Eloquent ORM があり、ざっくりいうと、クエリ及びクエリによって取得生成されたカラムをオブジェクト化したようなものがある。

これを使用するためには、Eloquent Model の設定、外部キーを使うのであればモデル同士の関連付けが必要になる。

まず、Model の生成から。

モデルの生成

ルートディレクトリで

php artisan make:model Models/Board

php artisan make:model Models/Comment

今回モデルは、app/Models に入れているので、ディレクトリも指定してあげる。

これで、モデルの生成は完了。

モデルとテーブルの関連付けについては、モデル内で実装する必要がある。

ただ、テーブル名がモデルの複数形である場合、Laravel の方で察して自動的にモデルとテーブルを関連付ける

モデル同士の関連付け

今回は、掲示板の書き込みとコメントが、1 対多の関係になっている。

それを Board モデルに書き込む。

その他の関係については、公式ドキュメントの Eloquent を参照

app\Models\Board
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Board extends Model
{
    //relation

    public function Comment(){

        return $this->hasMany('App\Models\Comment', 'board_id');
    }
}

これで関連付けは終わり。

コントローラーに反映

今回は、書き込みの取得については、コントローラーに実装する。

コントローラー生成も artisan コマンドで実行できる。

php artisan make:controller BoardController

コントローラーには、

use 文で Board と Comment は Eloquent モデルも使っていることを明言してあげる。

今回は index に処理を書き込んでいく。

コントローラーには、書き込みを全部(Board::all())とってきて変数$boards に入れてほしいと書く。

より詳しい条件として、書き込みの新しい順で(orderBy(‘created_at’, ‘desc’))、→ 書き込み 5 件ごとにページ分けして(paginate(5))もしてほしいと書き込む。

ここまでしたら、bbs ドキュメントの index を表示してね。( return view(‘bbs.index’))。

変数$boardsはboardsって名前で渡してね。(->with([‘boards’ => $boards,]);))

と書き込む。

app/Http/Controllers/BoardController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Board;
use App\Models\Comment;

class BoardController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */

    public function __construct()
    {
        //
    }

    public function index()
    {
        $boards = Board::all()->orderBy('created_at', 'desc')->paginate(5);
        return view('bbs.index')->with([
            'boards' => $boards,
         ]);
    }
}

ビューの生成

今回は、blade テンプレートを使用します。

{{変数}}でエスケープをいい感じにしてくれたり、@extend()でレイアウトの継承ができたりとても便利です。

今回はレイアウトに Bootstrap を使いますので、継承元には読み込みを指示しておきます。

基本的に変数は{{}}内で取り扱うので、そこを注目していきましょう。

resources/views/layouts/layouts.blade.php
<html>
  <head>
    <title>@yield('title')</title>
      <link href="{{ asset('css/app.css') }}" rel="stylesheet">
      <style>
        @yield('style')
    </style>
  </head>

  <body>
    <div id="app" class="row">
        <div class="col-2"></div>
        <div class="col-8">@yield('contents')</div>
        <div class="col-2"></div>
    </div>
    <script src="{{ asset('js/app.js') }}" defer></script>
  </body>
</html>
resources/views/bbs/index.blade.php
@extends('layouts.layouts')
  <span class=""><span class="">@</span></span>section('title', 'board.index')
<span class="">  @se</span>ction('contents')
    <span class="">@</span>foreach($boards as $board)
         <div class="row">
            <div class="col-md-12">
                <div class="media" style="padding:10px;"><img class="rounded-circle mr-3" style="margin:10px 0px 0px 10px;padding:0px;">
                    <div class="media-body">
                        <h5 style="margin:10px 10px 10px 0px;">{{ $board->name }} : <b>{{ $board->title }}</b></h5>
                        <small>投稿日時:{{ date("Y年 m月 d日",strtotime($board->created_at)) }}</small>
                        <div class="mt-1">
                            <p>{!! nl2br(e($board->content), false) !!}</p>
                        </div>
                        <div class="col">
                            <a class="btn btn-outline-success" data-toggle="collapse" href="#post{{ $board->id }}" role="button" aria-expanded="false" aria-controls="collapseExample">返信する</a>
                            <p>コメント数:{{ $board->comment()->count() }}</p>
                        </div>
                        <hr>
                            <div class="collapse" id="post{{ $board->id }}">
                                <form id="commentPost" action="{{ url('/') }}/comment/{{ $board->id }}/post" method="post">
                                    {!! csrf_field() !!}
                                    <label>名前</label>
                                    <input class="form-control col col-sm-10" type="text" name="comment_user_name" value="{{ old('name') }}">
                                    <label class="d-flex" style="margin:0px 0px 15px 0px;">内容</label>
                                    <textarea class="col col-sm-10" style="height:50%;" name="comments" value="{{ old('comment') }}"></textarea>
                                    <button class="btn btn-outline-success></button>
                                </form>
                            </div>
                            @foreach( $board->comment as $comment )
                                <div class="media"><img class="rounded-circle mr-3">
                                    <div class="media-body">
                                        <h5 style="margin:10px 10px 10px 0px;">{{ $comment->comment_user_name }}</h5>
                                        <small>投稿日時{{ date("Y年 m月 d日",strtotime($comment->created_at)) }}</small>
                                        <div class="mt-1">
                                            <p>{!! nl2br(e($comment->comments), false) !!}</p>
                                        </div>
                                    </div>
                                </div>
                                <hr>
                                @if($loop->iteration == 3 && $loop->remaining >= 1)
                                    {{-- ループ回数が3回時、ループ残が1以上ある場合--}}
                                    <div class="btn btn-outline-primary ml-3"><a href="{{ url('/')}}/bbs/show/{{$board->id }}">続きを読む</a></div>
                                    @break
                                @endif
                            @endforeach
                    </div>
                </div>
            </div>
        </div>
        @endforeach
    </div>
    <nav class="row justify-content-center">
        {{ $boards->appends(Request::only('keyword'))->links() }}
    </nav>
</div>

書き込みのデータは、コントローラーに boards に変数$boards として渡すように指示をしていたので、そこから取り出してあげます。

その際、配列を1つずつ取り出すなら、foreach{{$boards as $board}}で順番に取り出すことができます。

更に、内部を見ると、$boardのtitleを出したいということで、{{$board->title}}などが書けます。

コメントについても書き込みに関連付けられているので、$boardに関連付けられたcommentを、1件ずつ取り出したいという意味で、foreach($board->comment as $comment)で取り出します。

投稿内容については、 改行を含んでいますが、{{}}は改行もエスケープしてしまうので、改行だけはエスケープしないように指示を出しています。<p>{!! nl2br(e($comment->comments), false) !!}</p>

これで、一通りの表示はできますが、投稿機能はここでは実装されていません。

また別の機会に書ければと思います。