Help us understand the problem. What is going on with this article?

LaravelでbelongsToManyしてみた。1 <中間テーブルの構成を作成・一方向からのリレーション>

  1. コレ
  2. 関連したタグを取得
  3. タグから関連記事を取得

環境

  • Laravel5.6
  • mysql5.7
  • nginx
  • composer
  • php

今回やること

  1. 多対多の中間テーブルを作り、attachをすること。
  2. 記事に紐づくタグを作る。
  3. 大したことはやってないです。確認程度に眺めてください。
  4. tweetがスペルミスtwiiteになってますが悪しからず。し、知ってたんだからね!

使用メソッド

  • belongsToMany
  • attach
  • firstOrCreate

作るDB構成

記事 --- 中間テーブル --- タグ
--- id ---
id ---> twiite_id ---
user_id tag_id <--- id
contents --- ---
--- --- name
time time time

端的に言うと---

--twiite table--

id user_id contents tag1 tag2 tag3 tag4 tag5
1 1 1コメやったぜ first --- --- --- ---
2 1 laravelの記事を書いています。 laravel mysql php composer framework
3 2 ホンダ シビッグ(車) ほんだし ビッグ(だし) ほんだし ホンダ 本田 --- ---

と管理すると、無駄な枠がいくつもできてしまいますよね。。
そして、5コと設定したら、DBを変えるまで、5コです。
これでは、変更があってもすぐに対応できませんので分けて管理します。

やっていく。。

※プロジェクトがある前提で進めます。

今回作るプロジェクトはこれ
(vendorファイルと.envがそのままなので、消してからcomposer install してください。)

必要なファイルを作成

php artisan make:model -mrc <記事名>
php artisan make:model -mrc <タグ名>
php artisan make:migrate <中間テーブル名>

複数記事と複数タグのリレーションをします。
必要そうなマイグレーションファイルやコントローラなども面倒なので、オプション付けて、一緒に作ってしまいましょう。
中間テーブルはテーブルを作成できれば、あとはいらないので、付属ファイルは作らないでOK。

まずはDBを整える

<日付>_<記事名>table.php

//<記事名> = twiite
class CreateTwiitesTable extends Migration
{

    public function up()
    {
        Schema::create('twiites', function (Blueprint $table) {
            $table->increments('id');
            $table->Integer('user_id')->unsigned();
            $table->string('contents');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('twiites');
    }
}

<日付>_<タグ名>table.php

//<タグ名> = tag
class CreateTagsTable extends Migration
{

    public function up()
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name', 20);
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('tags');
    }
}

<日付>_<中間テーブル名>table.php

<中間テーブル名> = twiitetag
class CreateTwiiteTagTable extends Migration
{

    public function up()
    {
        Schema::create('tag_twiite', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('twiite_id');
            $table->integer('tag_id');
        });
    }

    public function down()
    {
        Schema::dropIfExists('tag_twiite');
    }
}

マイグレーションファイルはこれで完成です。では、

php artisan migrate 

エラーが起きていなければ成功です。DBでも確認しましょう。

mysql -u ユーザ名 -p
Password:*******

mysql>use DB名
mysql>show tables;

table に先ほどのテーブルが作られていれば、成功。

desc table <テーブル名>;

これで、テーブルの制約が確認できますよ。

ルートを通します。

routes\web.php に追加

Route::resource('/twiite', TwiiteController::class,['except' => ['show', 'create']]);

詳細の場所は好きにしてください。exceptで今回使わないので、除去しています。

表示を整えます。

超適当に機能だけを付けています。

<html>
<head>
    <meta charset="utf-8">
    <title>多:多</title>
</head>
<body>
    <form method="post">
        {{ csrf_field() }}
        <input style="width:400px; height:100px" name="contents"><!--本文-->
    <input style="width:300px; height:30px" name="writelineTags"
        placeholder="「,」で区切って、タグ付けする(上限5コ)"><!--タグ文字列-->
    <input type="hidden" value=<?= $userId; ?> name="user_id"><!--ログイン中のid-->
        <button>投稿</button>
    </form>
    <h1>ーーーあなたのツイート一覧ーーー</h1>
    <div style="border: 1px solid #000; display: box; width: 400px">
      @foreach ($twiiteList as $twiite)
      <p>{{ $twiite->contents }}</p><!--渡されたツイートデータ(なければ実行なし)-->
      @endforeach
    </div>
</body>
</html>

Modelを作る

リレーションの設定は、DB個別でもできます。
直接sql文をたたくか、マイグレーションフィルに外部キーを設定でもできます。
が、しかし、せっかくフレームワークを使っているので、
生かさなければ!!
modelにて、その流れを肩代わりしてくれます。
外部キーなどをつくるわけではないので、そこは間違えないように!!

<記事名>.php

class Twiite extends Model
{
    protected $fillable = [
        'user_id',
        'contents'
    ];

    protected $dates = [
        'created_at',
        'updated_at'
    ];

   public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }
}

