[Laravel] BelongsToで1対多のリレーション

今回はLaravelで1対多のデータを扱ってみます。1対多はデータベースのリレーションではbelongsToということが多いですね。今回は複数のチームを登録するteamsテーブル、メンバーを登録するmembersテーブルを用意します。

メンバーはどれか一つのteamに所属します。1つのチーム側から見ると複数のメンバーが登録されることになり、1対多の関係になります。teamsテーブルが親、membersテーブルが子の関係になっています。

メンバーの新規登録、更新(編集)の時に所属チームを登録したり変更したりするCRUD(Create, Read, Update, Delete)を作成してみます。

ModelとMigrationの作成

MemberモデルとTeamモデルを作成します。マイグレーションファイルも同時に作成します。

# php artisan make:model Member --migration
# php artisan make:model Team --migration

[モデル]
プロジェクトディレクトリ/app/Member.php
プロジェクトディレクトリ/app/Team.php

[マイグレーション]
プロジェクトディレクトリ/database/migrations/2020_04_13_154956_create_members_table.php
プロジェクトディレクトリ/database/migrations/2020_04_13_155219_create_teams_table.php

の4つのファイルが作られます。

マイグレーションの実行

まず、モデル(テーブル)にカラムを設定し、MySQLにテーブルを作成しましょう。

「2020_04_13_154956_create_members_table.php」

<?php

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

class CreateMembersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('members', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->integer('team_id');
            $table->timestamps();
        });
    }

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

$table->bigIncrements(‘id’);はデフォルトだと $table->(‘id’);になっています。このままだとリレーションがうまく設定されずエラーになりました。ここはbigIncrementsを付けるのを忘れないようにしましょう。この原因を見つけるまで時間がかかってしまいました。

Laravel5.8からは主キーのデフォルトがincrements()からbigIncrements()に変更になっています。
・INTの範囲は-2147483648〜2147483647[21億4千7百万までのデータ]
・BIGINTの範囲は-9223372036854775808~9223372036854775807[20桁までの数値]
大きな数値を使わない場合はINTのほうがパフォーマンスが良いのでINTの設定の方が良いです。(今回はテストなのでパフォーマンスは無視してBIGINT(bigIncrements)を使っています。

マイグレーションではBigIncrementで生成されたidはunSingledBigIntegerで指定(idに-値は使わない)

「020_04_13_155219_create_teams_table.php」

<?php

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

class CreateTeamsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('teams', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->timestamps();
        });
    }

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

こちらも $table->bigIncrements(‘id’);のbigIncrementsを忘れずに!

2つのマイグレーションファイルが作り終わったらコンソールからマイグレーションを実行します。

$ php artisan migrate

「membersテーブル」
2020-04-15_00h27_05

「teamsテーブル」
2020-04-15_00h27_38

Temamsテーブルには「サッカー・野球」のようなデータをphpMyAdminで入力しておきます。

モデル

「/app/Member.php」(子)のファイルを編集します。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Member extends Model
{
    protected $fillable = [
        'name',
        'team_id',
    ];

    public function team()
    {
        return $this->belongsTo('App\Team');
    }
}

まずはお決まりの$fillableでカラムへのアクセスを許可します。

public function team(){}がリレーションの設定になります。
$this->belongsTo(‘/app/親モデル名’);と記載するだけでリレーションを作成してくれます。外部キーがteam_idという(小文字モデル名_id)になっている場合は、自動的にteam_idが外部キーとして処理されます。membersテーブルの中にteam_idを作成しておいたのは外部キーとして利用するためです。

「/app/Team.php」(親)

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Team extends Model
{
    protected $fillable = [
        'name',
    ];

    public function members()
    {
        return $this->hasMany('App\Member');
    }
}

親の方も子と同様に$fillableを設定します。
こちらは1対多の多(親)になるので、hasMany()を使います。
$this->hasMany(‘App\Member’);と子モデルを設定します。
これで2つのリレーションの設定は終わりです。

ルーティング

routes/web.phpでルーティングを設定します。

Route::resource('/member', 'MemberController');

この1行を記載するとCRUDに必要なルーティングが自動設定されます。
ルーティングを確認してみましょう。

# php artisan route:list

2020-04-15_00h30_07

index()
create()
store()
show()
edit()
update()
destroy()
というルーティングが作られています。

コントローラーの作成

データベースの作成、モデルの作成、ルーティングの設定が終わったので、今度はコントローラを作っていきます。

# php artisan make:controller MemberController --resource

「app/Http/Controllers/MemberController.php」の設定
コントローラー部分は長くなりますが、ひとつひとつのアクションの中身はシンプルです。

コントローラー全体

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Member;
use App\Team;

