Is there a simpler way to return a belongs_to relation of an ActiveRecord result set? - ruby-on-rails

I have two models:
Answers:
belongs_to: user
User:
has_many: answers
Is there a way in Ruby or Rails to do the following in one go instead of creating an array and pushing the needed object into it?
def top_experts
answers = Answer.where(some constraints)
users = []
answers.each do |answer|
users << answer.user
end
users
end

You can user joins
def top_experts
Answer.where(some constraints).includes(:user).collect{|x| x.user}
# Will return an Array of users
end
EDIT:
Use includes for eager loading. It will reduce the no of queries executed to get user.

Hi you can use a select clause to select what should be returned along with the where clause
Vote.select(user_id).where(some contraints)

Use sub-query to get users:
User.where( :id => Answer.where(some constraints).select(:user_id) )
Refer: subqueries in activerecord

Related

Get array of fields from a belongs_to association?

I have a object User and object Post. A User has_many Posts and a Post belongs_to a User.
I can access the posts a user has using syntax like:
posts = #user.posts
However, I want to generate an array of ids of the posts a user owns, without looping through each one. Ideally, I'd like to do this:
ids = #user.posts.id
However, I get an error, saying that id is an invalid method for ActiveRecord--etc etc...
Is there any way to neatly do this without having to do a loop like:
ids = []
posts = #user.posts
posts.each |post| do
ids << post.id
end
Rails provides a native method for this:
#user.post_ids
Essentially, you can refer to the ids of a has_many association using <singular association name>_idspattern.
Try with ids = posts.map(&:id) or ids = posts.map{|p| p.id }
You can also try with ids = posts.pluck(:id)
You Can also use collect.
Collect returns the Array.
ids = #user.posts.collect(&:id)
try this :
#ids = #user.posts.map{|x| x.id }
Hope it will help.

Use Ruby's select method on a Rails relation and update it

I have an ActiveRecord relation of a user's previous "votes"...
#previous_votes = current_user.votes
I need to filter these down to votes only on the current "challenge", so Ruby's select method seemed like the best way to do that...
#previous_votes = current_user.votes.select { |v| v.entry.challenge_id == Entry.find(params[:entry_id]).challenge_id }
But I also need to update the attributes of these records, and the select method turns my relation into an array which can't be updated or saved!
#previous_votes.update_all :ignore => false
# ...
# undefined method `update_all' for #<Array:0x007fed7949a0c0>
How can I filter down my relation like the select method is doing, but not lose the ability to update/save it the items with ActiveRecord?
Poking around the Google it seems like named_scope's appear in all the answers for similar questions, but I can't figure out it they can specifically accomplish what I'm after.
The problem is that select is not an SQL method. It fetches all records and filters them on the Ruby side. Here is a simplified example:
votes = Vote.scoped
votes.select{ |v| v.active? }
# SQL: select * from votes
# Ruby: all.select{ |v| v.active? }
Since update_all is an SQL method you can't use it on a Ruby array. You can stick to performing all operations in Ruby or move some (all) of them into SQL.
votes = Vote.scoped
votes.select{ |v| v.active? }
# N SQL operations (N - number of votes)
votes.each{ |vote| vote.update_attribute :ignore, false }
# or in 1 SQL operation
Vote.where(id: votes.map(&:id)).update_all(ignore: false)
If you don't actually use fetched votes it would be faster to perform the whole select & update on SQL side:
Vote.where(active: true).update_all(ignore: false)
While the previous examples work fine with your select, this one requires you to rewrite it in terms of SQL. If you have set up all relationships in Rails models you can do it roughly like this:
entry = Entry.find(params[:entry_id])
current_user.votes.joins(:challenges).merge(entry.challenge.votes)
# requires following associations:
# Challenge.has_many :votes
# User.has_many :votes
# Vote.has_many :challenges
And Rails will construct the appropriate SQL for you. But you can always fall back to writing the SQL by hand if something doesn't work.
Use collection_select instead of select. collection_select is specifically built on top of select to return ActiveRecord objects and not an array of strings like you get with select.
#previous_votes = current_user.votes.collection_select { |v| v.entry.challenge_id == Entry.find(params[:entry_id]).challenge_id }
This should return #previous_votes as an array of objects
EDIT: Updating this post with another suggested way to return those AR objects in an array
#previous_votes = current_user.votes.collect {|v| records.detect { v.entry.challenge_id == Entry.find(params[:entry_id]).challenge_id}}
A nice approach this is to use scopes. In your case, you can set this up the scope as follows:
class Vote < ActiveRecord::Base
scope :for_challenge, lambda do |challenge_id|
joins(:entry).where("entry.challenge_id = ?", challenge_id)
end
end
Then your code for getting current votes will look like:
challenge_id = Entry.find(params[:entry_id]).challenge_id
#previous_votes = current_user.votes.for_challenge(challenge_id)
I believe you can do something like:
#entry = Entry.find(params[:entry_id])
#previous_votes = Vote.joins(:entry).where(entries: { id: #entry.id, challenge_id: #entry.challenge_id })

