🦣

Laravel11のバージョンアップで発生したSQLのエラーを解決した

2024/07/29に公開

はじめに

Laravel10で動作している基幹システムをLaravel11にバージョンアップしました!
その際下記のようなエラーが発生したので調査して解決したのでなせ発生したのか纏めてみようと思います

local.ERROR: SQLSTATE[42S21]: Column already exists: 1060 Duplicate column name 'id'

以前に業務委託の方に対応してもらったシステムと同じで、大規模なバージョンアップとならないように、定期的なメンテナンスを意識するようになりました
https://zenn.dev/levtech/articles/58a7af6cf42f5c

結論から

  • EagerLoading時のlimit句の仕様が変わってた[1]
  • Laravel11からリレーションで取得するときにlimit条件を追加すると、上記の仕様変更により発行されるクエリが変わる
  • さらに、リレーションの定義のところでJoin句を使用することが、Laravel側の想定外の使い方だったけど、システム内ではやってしまっていた

調査と解決の経緯

エラーは下記のリレーション取得時に発生している

Models/Store.php
public function trouble_shooting_relations_limited()
{
    return $this->hasMany(TroubleShootingRelation::class)
            ->join('trouble_shootings', function ($join) {
                $join->on('trouble_shootings.id', '=', 'trouble_shooting_relations.trouble_shooting_id')
                    ->whereNull('trouble_shootings.deleted_at');
            })
            ->orderBy('trouble_shootings.updated_at', 'desc')
            ->limit(10);
}

上記の処理で発生するクエリを確認する

select
    `laravel_table`.*,
    @laravel_row := if(
        @laravel_group = `store_id`,
        @laravel_row + 1,
        1
    ) as `laravel_row`,
    @laravel_group := `store_id`
from
    (
        select
            @laravel_row := 0,
            @laravel_group := 0
    ) as `laravel_vars`,
    (
        select
            *
        from
            `trouble_shooting_relations`
            inner join
                `trouble_shootings`
            on  `trouble_shootings`.`id` = `trouble_shooting_relations`.`trouble_shooting_id`
            and `trouble_shootings`.`deleted_at` is null
        where
            `trouble_shooting_relations`.`store_id` in(?)
        and `trouble_shooting_relations`.`deleted_at` is null
        order by
            `trouble_shooting_relations`.`store_id` asc,
            `trouble_shootings`.`updated_at` desc
    ) as `laravel_table`
having `laravel_row` <= 10 order by `laravel_row`;

ん、、!?
なんかlaravel_tableとか@laravel_rowとか@laravel_groupとか見慣れないけど、こんなクエリになるんだっけ🤔
でもこれ、mysqlの変数に代入するようになっているから発生している可能性ありそう

バージョンアップ前と比較する

リリースノートを確認

Laravelのソースコードで比較

リレーション内で使用しているメソッドを順々に確認したところ、limit句のみ変更されていた

こちらがLaravel10で処理されるlimit句
https://github.com/illuminate/database/blob/4ebb085d644da93097b9bd6cf6876f3f7af6c5cc/Query/Builder.php#L2549-L2564

こちらがLaravel11で処理されるlimit句
https://github.com/illuminate/database/blob/a65ad7f719458a670787078b5cd396c8694cabce/Eloquent/Relations/HasOneOrMany.php#L483-L498

処理内容に若干変更が加わっているし、なんなら呼び出されるClassもBuilderからHasOneOrMany変わっている!!

試しにlimit句をコメントアウトして実行してみるとエラーが起きない
ということは、リレーション取得時にlimit句が使えなくなったってことになる???
そんな訳ないか、そうなると、もっとエラーが多発していると思うんだけどな

リレーション処理でjoinってする?

Laravel側で変更があったにせよ、大部分では問題になっていないのでシステム側に問題があるのかもしれない
ということで問題の処理を再掲

return $this->hasMany(TroubleShootingRelation::class)
            ->join('trouble_shootings', function ($join) {
                $join->on('trouble_shootings.id', '=', 'trouble_shooting_relations.trouble_shooting_id')
                    ->whereNull('trouble_shootings.deleted_at');
            })
            ->orderBy('trouble_shootings.updated_at', 'desc')
            ->limit(10);

そういえばリレーションでjoinする必要あるっけ?
中間テーブルを経由したテーブルの情報を取得したい場合って、HasOneThroughHasManyThroughを使うんじゃなかったっけ?

このリレーションを使っている部分のソースコードを読んでみても、やりたいことはLaravelで用意している機能でできそうなので、当時の実装担当者がこの機能を知らずに実装したってことなのかな
Laravel側としてもリレーション定義の中でjoin句を書かれることは想定していなかったんじゃないかと思う

処理を書き換えて解決

というわけで、実際にやりたいことを確認しながら、こんな感じに書き換えて無事にエラーは解消されました!

Models/Store.php
public function trouble_shooting_relations_limited()
{
-    return $this->hasMany(TroubleShootingRelation::class)
-            ->join('trouble_shootings', function ($join) {
-                $join->on('trouble_shootings.id', '=', 'trouble_shooting_relations.trouble_shooting_id')
-                    ->whereNull('trouble_shootings.deleted_at');
-            })
+    return $this->belongsToMany(TroubleShooting::class, TroubleShootingRelation::class, 'store_id', 'trouble_shooting_id')
            ->orderBy('trouble_shootings.updated_at', 'desc')
            ->limit(10);
}

最後に

メジャーバージョンを上げると、後方互換のない変更があったりしますが大抵はリリースノートとかに書いてあると思うけど、今回の変更は記載なかったのでLaravelのissueやPRを探したりしてどんな変更が加わったのかわかりますが記載しておいて欲しいなって思いました!
それでも、システムの悪い部分を改善できたのでよかったです!定期的なバージョンアップは大事ですね!

脚注
  1. 元々パッケージとして提供されていたがLaravel11からは機能として統合されたみたい ↩︎

レバテック開発部

Discussion