モデルに実装したメソッドを利用してorderByしたい
- 評価
- クリップ 2
- VIEW 2,768
Laravel 5.4 を使用したWebアプリを作成中です。
基本的な仕組みは出来たのですが、並べ替えについて質問です。
class Item extends Model
{
public function myPrice () {
if (\Auth::User()->role == 'admin') {
$result = $this->master_price;
} else {
$data = $this->prices()->where('user_id', \Auth::User()->id)->first();
$result = (is_null($data) ? $this->master_price : $data->price);
}
return $result;
}
public function prices()
{
return $this->hasMany(Price::class);
}
}
Controller内で、このモデルクラスのmyPrice()を元に「orderBy」をかけたく思います。
※追記ここから
サンプルにする為コードを単純化していたのですが、単純にしすぎていました。
実際は上記のようなリレーションを挟んでの処理になります。
「該当のリレーションデータが存在しない場合は自クラスのデータを使う」という処理が入ります。
(自クラスのデータは絶対に入っています)
後出し仕様になってしまって申し訳ありません。
※追記ここまで
$itemList = Item::where('aaa', 'bbb')->orderBy('updated_by', 'desc')->paginate(20);
とりあえず上記コードが動くことは確認しました。
が、orderBy('myPrice', 'asc')
にすると(当然ながら)動きません。
Controller側でmyPriceと同じ条件分岐をすればいい話なのですが、出来れば同じ処理を2つ書きたくありません。
Item::where('aaa', 'bbb')->get()
したデータを元に配列作成、それをソートする方法もあると思いますが、
そうした場合paginateが使えないのでは?と思っています。
何か良い方法があればお教え頂けると助かります。
-
気になる質問をクリップする
クリップした質問は、後からいつでもマイページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
クリップを取り消します
-
良い質問の評価を上げる
以下のような質問は評価を上げましょう
- 質問内容が明確
- 自分も答えを知りたい
- 質問者以外のユーザにも役立つ
評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。
質問の評価を上げたことを取り消します
-
評価を下げられる数の上限に達しました
評価を下げることができません
- 1日5回まで評価を下げられます
- 1日に1ユーザに対して2回まで評価を下げられます
質問の評価を下げる
teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。
- プログラミングに関係のない質問
- やってほしいことだけを記載した丸投げの質問
- 問題・課題が含まれていない質問
- 意図的に内容が抹消された質問
- 過去に投稿した質問と同じ内容の質問
- 広告と受け取られるような投稿
評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。
質問の評価を下げたことを取り消します
この機能は開放されていません
評価を下げる条件を満たしてません
質問の評価を下げる機能の利用条件
この機能を利用するためには、以下の事項を行う必要があります。
- 質問回答など一定の行動
-
メールアドレスの認証
メールアドレスの認証
-
質問評価に関するヘルプページの閲覧
質問評価に関するヘルプページの閲覧
+2
masaya_ohashiさんの回答を元に試行錯誤した結果、解決しました。
一部エラー等出ていたのでscopeメソッドの中身だけ変更して動作するようにしました。
//Itemクラス内
public function scopeOrderByMyPrice($query) {
if (\Auth::User()->role == 'admin') {
return $query->orderBy('master_price', 'asc');
}
else {
$query
->selectRaw('COALESCE((SELECT price FROM prices WHERE prices.item_id = items.id and prices.user_id = ? limit 1), items.master_price) as myPrice', [\Auth::User()->id])
->addSelect('items.*')
->orderBy('myPrice', 'asc')
;
return $query;
}
}
変更した所
- メソッド名を「scopeOfOrderByMyPrice」から「scopeOrderByMyPrice」へ(Ofが不要)
- admin時は直接並べ替え
- 「:」を使用するプリペアドステートメントだとエラーが出たので「?」を使うように変更
- joinだとpricesのデータが無い場合アイテムデータも表示されない、leftJoinだとアイテムデータが重複表示される、groupByは何故かエラーが出る……ので、価格だけselectで追加取得を試みてCOALESCEで反映
- COALESCEはraw内でしか使えないようなのでselectからselectRawへ変更
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
checkベストアンサー
+1
scopeを使ってはどうでしょう?本来は独自のwhere句を書くための構文ですが、$queryを受け取れるのでこういった使い方もできるかもしれません(未検証)。
class Item extends Model
{
// roleによって使用するカラム名を切り分ける
private function myPriceColumnName() {
if (\Auth::User()->role == 'admin') {
return 'master_price';
} else {
return 'my_price';
}
}
public function myPrice () {
$column = $this->myPriceColumnName();
return $this->$column; // カラム名を変数を使って指定
}
public function scopeOrderByMyPrice($query) {
return $query->orderBy($this->myPriceColumnName(), 'asc'); // scope処理でorderByを付ける
}
}
# 使い方
$itemList = Item::where('aaa', 'bbb')
->orderByMyPrice() // scopeXXXという名前のメソッドはクエリビルダでこうやって呼べる
->paginate(20);
仕様変更後
正直仕様が複雑すぎて、PHP側の処理とSQL側の処理を共通化して書くとか無理です…同じ動作をするように力技でSQLを書くしかないかと思います。
全然動作確認してないので、このまま正しく動くかどうかはわかりませんが…joinとかDB::rawのサブクエリを使えばどうにかできるかと思います。
class Item extends Model
{
public function myPrice () {
if (\Auth::User()->role == 'admin') {
$result = $this->master_price;
} else {
$data = $this->prices()->where('user_id', \Auth::User()->id)->first();
$result = (is_null($data) ? $this->master_price : $data->price);
}
return $result;
}
public function prices()
{
return $this->hasMany(Price::class);
}
public function scopeOrderByMyPrice($query) {
if (\Auth::User()->role == 'admin') {
$query
->select(
'items.*', // itemsテーブルのカラム全てと
'items.master_price AS my_price' // master_priceをmy_priceとして追加
);
}
else {
$query
->join(\DB::raw('(SELECT item_id, price FROM prices WHERE user_id = :user_id) AS prices', ['user_id' => \Auth::User()->id]), 'prices.item_id', 'items.id') // pricesからuser_idの一致するレコードを引き出し、itemsのidと紐付ける
->groupBy('prices.item_id') // pricesのitem_idでグルーピングすることでpricesの結合を1件に絞り込む
->select(
'items.*', // itemsテーブルのカラム全てと
'COALESCE(prices.price, items.master_price) AS my_price' // joinしたpricesのpriceがあればpriceを、なければmaster_priceをmy_priceとして追加
);
}
return $query->orderBy('my_price', 'asc'); // my_price順でソート
}
}
使い方は仕様変更前と同じです。Model内の実装だけが変わります。
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
15分調べてもわからないことは、teratailで質問しよう!
- ただいまの回答率 88.37%
- 質問をまとめることで、思考を整理して素早く解決
- テンプレート機能で、簡単に質問をまとめられる
質問への追記・修正、ベストアンサー選択の依頼
masaya_ohashi
2017/11/20 18:30 編集
疑問ですが、pricesがhasManyになっているのはどうしてでしょう?myPriceは1個しかデータがない想定で動作しているようですが、myPriceは「複数pricesレコードがあった場合、先頭1件を使う」という仕様なのでしょうか?
masaya_ohashi
2017/11/20 18:35
もう一点、データベースはMySQLですか?
sakura_hana
2017/11/20 19:28
1つのアイテムに対し、複数のユーザーが異なる価格を付ける為このような形としています(1ユーザーが1つのアイテムに付ける価格は1つだけです)。データベースはMySQLです。