Rails Eager loading has_many associations for an existing object - ruby-on-rails

I am fairly new to rails & I am having this performance issue that I would appreciate any help with.
I have a User model & each user has_many UserScores associated. I am preparing a dashboard showing different user stats including counts of user_scores based on certain conditions. Here is a snippet of the code:
def dashboard
#users = Array.new
users = User.order('created_at ASC')
users.each do |u|
user = {}
user[:id] = u.id
user[:name] = u.nickname
user[:email] = u.email
user[:matches] = u.user_scores.count
user[:jokers_used] = u.user_scores.where(:joker => true).length
user[:jokers] = u.joker
user[:bonus] = u.user_scores.where(:bonus => 1).length
user[:joined] = u.created_at.strftime("%y/%m/%d")
if user[:matches] > 0
user[:last_activity] = u.user_scores.order('updated_at DESC').first.updated_at.strftime("%y/%m/%d")
else
user[:last_activity] = u.updated_at.strftime("%y/%m/%d")
end
#users << user
end
#user_count = #users.count
end
The issue I am seeing is repeated UserScore db queries for each user to get the different counts.
Is there a way to avoid those multiple queries??
N.B. I'm not sure if my approach for preparing data for the view is the optimal way, so any advice or tips regarding that will be greatly appreciated as well.
Thanks

You need to eager load users_scores to reduce multiple queries. #Slava.K provided good explanation on how to eliminate that.
Add includes(:user_scores) for querying users, and use ruby's methods to work with collections once data is fetched from DB through query.
See code below to understand that:
users = User.includes(:user_scores).order('created_at ASC')
users.each do |u|
....
user[:matches] = u.user_scores.length
user[:jokers_used] = u.user_scopes.select{ |score| score.joker == true }.length
user[:jokers] = u.joker
user[:bonus] = u.user_scores.select{ |score| score.bonus == 1 }.length
....
end
Also, The way you are preparing response is not clean and flexible. Instead you should override as_json method to prepare json which can consumed by views properly. as_json method is defined for models by default. You can read more about it from official documentation http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html or visit article on preparing clean json response where I explained about overriding as_json properly in-depth.

Use includes method for eager loading your has many associations. You can understand this concept here: https://www.youtube.com/watch?v=s2EPVMqOsTQ

Firstly, reference user_scores association in your query:
users = User.includes(:user_scores).order('created_at ASC')
Follow rails documentation associations eager loading: http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
Also note that where makes new query to the database even if your association is already preloaded. Therefore, instead of
u.user_scores.where(:joker => true).length
u.user_scores.where(:bonus => 1).length
try:
u.user_scores.count { |us| us.joker }
u.user_scores.count { |us| us.bonus == 1 }
You will probably have to rewrite .user_scores.order('updated_at DESC').first.updated_at.strftime("%y/%m/%d") somehow as well

Related

Best way to conditionally chain scopes