class MemberController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $members = Member::all();
        return view('member.index', compact('members'));
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        $teams = Team::all();
        return view('member.create', compact('teams'));
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        Member::create($request->all());
        return redirect()->route('member.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)
    {
        $teams = Team::all();
        $member = Member::find($id);
        return view('member.edit', compact('member', 'teams'));
    }

    /**
     * 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 = [
            'name' => $request->name,
            'team_id' => $request->team_id
        ];
        Member::where('id', $id)->update($update);
        return back()->with('success', '編集完了');
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        //
    }
}

index()

$members = Member::all();
return view('member.index', compact('members'));

「$members = Member::all();」では変数$membersに membersテーブルの全データを代入しています。
returnのところはデータをビューファイルに渡す設定です。
member/indexに$membersを渡しています。
compactは()の中に’members’と記載するだけで変数をビューに渡してくれる便利なメソッドです。

create()

$teams = Team::all();
return view('member.create', compact('teams'));

情報の新規登録ではTeam名をセレクトできるようにします。セレクトボックスを作るのにTeamのデータが必要なのでTeamのデータを取得してmember/createに渡します。

store()

 Member::create($request->all());
return redirect()->route('member.index')->with('success', '登録完了');

Member::create($request->all());は下記の2つを実行しています。
$request->all()で全データを連想配列で取得
Member::create()でmembersテーブルに新規データとして登録

「->with(‘success’, ‘登録完了’);」をつけて、登録完了の文字を登録させます。Laravelではこのように簡単にメッセージを表示させる仕組みが用意されています。

edit()

$teams = Team::all();
$member = Member::find($id);
return view('member.edit', compact('member', 'teams'));

「$teams = Team::all();」
編集の時にはセレクトボックスでTeamの値を使うので取得しています。

「$member = Member::find($id);」
編集する個別のデータをidで抽出

2つのデータをビューに渡しています。

update()

$update = [
     'name' => $request->name,
     'team_id' => $request->team_id
];
 Member::where('id', $id)->update($update);
return back()->with('success', '編集完了');

$requestの中のname,team_idを$updateに代入。
「Member::where(‘id’, $id)->update($update);」でidを抽出し、そのidのデータを$updateのデータに上書きします。
「return back()->with(‘success’, ‘編集完了’);」は元のページに戻る、そして編集完了の文字を出力するという意味になります。

ビューファイル

index.blade.php

<h1> Member List</h1>
<p><a href="{{ route('member.create') }}">Create Record</a></p>
 
@if ($message = Session::get('success'))
<p>{{ $message }}</p>
@endif
 
<table border="1">
    <tr>
        <th>Name</th>
        <th>Team</th>
        <th>Edit</th>
    </tr>
    @foreach ($members as $member)
    <tr>
        <td>{{ $member->name }}</td>
        <td>{{ $member->team->name }}</td>
        <th><a href="{{ route('member.edit',$member->id)}}">Edit</a></th>
    </tr>
    @endforeach
</table>


<?php
echo '<pre>' . var_export($members, true) . '</pre>';
?>

create.blade.php

<h1> Create Record </h1>
<p><a href="{{ route('member.index')}}"> Member List </a></p>
 
<form action="{{ route('member.store')}}" method="POST">
    @csrf
    <p>Name<input type="text" name="name" value="{{ old('name') }}"></p>
    <p>
        <select name="team_id">
            @foreach($teams as $team)
            <option value="{{ $team->id }}">{{ $team->name }}</option>
            @endforeach
        </select>
    </p>
    <input type="submit" value="Add Data">
</form>

edit.blade.php

<h1>Edit Member</h1>
<p><a href="{{ route('member.index')}}"> Member List</a></p>
 
@if ($message = Session::get('success'))
<p>{{ $message }}</p>
@endif
 
<form action="{{ route('member.update',$member->id)}}" method="POST">
    @csrf
    @method('PUT')
    <p>Name<input type="text" name="name" value="{{ $member->name }}"></p>
    <p>
        <select name="team_id">
            @foreach($teams as $team)
            <option value="{{ $team->id }}" @if($team->id === $member->team_id) selected='selected' @endif>{{ $team->name }}</option>
            @endforeach
        </select>
    </p>
    <input type="submit" value="Edit">
</form>

リレーションの確認

裏側ではどのようなデータが動いているのか見てみます。
index.blade.phpに

echo '<pre>' . var_export($members, true) . '</pre>';

を記載して「ドメイン名/member/index」にアクセスします。

長くなりますが、これが2件分のデーターです。Teamの内容が取得できていることがわかります。

Illuminate\Database\Eloquent\Collection::__set_state(array(
   'items' => 
  array (
    0 => 
    App\Member::__set_state(array(
       'fillable' => 
      array (
        0 => 'name',
        1 => 'team_id',
      ),
       'connection' => 'mysql',
       'table' => 'members',
       'primaryKey' => 'id',
       'keyType' => 'int',
       'incrementing' => true,
       'with' => 
      array (
      ),
       'withCount' => 
      array (
      ),
       'perPage' => 15,
       'exists' => true,
       'wasRecentlyCreated' => false,
       'attributes' => 
      array (
        'id' => 1,
        'name' => 'Taro',
        'team_id' => 1,
        'created_at' => '2020-04-14 15:10:24',
        'updated_at' => '2020-04-14 15:10:24',
      ),
       'original' => 
      array (
        'id' => 1,
        'name' => 'Taro',
        'team_id' => 1,
        'created_at' => '2020-04-14 15:10:24',
        'updated_at' => '2020-04-14 15:10:24',
      ),
       'changes' => 
      array (
      ),
       'casts' => 
      array (
      ),
       'classCastCache' => 
      array (
      ),
       'dates' => 
      array (
      ),
       'dateFormat' => NULL,
       'appends' => 
      array (
      ),
       'dispatchesEvents' => 
      array (
      ),
       'observables' => 
      array (
      ),
       'relations' => 
      array (
        'team' => 
        App\Team::__set_state(array(
           'fillable' => 
          array (
            0 => 'name',
          ),
           'connection' => 'mysql',
           'table' => 'teams',
           'primaryKey' => 'id',
           'keyType' => 'int',
           'incrementing' => true,
           'with' => 
          array (
          ),
           'withCount' => 
          array (
          ),
           'perPage' => 15,
           'exists' => true,
           'wasRecentlyCreated' => false,
           'attributes' => 
          array (
            'id' => 1,
            'name' => 'サッカー',
            'created_at' => NULL,
            'updated_at' => NULL,
          ),
           'original' => 
          array (
            'id' => 1,
            'name' => 'サッカー',
            'created_at' => NULL,
            'updated_at' => NULL,
          ),
           'changes' => 
          array (
          ),
           'casts' => 
          array (
          ),
           'classCastCache' => 
          array (
          ),
           'dates' => 
          array (
          ),
           'dateFormat' => NULL,
           'appends' => 
          array (
          ),
           'dispatchesEvents' => 
          array (
          ),
           'observables' => 
          array (
          ),
           'relations' => 
          array (
          ),
           'touches' => 
          array (
          ),
           'timestamps' => true,
           'hidden' => 
          array (
          ),
           'visible' => 
          array (
          ),
           'guarded' => 
          array (
            0 => '*',
          ),
        )),
      ),
       'touches' => 
      array (
      ),
       'timestamps' => true,
       'hidden' => 
      array (
      ),
       'visible' => 
      array (
      ),
       'guarded' => 
      array (
        0 => '*',
      ),
    )),
    1 => 
    App\Member::__set_state(array(
       'fillable' => 
      array (
        0 => 'name',
        1 => 'team_id',
      ),
       'connection' => 'mysql',
       'table' => 'members',
       'primaryKey' => 'id',
       'keyType' => 'int',
       'incrementing' => true,
       'with' => 
      array (
      ),
       'withCount' => 
      array (
      ),
       'perPage' => 15,
       'exists' => true,
       'wasRecentlyCreated' => false,
       'attributes' => 
      array (
        'id' => 2,
        'name' => 'Jiro',
        'team_id' => 2,
        'created_at' => '2020-04-14 15:10:53',
        'updated_at' => '2020-04-14 15:10:53',
      ),
       'original' => 
      array (
        'id' => 2,
        'name' => 'Jiro',
        'team_id' => 2,
        'created_at' => '2020-04-14 15:10:53',
        'updated_at' => '2020-04-14 15:10:53',
      ),
       'changes' => 
      array (
      ),
       'casts' => 
      array (
      ),
       'classCastCache' => 
      array (
      ),
       'dates' => 
      array (
      ),
       'dateFormat' => NULL,
       'appends' => 
      array (
      ),
       'dispatchesEvents' => 
      array (
      ),
       'observables' => 
      array (
      ),
       'relations' => 
      array (
        'team' => 
        App\Team::__set_state(array(
           'fillable' => 
          array (
            0 => 'name',
          ),
           'connection' => 'mysql',
           'table' => 'teams',
           'primaryKey' => 'id',
           'keyType' => 'int',
           'incrementing' => true,
           'with' => 
          array (
          ),
           'withCount' => 
          array (
          ),
           'perPage' => 15,
           'exists' => true,
           'wasRecentlyCreated' => false,
           'attributes' => 
          array (
            'id' => 2,
            'name' => '野球',
            'created_at' => NULL,
            'updated_at' => NULL,
          ),
           'original' => 
          array (
            'id' => 2,
            'name' => '野球',
            'created_at' => NULL,
            'updated_at' => NULL,
          ),
           'changes' => 
          array (
          ),
           'casts' => 
          array (
          ),
           'classCastCache' => 
          array (
          ),
           'dates' => 
          array (
          ),
           'dateFormat' => NULL,
           'appends' => 
          array (
          ),
           'dispatchesEvents' => 
          array (
          ),
           'observables' => 
          array (
          ),
           'relations' => 
          array (
          ),
           'touches' => 
          array (
          ),
           'timestamps' => true,
           'hidden' => 
          array (
          ),
           'visible' => 
          array (
          ),
           'guarded' => 
          array (
            0 => '*',
          ),
        )),
      ),
       'touches' => 
      array (
      ),
       'timestamps' => true,
       'hidden' => 
      array (
      ),
       'visible' => 
      array (
      ),
       'guarded' => 
      array (
        0 => '*',
      ),
    )),
  ),
))


Author: webmaster