【Laravel】hasOne / belongsTo について!

Laravel
この記事は約13分で読めます。

はじめに

ORM (Object Relational Mapper) = Eloquent は、Laravel を使う最強の利点といっても過言ではありません!

Laravel を採用する上では必ずマスターしたい機能ですね!

クロたん
クロたん

めちゃくちゃマスターするにゃ!

わい
わい

気を付けないといけないのは、Laravel の ORM は便利だけど、裏ではしっかりとクエリが流れているから、必ずどんなクエリが流れているかは確認してね!
Laravel 含めて世に出ているフレームワークは完ぺきではありません。
必ずパフォーマンス部分は意識する習慣はつけてね、クロたん・・・!

クロ
クロ

うるさいにゃ~~~。

環境

  • php version
php -v
PHP 7.4.33
  • Laravel version
php artisan -V
Laravel Framework 8.83.27

準備!!

記事のコマンドはすべて正常に動くか検証済みです。
かつ、最小限の構成をとっていますので、ぜひとも手を動かしながらやってみてください!

  • migration 作成( DDL は1つのファイルにまとめます。)
php artisan make:migration create_tables
<?php

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

class CreateTables extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        // userテーブルも使うが、デフォルトであるので記述しない

        // ブログテーブル
        Schema::create('blogs', function (Blueprint $table) {
            $table->id('blog_id');
            $table->unsignedBigInteger('category_id');
            $table->unsignedBigInteger('user_id');
            $table->string('title');
            $table->timestamps();
        });

        // カテゴリテーブル
        Schema::create('categories', function (Blueprint $table) {
            $table->id('category_id');
            $table->string('category_name');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('blogs');
        Schema::dropIfExists('categories');
    }
}
  • migration 実行
php artisan migrate:fresh

※確実に実行できるように fresh を付けています。

  • Model 作成
php artisan make:model Blog && php artisan make:model Category
  • それぞれの Blogモデル Categoryモデル に対して、
    primary key となるカラム名を $guarded に定義する。

▼Blogモデル

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Blog extends Model
{
    use HasFactory;
    // ↓ここの部分(createメソッドで挿入できるようにする。)
    protected $guarded = ['blog_id'];
    // ↓プライマリキーを明示的に指定する。(後で説明)
    protected $primaryKey = 'blog_id';

    /**
     * プライマリキーを返す(後で説明)
     * @return string
     */
    public function get_primary_key(): string
    {
        return $this->primaryKey;
    }
}

▼Categoryモデル

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    use HasFactory;

    // ↓ここの部分(createメソッドで挿入できるようにする。)
    protected $guarded = ['category_id'];
    // ↓プライマリキーを明示的に指定する。(後で説明)
    protected $primaryKey = 'category_id';

    /**
     * プライマリキーを返す(後で説明)
     * @return string
     */
    public function get_primary_key(): string
    {
        return $this->primaryKey;
    }
}

▼Userモデル

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    // ↓プライマリキーはデフォルトのIdを使用する。(標準搭載のテーブルはいじらない方がいい。)
    protected $primaryKey = 'id';
    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    /**
     * プライマリキーを返す(後で説明)
     * @return string
     */
    public function get_primary_key(): string
    {
        return $this->primaryKey;
    }
}

Laravel ではテーブルのプライマリキーは暗黙的に [id] となっている
しかし、実務において [id] では、テーブル構造が複雑になってくると、
カラム名の衝突 や なんのId? といった具合にわかりにくさが増してしまうので、明示的に指定してあげる。

また、後に説明する hasOne(引数1, 引数2, 引数3) の 引数2,3 が混乱を招きやすいので、
分かりやすいように get_primary_key() を記述しておきます。

  • テストデータを tinker を用いて作成する
php artisan tinker
  • ↓をコピペで流す!
\App\Models\User::create([
    'name' => 'ユーザー1',
    'email' => 'hogehoge@example.com',
    'password' => bcrypt('password'),
]);

\App\Models\Category::create([
    'category_name' => 'カテゴリ1'
]);

\App\Models\Blog::create([
    'category_id' => 1,
    'user_id' => 1,
    'title' => 'タイトル1',
]);

もちろん、Seeder でもいいんですが、そこの説明をすると長ったらしくなるので、tinker で代替。

