Search code examples
ruby-on-railsactiverecordrablsqlperformance

Join current_user's rate to each song


I need to show current user's rate of the song in index.

I have associated models:

class Song < ActiveRecord::Base
  has_many :votes, dependent: :destroy

  def user_rate(user)
    votes.find_by(user_id: user).try(:rate)
  end
end

class Vote < ActiveRecord::Base
  belongs_to :song, counter_cache: true
  belongs_to :user
end

votes table:

  create_table "votes", force: :cascade do |t|
    t.integer  "rate"
    t.integer  "user_id"
    t.integer  "song_id
  end

And index action in SongsController:

def index
  respond_with @songs = Song.includes(:votes).all
end

In songs/index.json.rabl I look for current user vote (each user can vote only once)

collection @songs
extends "songs/base"
attributes :thumb_cover
node(:current_user_rate){ |song| song.user_rate(current_user) }

but the song.user_rate(current_user) generates n+1 queries :(

  User Load (0.9ms)  SELECT  "users".* FROM "users"  ORDER BY "users"."id" ASC LIMIT 1
  Song Load (0.6ms)  SELECT "songs".* FROM "songs"
  Vote Load (0.8ms)  SELECT  "votes".* FROM "votes" WHERE "votes"."song_id" = ? AND "votes"."user_id" = 1 LIMIT 1  [["song_id", 1]]
  Vote Load (0.3ms)  SELECT  "votes".* FROM "votes" WHERE "votes"."song_id" = ? AND "votes"."user_id" = 1 LIMIT 1  [["song_id", 2]]

Is there a way I can joins current user's vote.rate column to each song in one query?


Solution

  • You'll be best diving into ActiveRecord Association Extensions:

    #app/models/song.rb
    class Song < ActiveRecord::Base
      has_many :votes, dependent: :destroy do
         def user_rate(user)
           find_by(user_id: user.id).try(:rate)
         end
      end
    end
    

    This would allow:

    @song   = Song.find params[:id]
    @rating = @song.votes.user_rate current_user #-> 5 or false
    

    If you didn't want to perform another query, you could try your hand at the proxy_association object which is accessible through the extension:

    #app/models/song.rb
    class Song < ActiveRecord::Base
      has_many :votes, dependent: :destroy do
         def user_rate(user)
           ratings = proxy_association.target.map {|f| [f.user_id, f.rate]}
           rating = ratings.select { |user_id, rate| user_id == user.id }
           rating.first[1] if rating.first
         end
      end
    end
    

    You'll be able to use:

    @song   = Song.find params[:id]
    @rating = @song.votes.try(:by, current_user) #-> 5 or false
    
    if @rating
     ...
    end
    

    Untested, but I've used proxy_association before in similar ways to this.