I'm trying to extend the functionality of my serverside datatable. I pass some extra filters to my controller / datatable, which I use to filter results. Currently in my model I am testing whether the params are present or not before applying my scopes, but I'm not convinced this is the best way since I will have a lot of if/else scenario's when my list of filters grows. How can I do this the 'rails way'?
if params[:store_id].present? && params[:status].present?
Order.store(params[:store_id]).status(params[:status])
elsif params[:store_id].present? && !params[:status].present?
Order.store(params[:store_id])
elsif !params[:store_id].present? && params[:status].present?
Order.status(params[:status])
else
Order.joins(:store).all
end
ANSWER:
Combined the answers into this working code:
query = Order.all
query = query.store(params[:store_id]) if params[:store_id].present?
query = query.status(params[:status]) if params[:status].present?
query.includes(:store)
You could do it like this:
query = Order
query = query.store(params[:store_id]) if params[:store_id].present?
query = query.status(params[:status]) if params[:status].present?
query = Order.joins(:store) if query == Order
Alternatively, you could also just restructure the status and store scopes to include the condition inside:
scope :by_status, -> status { where(status: status) if status.present? }
Then you can do this instead:
query = Order.store(params[:store_id]).by_status(params[:status])
query = Order.joins(:store) unless (params.keys & [:status, :store_id]).present?
Since relations are chainable, it's often helpful to "build up" your search query. The exact pattern for doing that varies widely, and I'd caution against over-engineering anything, but using plain-old Ruby objects (POROs) to build up a query is common in most of the large Rails codebases I've worked in. In your case, you could probably get away with just simplifying your logic like so:
relation = Order.join(:store)
if params[:store_id]
relation = relation.store(params[:store_id])
end
if params[:status]
relation = relation.status(params[:status])
end
#orders = relation.all
Rails even provides ways to "undo" logic that has been chained previously, in case your needs get particularly complex.
The top answer above worked for me. Here is an example of its' real-life implementation:
lessons = Lesson.joins(:member, :office, :group)
if #member.present?
lessons = lessons.where(member_id: #member)
end
if #office.present?
lessons = lessons.where(office_id: #office)
end
if #group.present?
lessons = lessons.where(group_id: #group)
end
#lessons = lessons.all

removing objects from an array during a loop

I am trying to filter the results of an user search in my app to only show users who are NOT friends. My friends table has 3 columns; f1 (userid of person who sent request), f2 (userid of friend who received request), and confirmed (boolean of true or false). As you can see, #usersfiltered is the result of the search. Then the definition of the current user's friend is established. Then I am trying to remove the friends from the search results. This does not seem to be working but should be pretty straight forward. I've tried delete (not good) and destroy.
def index
#THIS IS THE SEARCH RESULT
#usersfiltered = User.where("first_name LIKE?", "%#{params[:first_name]}%" )
#THIS IS DEFINING ROWS ON THE FRIEND TABLE THAT BELONG TO CURRENT USER
#confirmedfriends = Friend.where(:confirmed => true)
friendsapproved = #confirmedfriends.where(:f2 => current_user.id)
friendsrequestedapproved = #confirmedfriends.where(:f1 => current_user.id)
#GOING THROUGH SEARCH RESULTS
#usersfiltered.each do |usersfiltered|
if friendsapproved.present?
friendsapproved.each do |fa|
if usersfiltered.id == fa.f1
#NEED TO REMOVE THIS FROM RESULTS HERE SOMEHOW
usersfiltered.remove
end
end
end
#SAME LOGIC
if friendsrequestedapproved.present?
friendsrequestedapproved.each do |fra|
if usersfiltered.id == fra.f2
usersfiltered.remove
end
end
end
end
end
I would flip it around the other way. Take the logic that is loop-invariant out of the loop, which gives a good first-order simplification:
approved_ids = []
approved_ids = friendsapproved.map { |fa| fa.f1 } if friendsapproved.present?
approved_ids += friendsrequestedapproved.map { |fra| fra.f2 } if friendsrequestedapproved.present?
approved_ids.uniq! # (May not be needed)
#usersfiltered.delete_if { |user| approved_ids.include? user.id }
This could probably be simplified further if friendsapproved and friendsrequestedapproved have been created separately strictly for the purpose of the deletions. You could generate a single friendsapproval list consisting of both and avoid unioning id sets above.
While I agree that there may be better ways to implement what you're doing, I think the specific problem you're facing is that in Rails 4, the where method returns an ActiveRecord::Relation not an Array. While you can use each on a Relation, you cannot in general perform array operations.
However, you can convert a Relation to an Array with the to_a method as in:
#usersfiltered = User.where("first_name LIKE?", "%#{params[:first_name]}%" ).to_a
This would then allow you to do the following within your loop:
usersfiltered.delete(fa)

How to query many fields, allowing for NULL

I have a Rails site that logs simple actions such as when people upvote and downvote information. For every new action, an EventLog is created.
What if the user changes his or her mind? I have an after_create callback that looks for complementary actions and deletes both if it finds a recent pair. For clarity, I mean that if a person upvotes something and soon cancels, both event_logs are deleted. What follows is my callback.
# Find duplicate events by searching nearly all the fields in the EventLog table
#duplicates = EventLog.where("user_id = ? AND event = ? AND project_id = ? AND ..., ).order("created_at DESC")
if #duplicates.size > 1
#duplicates.limit(2).destroy_all
end
The above code doesn't quite work because if any of the fields happen to be nil, the query returns [].
How can I write this code so it can handle null values, and/or is there a better way of doing this altogether?
If I understood this correctly,
some of the fields can be nil, and you want to find activity logs that have same user_id, same project_id or project id can be nil.
So I guess this query should work for you.
ActivityLog.where(user_id: <some_id> AND activity: <complementary_id> AND :project_id.in => [<some_project_id>, nil] ....)
This way you would get the complementary event logs where user_id is same and project id may or may not be present
class ActivityLog
QUERY_HASH = Proc.new{ {user_id: self.user_id,
activity: complementary_id(self.id),
and so on....
} }
How about:
# event_log.rb
def duplicate_attr_map
{
:user_id,
:project_id
}
end
def duplicates
attribs = duplicate_attr_map.reject_if(&:blank?)
query = attribs.map { |attr| "#{attr} = ?" }.join(' AND ')
values = attribs.map { |attr| self.send(attr) }
EventLog.where(query, *values).order("created_at DESC")
end
def delete_duplicates(n)
duplicates.limit(n).delete_all if duplicates.size > 1
end
# usage:
# EventLog.find(1).delete_duplicates(2)
not tested, could be improved

Rails 3 multiple parameter filtering using scopes

Trying to do a basic filter in rails 3 using the url params. I'd like to have a white list of params that can be filtered by, and return all the items that match. I've set up some scopes (with many more to come):
# in the model:
scope :budget_min, lambda {|min| where("budget > ?", min)}
scope :budget_max, lambda {|max| where("budget < ?", max)}
...but what's the best way to use some, none, or all of these scopes based on the present params[]? I've gotten this far, but it doesn't extend to multiple options. Looking for a sort of "chain if present" type operation.
#jobs = Job.all
#jobs = Job.budget_min(params[:budget_min]) if params[:budget_min]
I think you are close. Something like this won't extend to multiple options?
query = Job.scoped
query = query.budget_min(params[:budget_min]) if params[:budget_min]
query = query.budget_max(params[:budget_max]) if params[:budget_max]
#jobs = query.all
Generally, I'd prefer hand-made solutions but, for this kind of problem, a code base could become a mess very quickly. So I would go for a gem like meta_search.
One way would be to put your conditionals into the scopes:
scope :budget_max, lambda { |max| where("budget < ?", max) unless max.nil? }
That would still become rather cumbersome since you'd end up with:
Job.budget_min(params[:budget_min]).budget_max(params[:budget_max]) ...
A slightly different approach would be using something like the following inside your model (based on code from here:
class << self
def search(q)
whitelisted_params = {
:budget_max => "budget > ?",
:budget_min => "budget < ?"
}
whitelisted_params.keys.inject(scoped) do |combined_scope, param|
if q[param].nil?
combined_scope
else
combined_scope.where(whitelisted_params[param], q[param])
end
end
end
end
You can then use that method as follows and it should use the whitelisted filters if they're present in params:
MyModel.search(params)

Performance: minimize database hitting

I am using Ruby on Rails 3.0.7 and I am trying to minimize database hitting. In order to do that I retrieve from the database all Article objects related to a User and then perform a search on those retrieved objects.
What I do is:
stored_objects = Article.where(:user_id => <id>) # => ActiveRecord::Relation
<some_iterative_function_1>.each { |...|
stored_object = stored_objects.where(:status => 'published').limit(1)
...
# perform operation on the current 'stored_object' considered
}
<some_iterative_function_2>.each { |...|
stored_object = stored_objects.where(:visibility => 'public').limit(1)
...
# perform operation on the current 'stored_object' considered
}
<some_iterative_function_n>.each { |...|
...
}
The stored_object = stored_objects.where(:status => 'published') code will really avoid to hitting the database (I ask this because in my log file it seams still run a database query for each iteration)? If no, how can I minimize database hitting?
P.S.: in few words, what I would like to do is to work on the ActiveRecord::Relation (an array of ) but the where method called on it seams to hit the database.
Rails has functionality to grab chunks of the database at one time, then iterate over the rows without having to hit the database again.
See "Retrieving Multiple Objects in Batches" for more information about find_each and find_in_batches.
Once you start iterating over stored_objects (if that's what you're doing), they'll be loaded from the database. If you want to load only the users's published articles, you could do this:
stored_objects = Article.where(:user_id => id, :status => 'published')
If you instead want to load published and unpublished articles and do something different with the published ones, you could do this:
stored_objects = Article.where(:user_id => id)
stored_objects.find_all { |a| a.status == 'published' }. each do |a|
# ... do something with a published article
end
Or perhaps:
Article.where(:user_id => id).each do |article|
case article.status
when 'published'
# ... do something with a published article
else
# ... do something with an article that's not published
end
end
Each of these examples performs only one database query. Choosing which one depends on which data you really want to work with.

Resources