クマのブログ

つまづいたところ、学びを書いていきます

【Laravel】リレーション(1対1)

前提

背景

  • 業務中、Laravelのリレーションを使った処理を書くことに。

  • ただ、実装中にあまり理解が追い付いていないことに気づき、改めて基礎を学習

  • 書いてあることはほぼマニュアルの内容ですが、あまりにも悔しすぎたので同じ過ちを繰り返さないようにアウトプット

  • テーブルとかカラムの前提はマニュアルに準じます。

参考

公式マニュアル

理解不足箇所

プロジェクトのソースコードを読んでて思ったことは大きく分けて以下2点。

  • アサインしているプロジェクトのbelongsto, hasOne, hasMany, belongsToManyが何を意味しているか分からない

  • 上記各メソッドの理解はできても、とっさにテーブルの関係性を理解し、正しくメソッドを使い分けることができない

 →実際にプロジェクトのソースコードを読んで 「1対多」の関係で実装しなければいけない箇所を「多対多」で実装しようとして5時間近く溶かしました

上記二点をメインでアウトプットしていく

※本当はポリモーフィズムのリレーションもあるんですが、別でアウトプット

内容

前提としてリレーションには大きく分けて3つの関係性がある

  • 1対1
  • 1対多
  • 多対多

今回は「1対1」を重点的にやっていく

1対1

1つのテーブルの要素に対して、1つの別テーブルの要素が従属している状態。例えば以下状況。

  • とあるお店の会員登録システムがある。

  • 会員登録には電話番号が必要である時、システム内で電話番号が全く同じ人がいてはいけないので、必然的に1ユーザーに対して、1つの電話番号が紐づくことになる。

また逆も同じ。(1つの電話番号に対して、1ユーザーが紐づく)

この状況が1対1の関係

前提

  • データベース内にはUserテーブルとPhoneテーブルが分かれており、各テーブルのカラムは以下の通り

↓Userテーブル

カラム名 属性
id int
name string

↓Phoneテーブル

カラム名 属性
id int
user_id int
number string
  • モデルも各テーブルごとに準備

実装方法

  • UserモデルからPhoneモデルを参照したい(=値を取得したい)のであればUserモデルの記述は以下の通り書いてリレーションを張ることができる。
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public function phone()
    {
        return $this->hasOne(Phone::class);
    }
}
  • 実際に値を取得する際は以下の通り。ここではControllerで値を取得している
$phone = User::find(1)->phone; // Phoneテーブルの1行目のデータが取得できる
注意点(外部キーについて)
  • ここで、リレーションを張るには外部キーが必要であるということを思い出してほしい

  • Eloquantではモデル名に基づいて外部キーを自動で設定してくれるという便利な機能がある

↓マニュアル引用

Eloquentは、親モデル名に基づきリレーションの外部キーを決定します。 この場合、Phoneモデルは自動的にuser_id外部キーを持っているとみなします。

  • 今回の場合、Eloquantは

「Userモデルでリレーションを張るhasOneが実装された!

じゃあ、Phoneモデル内に「user(モデル名)」と「_id(接続詞。勝手に設定する)」を合わせた外部キー"user_id"カラムがあるはずだ!」

と探し、見事Postモデル内に"user_id"が見つかったらリレーションを張る、ということ

  • ただ、開発都合上Postテーブル内に「モデル名+_id」のカラムを設定しない場合もある。

そんな時は以下のようにhasOneでリレーションを張る際に第2引数にリレーション先の外部キーを指定することで、正常にリレーションを張ることができる。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public function phone()
    {
        return $this->hasOne(Phone::class), 'foreign_key' ; 
    }
}

例えば、PhoneモデルとUserモデルとのリレーションを張るにあたって、Phoneモデルが持つ外部キーがuser_tel_idであれば、以下のように書く。

        return $this->hasOne(Phone::class), 'user_tel_id' ; 
SQLで考えるとわかりやすい
  • EloquantデフォルトのSQL文 SELECT * FROM user, phone WHERE user.id = phone.user_id.

  • 第2引数を指定した場合 SELECT * FROM user, phone WHERE user.id = phone. user_tel_id.

グレーハッチング部が変化していると考えればOK

逆の参照も同じ
  • 逆にPhoneモデルからUserモデルを参照したい(=値を取得したい)ならば、hasOneではなく、belongsToを使用し、参照先を変更することで実装できる。

所感

  • 今回の一番の収穫は外部キーがEloquant設定値でなくても外部キーを指定すれば、問題なくリレーションを張れることを知ったことです。

  • また、なじみ深いSQLに置き換えることでEloquantで何してくれているのか理解できたのも大きかった。

  • 本当は1対多、多対多も一緒にまとめようとしたんだが、量が多いので、分けます。

※以下は自分の振り返り用の反省なので、読まなくてOKです。

  • 今回、私が「駆け出しエンジニア」だとしてもエラー規模に対して工数をかけすぎたと思った。

効率的なエラーとの向き合い方として「うまくいかない原因を特定する」ことが大事。

方法は様々だが例えば以下のように大きなところから小さなところへ原因を特定する作業が必要

  • 原因はソースコードか、データベースか、はたまた別のツールか

  • Controllerか、Modelか、Viewか、Middlewareか

  • ベースであるLaravel本体か、ライブラリ特有のエラーか

  • 原因特定のための手段としては「小さく実行・デバック」が有効

→例えば二重配列の処理などでエラーが発生したら、一重配列でも同じエラーが発生するか否かで原因特定ができるかもしれない

  • とはいえ、今回の経験を通して得たものは以下二つ。

  →三か月後、半年後に「こんなエラーでつまづいていたのか」と振り返り、成長を感じられるモチベーションの糧になる。

  • 今後の伸びしろが非常に大きいこと

  →「自分はまだまだだ!なぜなら、このエラー対応にも長時間かけたのだから」と謙虚にひたむきに学び続けるモチベーションになる。