外部キーがあれば、第二引数に設定したものが外部キーとして扱える。
今回はないが、リレーション相手のtagテーブルのidを保存しておきたいため、
idは外部キーではなく、indexの主キーでのアクセスとなる。

今回のこのページで作っているのは、一方的なリレーションになるので、
tagからのhasmanyは無い。後で続編予定。。そこで、作る。。

現状できないこと
  • タグ情報から関連する記事を取得できない。
  • タグを独自で作ったり、削除ができない。というか全く扱えないw
  • 空文字の入力への対応ができない。
  • ほぼヴァリデーションしてないので、ガタガタ。。
  • 複数同じタグを入力できてしまう。カウントも増えるので、不正が可能。

Controller を作る

最後に作るのはコントローラ!

最後でなくてもいいんだけど、DBの入出力と、フロントの表示が整ってから、必要なデータの処理を
する方が、わかりやすいと思うんだ。。。

<記事>Controller.php

use App\Twiite;
use App\Tag;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class TwiiteController extends Controller
{
    protected $twiite;

    public function __construct(Twiite $twiite){
        $this->middleware('auth');//ユーザ認証
        $this->twiite = $twiite;//twiiteクラスのインスタンス化
    }

    public function index(Request $request)
    {
        $userId = Auth::id();//ログイン中のid

        $twiiteList = $this->twiite->where('user_id',$userId)
            ->orderby('created_at','desc')
            ->get();//ユーザのtwiite全取得

        return view('index', compact('twiiteList', 'userId'));//index view
    }

    public function store(Request $request)
    {
      $userId = Auth::id();//ログイン中のid
      $inputs = $request->all();//リクエスト全て

      if(!empty($inputs['writelineTags'])){
        $tags = $this->setTagDivide($inputs['writelineTags']);//文字列を配列に分割「,」
        $tag_ids = $this->getIds($tags);//分割したタグを作り(またはカウントを追加し、)、そのidを取得
      }

      $this->twiite->contents = $inputs['contents'];//contentsデータを追加
      $twiite = $this->twiite->create($inputs);//twiiteデータ一件を作成&一件のデータを持つmodelのオブジェ 
 クト取得
      $twiite->tags()->attach($tag_ids);//中間テーブルに格納

      return redirect()->to('twiite');//indexへリダイレクト
    }

    public function setTagDivide($tagstring){
      $tags = explode(',',$tagstring);//文字列から特定の文字に応じて区切り、配列として格納。
      return $tags;//Is Array.
    }

    public function getIds($tags){
      $tagClass = new Tag();//tagを扱うクラスをインスタンス化
      $recodes = [];//帰り値の初期化
      $count = 5;
      foreach($tags as $tag){
        $count--;//カウント
        if($count <= 0) exit;//上限を超える場合は強制退場

        $aleadyTag = $tagClass->firstOrCreate(['name' => $tag]);//あれば取得、なければ作成。
        $aleadyTag->increment('counts');
        $recodes[] = $aleadyTag->id;
      }
         // dd($recodes);//tag_id,tag_id,tag_id..
        return $recodes;
    }
}

※Authはユーザ認証のメソッドです。
ユーザーは一人ではないので、そのidなどを判定するために使っています。
処理にはAuthやmidlewereなどは直接関係ないです。

見る人が見ると、「はあ?」と怒られてしまいそうな記述ですが、
急ぎ説明用に作ったものなので、ご容赦を。。

とりあえず、それぞれ横に書いてあるdirectionの通りの意味です。
詳細は自分で調べてください。

今回重要なのは、ここ。

      $twiite = $this->twiite->create($inputs);
      $twiite->tags()->attach($tag_ids);

$this->twiiteは、いわばnew Twiite() やTwiite::Classで作れるもののことで、モデルクラスをインスタンス化したものです。
createメソッドは返り値が追加したデータを持つ、モデルクラスのオブジェクトなので、
今回は1件のデータをattributeに持つ、オブジェクトになります。

当然モデルクラスにデータが入っているだけなので、インスタンスとは何ら機能に変化はありません。
そこから、先ほど<記事名>.phpで行った、tags()というメソッドを使い、
アタッチをしています。。
attacheを扱っているのは<記事名>クラスなので、その(指定がなければidになる)id、
attacheで送るデータは()内に入った、タグのid。

attacheメソッドは引数が配列データで送れるので、その場合は、複数個中間テーブルに保存されることになります。
ex)記事1個目にタグが複数ある場合。

twiite_id tag_id
1 1
1 2
1 3
1 4
2 4

こんな感じで、中間テーブルが同一の記事idに複数一気に作成されます。

中間テーブルの利用。

登録をしただけじゃ、何の意味もないので、これを呼び出す必要もあります。
が、今回はこのへんで! 次回の記事に続きを書いていきますので、こちら(次)も見てください。

本記事シリーズの完成版はこちら。gitの管理をしっかりしました。
https://github.com/175B005/vueLaravel

また違うのをやっていくので、ここのブランチはvue.jsで追加していく用です。
belongstomanyというブランチでここまでを残しておきますので、
どうぞよろしくお願いします。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした