質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.47%
Laravel

LaravelとはTaylor Otwellによって開発された、オープンソースなPHPフレームワークです。Laravelはシンプルで表現的なシンタックスを持ち合わせており、ウェブアプリケーション開発の手助けをしてくれます。

Laravel 5

Laravel 5は、PHPフレームワークLaravelの最新バージョンで、2014年11月に発表予定です。ディレクトリ構造がが現行版より大幅に変更されるほか、メソッドインジェクションやFormRequestの利用が可能になります。

Q&A

解決済

1回答

4134閲覧

「複数あるリレーション先のデータの最小値」からリレーション元をソートしたい

riz

総合スコア30

Laravel

LaravelとはTaylor Otwellによって開発された、オープンソースなPHPフレームワークです。Laravelはシンプルで表現的なシンタックスを持ち合わせており、ウェブアプリケーション開発の手助けをしてくれます。

Laravel 5

Laravel 5は、PHPフレームワークLaravelの最新バージョンで、2014年11月に発表予定です。ディレクトリ構造がが現行版より大幅に変更されるほか、メソッドインジェクションやFormRequestの利用が可能になります。

1グッド

1クリップ

投稿2019/05/02 13:40

前提・実現したいこと

リレーション先の値を使って親データをソートしようと考えています。

商品名を入力し、下図のような検索結果がでた場合に、98円のセブンイレブンが一番上にくるようにしたいイメージです。
イメージ説明

調べたところ、こちらのサイトの方法がヒットしたのですが、自分の環境での適応がうまくできなかったのでアドバイス頂きたいです。

データベースは下図のようにしており、
has many 中間テーブル
├ belongs to 商品 └belongs to 店名
└ has many 金額や数量
のような形をとっています。
イメージ説明

該当のソースコード

現在のコントローラ

php

1 $items = Item::with('middle.shop', 'middle.information') 2 -> where('name', 'LIKE', "%$query%") 3 -> get();

試したこと

joinでテーブルをつなぎ金額でソートしてからgroupBy()するようなことも試みたのですが、カラムが全て同じ階層になってしまうのでビューのテーブル作成がうまくできませんでした…

補足情報(FW/ツールのバージョンなど)

PHP Version 7.1.26
Laravel Framework 5.8.4

mpyw👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

Not exists 最適化狙いの戦略はどうでしょうか?

php

1class ItemShop extends Pivot 2{ 3 protected $table = 'item_shop'; 4 5 public function scopeOrderByLowestMoney(Builder $query): Builder 6 { 7 return $query 8 ->join('information', 'item_shop.id', '=', 'information.item_shop_id') 9 ->leftJoin('information as filter', function (JoinClause $join) { 10 $join->on('information.item_shop_id', '=', 'filter.item_shop_id'); 11 $join->on(function (JoinClause $join) { 12 $join->on('information.id', '>', 'filter.id'); 13 $join->on('information.money', '=', 'filter.money'); 14 $join->orOn('information.money', '>', 'filter.money'); 15 }); 16 }) 17 ->whereNull('filter.id') 18 ->select('item_shop.*') 19 ->orderBy('information.money') 20 ->orderBy('information.id'); 21 } 22 23 public function scopeWhereItem(Builder $query, ...$args): Builder 24 { 25 return $query 26 ->join('items', 'item_shop.item_id', '=', 'items.id') 27 ->where(function (Builder $query) use ($args) { 28 $query->setModel(new Item())->where(...$args); 29 }); 30 } 31 32 public function item(): BelongsTo 33 { 34 return $this->belongsTo(Item::class); 35 } 36 37 public function shop(): BelongsTo 38 { 39 return $this->belongsTo(Shop::class); 40 } 41 42 public function information(): HasMany 43 { 44 return $this->hasMany(Information::class); 45 } 46}

まず発想の転換が必要です。結果的に 表示したいレコードは item_shop テーブル行単位 なので,カスタムピボットクラス ItemShop が必要なことに注意しましょう。これに ItemShopInformation を後から Eager Loading で取ってくるという流れでいきましょう。
(複数商品が選択された場合,同じ店舗が商品数ぶんだけ現れてもいいと考える)