クロ
クロ

tinker はこういうとき便利だにゃ~。

  • Route 定義
Route::get('', [\App\Http\Controllers\IndexController::class, 'index']);
  • Controller 作成
php artisan make:controller IndexController

本題: hasOne / belongsTo

hasOne / belongsTo の考え方は 1:1 です。
これらの違いは、

そのModelが持つデータは、対等関係で何と1:1 の関係を持っているのかを表すのが hasOne
そのModelが持つデータは、なにの操作で生成されるのか、を表すのが belongsTo

こう説明されても、実運用でどうあてはめていけばいいのかが分からないというのが、
分かりやすい具体例の落とし穴ではありますが、正直、慣れの部分が非常に大きいです。

上述の例で具体的にいうと、

Blog は Category を1つ持つことができ、そのデータ間で主従関係はないので hasOne
Blog は User によって作成されるので、belongsTo

次節から具体的なコードを見て、感覚をつかんでください!

hasOne

  • \App\Models\Blog に以下を記載
public function category(): hasOne
{
    $category_model = new \App\Models\Category();
    # ↓リレーション先のモデル( Category )のプライマリキーが第2引数に来てる
    return $this->hasOne($category_model, $category_model->get_primary_key(), $this->primaryKey);
}
わい
わい

必ず、namespace 下に

use Illuminate\Database\Eloquent\Relations\HasOne;

↑を記述してください。

  • IndexController に以下を記載
public function index()
{
    // blog_id = 1 を条件にまずデータを取得している。ここも重要。
    $blog_data = \App\Models\Blog::find(1);
    // 取得してきた blogデータ の category_id を使ってリレーションを形成している
    $category_data = $blog_data->category;
}

->category の部分が、Modelで定義した関数部分となる。

  • ↓実際に流れているクエリを確認
$blog_data->category()->dd();
select * 
from `categories` 
where `categories`.`category_id` = 1
and `categories`.`category_id` is not null
クロ
クロ

categoriesテーブルから、category_id を条件に付与して取得してるんだけなんだな~。

belongsTo

  • \App\Modes\Blog に以下を記載
public function user(): belongsTo
{
    $user_model = new \App\Models\User();
    # ↓リレーション先のモデル( User )のプライマリキーが第3引数に来てる
    return $this->belongsTo($user_model, 'user_id', $user_model->get_primary_key());
}
ケン
ケン

必ず、namespace 下に

use Illuminate\Database\Eloquent\Relations\BelongsTo;

↑を記述する!

※hasOne と belongsTo で、引数が逆になる。ここが厄介。。。

  • IndexController に以下を記載
public function index()
{
    // blog_id = 1 を条件にまずデータを取得している。ここが肝。
    $blog_data = \App\Models\Blog::find(1);
    // 取得してきた blogデータ の user_id を使ってリレーションを形成している
    $user_data = $blog_data->user;
}

->user の部分が、Modelで定義した関数部分となる。

  • ↓実際に流れているクエリを確認
$blog_data->user()->dd();
select * 
from `users` 
where `users`.`id` = 1

さいごに

hasOne と belongsTo のリレーションを解説しました。

クエリの確認で dd() していたように、追加で↓のようにどんどん繋げられるので応用も効きます!

$blog_data->user()->where('name', 'ユーザー名')->get();

使わない選択肢はない!これを機会に、マスターしましょう。

当ブログでは、他にも hasMany / belongsToMany を紹介していますのでそちらもぜひご覧ください。

作成したファイルを削除 (不要ファイルが残るのが気持ち悪い人向け)※Linux コマンドで削除しています。

cd /path/to/project

/path/to/project には、artisanファイル が格納されているディレクトリパスを指定する。

  • Controller 削除
rm app/Http/Controllers/IndexController.php
  • Model 削除
find $(pwd)/app/Models/ -type f -not -name "User.php" | xargs rm
  • migrationファイル 削除
 find $(pwd)/database/migrations/ -name "*create_table*" | xargs rm
  • Route定義は行削除(これは Linuxコマンド ではなく普通に削除)
Route::get('', [\App\Http\Controllers\IndexController::class, 'index']);
クロたん
クロたん

またね~~~~。

コメント