Group by ruby on rails - ruby-on-rails

I need to group the users based on created_at column by year and month,
User.all.group_by{ |q| [q.created_at.year, q.created_at.month]},
where I am getting hash with key [year, month], Is there any way to group the records which results like
{
year1 =>{ month1 =>[array of records], month2=>[array]},
year2 =>{ month1 =>[array of records], month2=>[array]}
}

Try to the following:
User.all
.group_by { |user| user.created_at.year }
.transform_values { |users| users.group_by { |user| user.created_at.month } }

you can get by
result = {}
User.all.each do |user|
result[user.created_at.year] = {} if !result[user.created_at.year].present?
result[user.created_at.year][user.create_at.month] = [] if !result[user.created_at.year][user.create_at.month].present?
result[user.created_at.year][user.create_at.month].push(user.attributes)
end

There are many ways to do this. Although the suggested duplicate does propose a solution, there are other ways to achieve it. One of them is using the code below (it's a refactor of one of the first answers).
{}.tap do |hash|
User.all.find_each do |user|
year = user.created_at.year
month = user.created_at.month
hash[year] ||= {}
hash[year][month] ||= []
hash[year][month] << user
end
end
What's good about this code is that you are not loading all user records into memory (bad if you have, let's say, 1M user records) because it uses find_each which, by default, fetches users by 1000.
It also only goes through each item once, unlike the accepted answer in the duplicate suggested above.
Overall, there's a lot of ways to tackle your problem in ruby. It's up to you to discover what you think is clean code. But make sure that what you decide to use is efficient.

Related

Rails Eager loading has_many associations for an existing object

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

Return all user profile except current users'

I am iterating over all user profiles to get their country. I wish not to include the current user's profile. If I have a total of 10 users, I want only 9 user's country:
# helper function
def method_name
Profile.all.select do |m|
n = m.country.class == String # Because most countries will be nil. A shorter way to not include nil profiles?
return n.reject! {|x| x == current_user.profile.user_id }
end
end
The method above should return all user's profile except for the current user.
The error I got is
undefined method `reject!' for false:FalseClass
The reason? In my views I could:
<%= method_name.map do |p| p.country end %>
Beware of using Profile.all here, because you will potentially use a lot of memory instantiating a profile object for every row in your database.
I think the best way to do this is to create a method within your Profile model, which performs the query, and uses pluck() to return an array containing just the countries you want.
def self.countries_except_for(user)
where.not(user_id: user.id, country: nil).pluck(:country)
end
Now you can just call:
Profile.countries_except(current_user)
To get the array of countries. This approach is much more efficient than looping through as in your question.
The line n = m.country.class == String sets n to true or false, thefore the next line fails, because you try to call reject! {|x| x == current_user.profile.user_id } on that boolean value.
One way to solve the problem might be to fix your code like this:
def method_name
Profile.all.select do |p|
p.country.present? && p != current_user.profile
end
end
Another way - and my preferred one - would be to only load matching record from the database:
def method_name
Profile.where.not(user_id: current_user.id).where.not(country: nil)
end

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)

Map/Collect first true result

Consider the following
users.collect do |user|
user.favorite_song.presence
end.compact.first
In this case, I want the first favorite song I encounter among my users.
Can this be written in a nicer way?
I tried
users.find do |user|
user.favorite_song.presence
end
But it returns the first user with a favorite song, rather than the favorite song itself.
If the users array isn't too big, your first solution is fine, and can be rewritten like this:
users.map(&:favorite_song).compact.first
You can also modify your second approach as follows:
users.find { |user| user.favorite_song.present? }.favorite_song
Both of these solutions assume that there exists a favorite_song in some user and will raise an exception if there isn't. You can elegantly avoid this with try (Rails only):
users.find { |user| user.favorite_song.present? }.try(:favorite_song)
What about:
users.each do |user|
break user.favorite_song if user.favorite_song.present?
end
Will return user.favorite_song if condition is true otherwise will return users
favorite_song = nil
users.map do |user|
favorite_song = user.favorite_song
break if favorite_song
end
Please have a try with
users.map{|user| user.favorite_song}.compact.first
As of Ruby 2.0 you can use Enumerator::Lazy#lazy to solve this more cleanly and efficiently:
users.lazy.map(&:favorite_song).find(&:present?)
This is more efficient because it will only call favorite_song on users until it finds one that's present. It's cleaner because it's more concise and lazy is self documenting of the intention.

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

Resources