item_shop テーブルのソートのために,information を内部結合します。さらに繰り返して外部結合を行い,この際は 「金額がより高いもの」 または 「金額が同じだが情報IDがより大きいもの」 だけを対象として,結合相手が存在しなかったところには NULL を埋めます。そして,NULL が埋められたところだけを逆に残せば 「金額が最も安いもの」 だけが残ります。これをスコープとして定義しましょう。

items に関しては whereHas を使っても解決できますが,クロージャだと書きにくい上サブクエリだとパフォーマンスが劣化する可能性に繋がるので,こちらもついでにスコープとして定義しておきます。

ItemShop::with('item', 'shop', 'information') ->whereItem('items.name', 'like', '%' . addcslashes($query, '\_%') . '%') ->orderByLowestMoney() ->get();

LIKE 検索における \ _ % のエスケープも忘れずに。また,最安値が重複したときのために第二ソート軸として一意な id も入れておきましょう。


最初に実行されるSQL(参考)

sql

1select 2 `item_shop`.* 3from 4 `item_shop` 5 inner join 6 `items` 7 on `item_shop`.`item_id` = `items`.`id` 8 inner join 9 `information` 10 on `item_shop`.`id` = `information`.`item_shop_id` 11 left join 12 `information` as `filter` 13 on `information`.`item_shop_id` = `filter`.`item_shop_id` 14 and ( 15 `information`.`id` > `filter`.`id` 16 and `information`.`money` = `filter`.`money` 17 or `information`.`money` > `filter`.`money` 18 ) 19where 20 (`items`.`name` like ?) 21and `filter`.`id` is null 22order by 23 `information`.`money` asc, 24 `information`.`id` asc

投稿2019/05/03 04:19

編集2019/05/03 19:04
mpyw

総合スコア5223

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

mpyw

2019/05/03 05:42

まあ Not Exists 最適化があるといっても(プレフィクスマッチングではない)LIKE検索を使っている時点でパフォーマンス面はお察しなので,あまり期待はせず…
riz

2019/05/03 08:10 編集

丁寧なご回答ありがとうございます! 知らなかったことばかりで、非常に参考になりました。 また、エスケープやidによるソートまでご親切にありがとうございます。 頂いた方法を実際に組み込んでみたところ、orderByMoney()で見事に価格順に並べられたのですが、with()を通す際に検索結果数がヒット数×ヒット数となってしまい、上のバナナの例の順番で同じ結果が5つ並ぶような形になってしまいました。 スコープ関数からクエリービルダー、サブクエリまでほとんど理解できていないので、ひとまずキャッチアップしなが解決策を探してみます。
mpyw

2019/05/03 10:32

1つの item_shop に対して複数の information があるということでしょうか…?
mpyw

2019/05/03 10:42

(↑いや,それでも動くはず…なんでだろう)
mpyw

2019/05/03 10:56

ちょっと勘違いしてました,修正します。item_id ではなく item_shop_id で一意な結果を残すべきでした
mpyw

2019/05/03 11:47

大幅に加筆修正しました!
riz

2019/05/03 14:36

ありがとうございます! おかげさまで、思い描いていた通りの実装ができました。 まだ「何が何だか分かっていないコードがとりあえず動かせている」という状態ですので、数日かけて頂いた情報を参考に動きを勉強させてもらおうと思います。 ここまで丁寧に回答を頂けたのが凄くありがたいので、お気持ち程度ですが、公開メールアドレスにアマギフ投げ銭させて頂きます!
mpyw

2019/05/03 18:41

恐縮です!こちらこそありがとうございました。
mpyw

2019/05/03 18:58

(蛇足ですが,MySQL 8.0 以降または PostgreSQL の場合は,ウィンドウ関数を使うと「LEFT JOIN ... WHERE id IS NULL」の Not Exists 最適化狙いのクエリよりも高い可読性と高いパフォーマンスを両立できます。今回はあくまで MySQL 5.x 向けのクエリです)
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.47%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問