ActiveRecord query returns an incorrect model

I have been scratching my head over this one for a little while, and though I'm sure its a stupid mistake, I've reached the point where I must consult SO if I am to preserve the hair follicles I have left.
I've written a function in Rails (3.1.2) which should return an array populated with ActiveRecord model objects (users, in this case) which meet a certain criterion. The criterion is that the user's current list (denoted by the field active_list_id) must not be nil. The code follows:
def build_list_array
#lists = Array.new
User.all.each do |user|
#active_list_id = user.active_list_id
#lists<< List.find(#active_list_id) if #active_list_id != nil #TODO WHAT?!? WHY IS THIS RETURNING USERS?
end
end
As you can see, I'm initializing an empty array, cycling through all users and adding their active list to the array if the relevant reference on the user record is not nil. The problem is, this is returning user objects, not list objects.
Here are the associations from the user and list models:
user model:
has_many :lists
has_many :tasks
list model:
belongs_to :user
A brief word about the reference to active_list: A user can have many lists, but only one is active at any time. Therefore, I need to reference that list on the user record. The active list is not a foreign key in the typical sense, then.
I appreciate any help you can give me...Thanks =)
As it stands, your build_list_array will return an array of User because of the behavior of each. When iterating over a collection using each, the call to each returns the original collection.
For example,
list = []
# returns => []
[1,2,3,4,5].each { |number| list << number * 10 }
# returns => [1, 2, 3, 4, 5]
list
# returns => [10, 20, 30, 40, 50]
In your code, the last statement in your build_list_array method is the each call, meaning the return value of each is what is returned by the method. If you simply added a return statement at the end of the method you would be good to go.
def build_list_array
#lists = Array.new
User.all.each do |user|
#active_list_id = user.active_list_id
#lists<< List.find(#active_list_id) if #active_list_id
end
return #lists # Actually return #lists
end
That being said, you should probably use something like Bradley's answer as a basis for more "correct" Rails code.
each always returns the collection it iterates on (no matter what happens inside the block). Sounds like you want to return #lists at the end of your method.
You seem to be making a curious use of instance variables. You could also fetch this in one query via a join, something along the lines of
List.joins('inner join users on active_list_id =lists.id')
Activerecord's Arel is your friend here:
User.where(:active_list_id.not_eq => nil)
Extending Steven's answer, to get the Lists
class User
belongs_to :active_list, :class_name => "List"
def build_list_array
#lists = User.where('active_list_id is not null').map(&:active_list).compact

Help in ActiveRecord find with include on conditions

I have a Coach Model which:
has_many :qualifications
I want to find all coaches whose some attribute_id is nil and they have some qualifications. Something which is like.
def requirement
legal_coaches = []
coaches = find_all_by_attribute_id(nil)
coaches.each do |coach|
legal_coaches << coach if coach.qualifications.any?
end
legal_coaches
end
Is there a way to get all such records in one line ?
find_all_by_attribute_id(nil).select(&:qualification)
I think you can't do that via purely ruby syntax. I can only think of the following (ugly) way
Coach.find(:all, :conditions => "attribute_id IS NULL AND EXISTS(SELECT * FROM qualifications WHERE coach_id = coaches.id)")

ActiveRecord and SELECT AS SQL statements

I am developing in Rails an app where I would like to rank a list of users based on their current points. The table looks like this: user_id:string, points:integer.
Since I can't figure out how to do this "The Rails Way", I've written the following SQL code:
self.find_by_sql ['SELECT t1.user_id, t1.points, COUNT(t2.points) as user_rank FROM registrations as t1, registrations as t2 WHERE t1.points <= t2.points OR (t1.points = t2.points AND t1.user_id = t2.user_id) GROUP BY t1.user_id, t1.points ORDER BY t1.points DESC, t1.user_id DESC']
The thing is this: the only way to access the aliased column "user_rank" is by doing ranking[0].user_rank, which brinks me lots of headaches if I wanted to easily display the resulting table.
Is there a better option?
how about:
#ranked_users = User.all :order => 'users.points'
then in your view you can say
<% #ranked_users.each_with_index do |user, index| %>
<%= "User ##{index}, #{user.name} with #{user.points} points %>
<% end %>
if for some reason you need to keep that numeric index in the database, you'll need to add an after_save callback to update the full list of users whenever the # of points anyone has changes. You might look into using the acts_as_list plugin to help out with that, or that might be total overkill.
Try adding user_rank to your model.
class User < ActiveRecord::Base
def rank
#determine rank based on self.points (switch statement returning a rank name?)
end
end
Then you can access it with #user.rank.
What if you did:
SELECT t1.user_id, COUNT(t1.points)
FROM registrations t1
GROUP BY t1.user_id
ORDER BY COUNT(t1.points) DESC
If you want to get all rails-y, then do
cool_users = self.find_by_sql ['(sql above)']
cool_users.each do |cool_user|
puts "#{cool_user[0]} scores #{cool_user[1]}"
end

Resources