2715


TeamモデルとPlayerモデルがhas_manyの関係でpolymorphicなScoreモデルを持つというような構成で
チームやプレイヤーに得点を付けることができるシステムを作成しています。

チームに所属しているプレイヤーの一覧を表示するときに Scoreモデルが持つvalueカラムの合計値を使ってソートをかけたいです。

Playerモデルに以下のスコープを作成しました。

[code]

    scope :sorted, -> {
      joins(:scores)
      .group("players.id")
      .order("sum(scores.value) desc")
    }
    
[/code]

しかし、これだとひとつもスコアを付けられていない要素が含まれずに スコアを付けられている要素の中でソートが行われてしまいます。

スコアが付けられていない要素に対してはデフォルトで０点ということにして 全ての要素を取得したいのですがどうすればよいでしょうか？


CC BY-SA 3.0
2014-12-27T16:02:57.713
2707
2015-01-15T14:57:12.413
2015-01-15T14:57:12.413
4978
5840
question
ruby-on-rails sort rails-activerecord
ActiveRecord4でpolymorphicなScoreの値の合計値でソートしたい。


例によって、より良い回答が付くまでの繋ぎとして、粗い回答を付けておきます。

要求されている操作は SQL 的には `LEFT OUTER JOIN` という操作です。 検索する際はこれをキーワードにすると良さそうです。

検索の結果、こんな記事: [Railsで生SQLを書かずにLEFT
JOINする方法](http://qiita.com/unosk/items/403d0fdaf91ca86aa099) が見つかりました。
こちらの記事によれば `includes` を使う、もしくは [squeel](https://github.com/activerecord-
hackery/squeel) を使うということのようです。


CC BY-SA 3.0
2014-12-27T17:06:01.220
2712
2014-12-28T01:46:54.227
2014-12-28T01:46:54.227
208
208
2707
answer


似たようなモデル構造のプロジェクトがあったので、実際に動かしながら試してみました。

回答からまず書くと、たぶんこんな感じでいけるんじゃないかと思います。

[code]

    scope :sorted, -> {
      joins("LEFT OUTER JOIN scores ON scores.scorable_id = players.id AND scores.scorable_type = 'Player'")
      .group("players.id")
      .order("COALESCE(SUM(scores.value), 0) DESC")
    }
    
[/code]

`scorable` というのはこちらの想像で付けた名前です。ここは実際のカラムの名前に変更してください。  
ちなみにクラス定義でいうと、こんな定義をイメージしています。

[code]

    class Player
      has_many :scores, as: :scorable
    
    class Score
      belongs_to :scorable, polymorphic: true
    
[/code]

あと、`COALESCE` というのはPostgresでNULL（=スコアが付けられていない要素）を2番目の引数の値（=0）に変更する関数です。  
使用するRDBMSによっては別の関数を使うかもしれません。

補足事項として、Railsで外部結合（LEFT OUTER JOIN）を使う場合は、`includes` + `references`
をよく使うのですが、これを使うと scores のカラムも一緒に取得しようとするためにSQLエラーが起きます。

コードで書くとこんな感じです。（エラーが起きる）

[code]

    scope :sorted, -> {
      includes(:scores)
      .references(:scores)
      .group("players.id")
      .order("COALESCE(SUM(scores.value), 0) DESC")
    }
    
[/code]

なので、今回は（極力避けたい）生のSQLでテーブルをJOINしています。

あと、もう一点だけ。  
KoRoN さんも書いているように、[Squeel](https://github.com/activerecord-
hackery/squeel)というgemを使うとJOINをスッキリ書けます。

[code]

    scope :sorted, -> {
      joins{ scores.outer }
      .group("players.id")
      .order("COALESCE(SUM(scores.value), 0) DESC")
    }
    
[/code]

以上、ご参考までに。


CC BY-SA 3.0
2014-12-27T21:20:13.703
2715
2014-12-27T22:44:21.257
2014-12-27T22:44:21.257
85
85
2707
answer