🙆‍♀️

[Laravel] EloquentリレーションのBelongsToMany(多対多)を使う過程でキーが上書きされる件の対処

2024/02/13に公開

前提

テーブル構成

  • usersテーブル
  • postsテーブル
  • post_userテーブル(中間テーブル)
    • idカラムを持っている

事象

public function posts(): BelongsToMany
{
    return $this->belongsToMany(Post::class);
}

のとき、

User::posts()->get();

のようにリレーションを呼ぶと
posts.idpost_user.idで上書きされてしまう事象が起きる。
※ withPivot()などで取得カラムを指定しても発生。

原因

(Eloquent)BuilderのbelongsToMany()メソッドではinner joinを内部的に行なっているため。

※ 詳細

dd($posts->toSql());

の結果が

select `posts`.`*` inner join `post_user` on ...

であり、joinの際に同一名のカラムはMySQL側で上書きされるため。

対応

対応①: Laravelライクな対応

JOINをさせなければ良いので、Eloquentの通常的なリレーションを呼べば発生しないはず。

$user->postUser()->post()->get();

-> おそらくN+1個のクエリが発行されてしまう。

select * from post_user where user_id = X;
select * from posts where id = 1;
...

※ 本記事は対応②を実施した備忘録なので、今回は未検証です。

対応②: belongsToManyっぽいメソッドを作る

これを避けるために、なんとかデフォルトのbelongsToManyっぽくふるまう関数を作成することで対応した。

public function posts(): Builder
{
    return Post::join('post_user', 'posr_user.post_id', '=', 'posts.id')
        ->select('posts.*', 'post_user.sort')
        ->where('post_user.user_id', $this->id)
        ->orderBy('post_user.sort', 'asc');
}

メリット

  • 一応Post.php側のモデルクラスインスタンスに、belongsToMany withPivotを使ったときと同じ値の入り方をしてくれる。
  • 返り値はBuilderなので、他のscopeなどとチェーンできる。
  • クエリの発行数が抑えられる(はず。なんか最近N+1問題が解消しつつあるみたいな情報を見た気もするので違ったら教えてください。)

デメリット

  • これ自体がスコープ関数ではないので最初に使わなければならない。scopeにするならPost.php側に作って、User側からはそれを呼ぶだけになるが、どちらにせよ依存の謎矢印問題が起きるし、変なstatic関数がPost側にできる方が気持ち悪いので今回はUser側においた。
  • 変更に弱い。
  • PostテーブルのEloquent Builderを作成する関数がUser側にできてしまいやや密結合的。

一旦これで乗り切ろうと思います。何かいい方法があれば教えてください。

Discussion