How can I optimize and combine these inefficient database queries? - ruby-on-rails

Here's are my DB queries:
User has_many UserFollow (UserFollow is a relationship between User models).
User has_many Photos.
Photo has_many PhotoFollow relationships (PhotoFollow is a relationship between User and Photo models).
#user_list = Array.new
user_followers = UserFollow.where("user_1_id = ?", current_user.id).includes(:follower)
user_followers.each do |f|
#user_list << f.user
end
photos = Photo.where("user_id = ?", current_user.id).includes(:follow_relationships => [:photo])
photos.each do |p|
p.follow_relationships.each do |f|
#user_list << f.user if !#user_list.include? f.user
end
end
if #user_list.size < 150
users = User.where("verified = ? and first_name IS NOT NULL and last_name IS NOT NULL", true).includes(:solutions).limit(150 - #user_list.size)
users.each do |u|
#user_list << u if !#user_list.include? u
end
end
All of this takes a ridiculous amount of time obviously. Using includes helps, but I am wondering if there is some way to more efficiently do this set of operations.
Thanks,
Ringo

Append your associations first.
class User < ActiveRecord::Base
has_many :follows_to_user, :class_name => 'UserFollow', :foreign_key => 'follower_id'
has_many :follows_to_photo, :class_name => 'PhotoFollow', :foreign_key => 'user_id' # if you don't have it now
end
Now, first two queries you can make much more elegant within just one SQL query, returning AR::Relation scope.
#user_list = User.includes(:follows_to_user => {}, :follows_to_photo => {:photo => {}}).where(["user_follows.user_1_id = :user_id OR photos.user_id = :user_id", :user_id => current_user.id])
About 150... [updated]
Of course you'd better to implement that logic appending the previous SQL statement with conditions and UNION statement (using SQL syntax only), that should return AR::Relation and will be a little faster. But you can stay lazy and leave it in ruby, although it will return Array:
if (count = #user_list.count) && count < 150 # run COUNT just once and store value into local variable
#user_list |= User.where("verified = ? and first_name IS NOT NULL and last_name IS NOT NULL", true).includes(:solutions).limit(150 - count)
end

Looking at your code, you're planning on getting a list of users to your #user_list. You can build up the list of user ids first so we don't create unnecessary AR objects
First code
#user_list = Array.new
user_followers = UserFollow.where("user_1_id = ?", current_user.id).includes(:follower)
user_followers.each do |f|
#user_list << f.user
end
can be changed to
# assuming you have a user_id column on user_follows table
user_ids = User.joins(:user_follows).where(user_follows: { user_1_id: current_user.id })
.uniq.pluck('user_follows.user_id')
Second code
photos = Photo.where("user_id = ?", current_user.id).includes(:follow_relationships =>[:photo])
photos.each do |p|
p.follow_relationships.each do |f|
#user_list << f.user if !#user_list.include? f.user
end
end
can be changed to
user_ids += Photo.where(user_id: current_user.id).joins(follow_relationships: :photo)
.uniq.pluck('follow_relationships.user_id')
Third code
if #user_list.size < 150
users = User.where("verified = ? and first_name IS NOT NULL and last_name IS NOT NULL", true).includes(:solutions).limit(150 - #user_list.size)
users.each do |u|
#user_list << u if !#user_list.include? u
end
end
can be changed to
user_ids += users = User.where(verified: true)
.where('first_name IS NOT NULL AND last_name IS NOT NULL')
.where('id NOT IN (?)', user_ids)
.limit(150 - user_ids.size).pluck(:id)
Then you can just fetch all the users using user_ids
#user_list = User.where(id: user_ids)

There has to be a better way then my answer, but why don't you include :user since you're loading them when iterating over your queries?
#user_list = Array.new
user_followers = UserFollow.includes(:user).where("user_1_id = ?", current_user.id)
# why did you include followers?
user_followers.each do |f|
#user_list << f.user
end
photos = Photo.includes(follow_relationships: { photo: :user }).where("user_id = ?", current_user.id)
photos.each do |p|
p.follow_relationships.each do |f|
#user_list << f.user unless #user_list.include? f.user
end
end
if #user_list.size < 150
users = User.where("verified = ? and first_name IS NOT NULL and last_name IS NOT NULL", true).limit(150 - #user_list.size)
# why did you include solutions?
users.each do |u|
#user_list << u unless #user_list.include? u
end
end
Maybe this is faster, I'm not sure:
#follower_ids = UserFollow.where("user_1_id = ?", current_user.id).pluck(:user_1_id).uniq
#photo_ids = Photo.joins(follow_relationships: :photo)
#photo_ids = #photo_ids.where("user_id = ? and user_id not in (?)", current_user.id, #follower_ids)
#photo_ids = #photo_ids.pluck(:user_id).uniq
#followers = User.where("id in (?)", #follower_ids)
#photo_users = User.where("id in (?) and not in (?)", #photo_ids, #follower_ids)
#array_size = (#follower_ids + #photo_ids).size
if #array_size < 150
#users = User.where("verified = ? and first_name is not null and last_name is not null", true)
#users = #users.where("id not in (?)", #photo_ids + #follower_ids).limit(150 - #array_size)
else
#users = []
end
#final_array = #followers + #photo_users + #users
I haven't tested if this works, or if it's faster. It has more database queries but less iterations.
Update
What if you added another column to the users model that gets updated with a value from 1 to 3, depending on whether they have followers, photos, or nothing.
Then all you would need to do is:
# in User model
def self.valid_users
where("verified = ? and first_name is not null and last_name is not null", true)
end
#users = User.valid_users.order("sortable ASC").limit(150)

This is just a guess.
#user_list = current_user.followers.where("verified = ? and first_name IS NOT NULL and last_name IS NOT NULL", true).includes(:solutions, :photos => {:follow_relationships}).limit(150)
Over all I think its all wrong due to the complexity alone. If you cant read what its doing clearly then you should start over again.

Have you set up table indexes for your tables?
If you haven't yet, set them up for all of your foreign keys and for columns that you need to include in your conditions. In your DB migration script (with your correct table and column names, of course). They will speed up your queries, especially if you have a large dataset:
add_index :user_follows, :follower_id
add_index :user_follows, :followed_id
add_index :photos, :user_id
add_index :photo_follow_relationships, :photo_id
add_index :photo_follow_relationships, :follower_id
add_index :users, :verified
add_index :users, :first_name
add_index :users, :last_name
Also, some comments and recommendations:
# ... [SNIP] Query and add to user list.
user_followers = [] # You are not actually using the UserFollow records. Unload
# them from memory. Otherwise, they will be stored until we
# leave the block.
# There is no need to fetch other Photo data here, and there is no need to load
# :photo for FollowRelationship. But there is a need to load :user.
photos = Photo.where(:user_id => current_user.id).select('photos.id').
includes(:follow_relationships => [:user])
# ... [SNIP] Add to user list.
photos = [] # Unload photos.
# ... [SNIP] Your condition.
# There is no need to load :solutions for the users here.
users = User.where("verified = ? and first_name IS NOT NULL and last_name IS NOT NULL", true).limit(150 - #user_list.size)
# ... [SNIP] Add to user list.
It would, of course, be nicer if you refactor the code too, as in some of mind blank's recommendations. You can also manke use of has_many :through associations too to clean up your controller.

Add the following relationships to your models
class User
has_many :user_follows
has_many :inverse_user_follows, :class_name=>'UserFollow', :foreign_key=>:follower_id
# followers for user
has_many :followers, :through => :user_follows
# users followed by user
has_many :followed_users, :through => :inverse_user_follows, :source => :user
# photos created by user
has_many :photos
has_many :photo_user_follows, :through => :photos, :source => :user_follows
# followers for user's photo
has_many :photo_followers, :through => :photo_user_follows, :source => :follower
has_many :photo_follows
# photos followed by user
has_many :followed_photos, :source => :photo, :through => :photo_follows
end
class UserFollow
# index user_id and follower_id columns
belongs_to :user
belongs_to :follower, :class_name => "User"
end
Photo related models
class Photo
# index user_id column
belongs_to :user
has_many :photo_follows
has_many :followers, :through => :photo_follows
end
class PhotoFollow
# index photo_id and follower_id columns
belongs_to :photo
belongs_to :follower, :class_name => "User"
end
Now you can get the users who are current users followers OR current user's photo followers OR active users..
user_ids = current_user.follower_ids | current_user.photo_follower_ids
User.where("ids IN (?) OR
( verified = ? and first_name IS NOT NULL and last_name IS NOT NULL )",
user_ids, true).limit(150)

Related

Rails join through 2 other tables

I am trying to join tables to get an object.
I have these models:
class Company < ApplicationRecord
has_many :users
end
class Claim < ApplicationRecord
has_many :uploads, dependent: :destroy
validates :number, uniqueness: true
belongs_to :user, optional: true
end
class User < ApplicationRecord
belongs_to :company
has_many :claims
end
Basically I want to select all claims that belong to users that belong to a company.
Somethings I have tried:
(This works but is terrible and not the rails way)
#claims = []
#company = Company.find(params[:id])
#users = #company.users
#users.each do |u|
u.claims.each do |c|
#claims.push(c)
end
end
#claims = #claims.sort_by(&:created_at)
if #claims.count > 10
#claims.shift(#claims.count - 10)
end
#claims = #claims.reverse
This is close but doesn't have all the claim data because its of the user:
#claims = User.joins(:claims, :company).where("companies.id = users.company_id").where("claims.user_id = users.id").where(company_id: params[:id]).order("created_at DESC").limit(10)
I tried this but keep getting an error:
#claims = Claim.joins(:user, :company).where("companies.id = users.company_id").where("claims.user_id = users.id").where(company_id: params[:id]).order("created_at DESC").limit(10)
error: ActiveRecord::ConfigurationError (Can't join 'Claim' to association named 'company'; perhaps you misspelled it?)
Any ideas what I should do or change?
Based on your relations, you should use
Claim.joins(user: :company)
Because the Company is accessible through the relation Claim <> User.
If you wanted to join/preload/include/eager load another relation, let's say if Claim belongs_to :insurance_company, then you would add it like this:
Claim.joins(:insurance_company, user: :company)
Similar questions:
Join multiple tables with active records
Rails 4 scope to find parents with no children
That being said, if you want to
select all claims that belong to users that belong to a company
Then you can do the following:
Claim
.joins(:user) # no need to join on company because company_id is already on users
.where(company_id: params[:id])
.order(claims: { created_at: :desc })
.limit(10)
Tada!

Find Total Votes a User has on their Posts

I'm using the thumbs_up gem to get votes(likes) on Posts.
I have a page for each user's statistics and one of the statistics I'm trying to find shows how many people have voted (liked) the current_user's Posts. Here is what I have so far, I just need to include something that shows only the count that was on the current_users posts.
#vote_count = Vote.where("voteable_type = ?", "Update").count
# This shows all of the votes on all of the updates instead of ONLY the vote count of the current_user's updates
The Votes table has these columns
voteable_id
voteable_type
voter_id
voter_type
...
...
I think I have to associate the voteable_id to the current_user's update_id but I can't figure it out.
Vote Model
scope :for_voter, lambda { |*args| where(["voter_id = ? AND voter_type = ?", args.first.id, args.first.class.base_class.name]) }
scope :for_voteable, lambda { |*args| where(["voteable_id = ? AND voteable_type = ?", args.first.id, args.first.class.base_class.name]) }
scope :recent, lambda { |*args| where(["created_at > ?", (args.first || 2.weeks.ago)]) }
scope :descending, lambda { order("created_at DESC") }
belongs_to :voteable, :polymorphic => true
belongs_to :voter, :polymorphic => true
attr_accessible :vote, :voter, :voteable if ActiveRecord::VERSION::MAJOR < 4
# Comment out the line below to allow multiple votes per user.
validates_uniqueness_of :voteable_id, :scope => [:voteable_type, :voter_type, :voter_id]
Edit
# user.rb
has_many :updates
# update.rb
belongs_to :user
Try with:
user.updates.joins(:votes).where(votes: { vote: true }).count

select specific attributes from array rails

I have post model
class Post < ActiveRecord::Base
acts_as_voteable
end
and Vote model
class Vote < ActiveRecord::Base
scope :for_voter, lambda { |*args| where(["voter_id = ? AND voter_type = ?", args.first.id, args.first.class.name]) }
scope :for_voteable, lambda { |*args| where(["voteable_id = ? AND voteable_type = ?", args.first.id, args.first.class.name]) }
scope :recent, lambda { |*args| where(["created_at > ?", (args.first || 2.weeks.ago)]) }
scope :descending, order("created_at DESC")
belongs_to :voteable, :counter_cache=>true,:polymorphic => true,:touch=>true
belongs_to :voter, :polymorphic => true
attr_accessible :vote, :voter, :voteable
# Comment out the line below to allow multiple votes per user.
validates_uniqueness_of :voteable_id, :scope => [:voteable_type, :voter_type, :voter_id]
end
when I get the post voters with these method
<% #post.voters_who_voted.each do |voter|%>
<%= voter.name %>
<% end %>
I load my database
how can I select only the user name and user id from these array?
update I changed my code I am using thumbs_up gem I pasted less code first to simplify the question
What do you mean by "load database"? If you want to select only id and name columns, then use #post.users.select([:id, :name]).each ...
Or is it about this problem (according to code that you provided)?
UPD.
voters_who_voted loads all voters and returns array https://github.com/bouchard/thumbs_up/blob/master/lib/acts_as_voteable.rb#L113. You have to add own association to Post model:
has_many :voters, :through => :votes, :source => :voter, :source_type => 'User'
It's just example, perhaps voters will clash with already existing method, if any.
Then use it here instead of voters_who_voted
did you try collect method ??
names = #post.users.collect(&:name)
ids = #post.user.collect(&:id)
If you want it to be related you can make a HASH with it. Id's mapped to the names.

Find condition needs refactoring/optimising on polymorphic association

I'm building a recommendation method for users in my project. Users generate interest records whenever they view, create, comment or interact with objects (weighted depending on the action).
I've written a find method that looks at a user's interests, and then finds users who are also interested in those items. However, it is horrendously inefficient, making as many db calls as the user has interests (up to 50).
Here's a chopped down version of what's going on:
#User.rb
...
has_many :interests, :as => :interestable, :dependent => :destroy
def recommendations
recommendations = []
Interest.for(self).limit(50).each do |item|
recommendations << Interest.other_fans_of(item)
end
user_ids = recommendations.flatten.map(&:user_id).uniq
end
...
#interest.rb
...
belongs_to :user
belongs_to :interestable, :polymorphic => true
named_scope :for, lambda { |user| { :conditions => { :user_id => user.id } } }
named_scope :limit, lambda { |num| { :limit => num } }
named_scope :other_fans_of, lambda { |interest| { :conditions => { :interestable_type => interest.interestable_type, :interestable_id => interest.interestable_id } } }
default_scope :order => "weight DESC"
...
Are there any sql geniuses out there who can turn that into one nice clean db call?
Something like this should do the job. There might be prettier ways…
class User < ActiveRecord::Base
#...
def recommendations
# get a list of the relevant interests
the_interests = Interest.for(self).limit(50).map{|x| [x.interestable_type, x.interestable_id]}
# make some sql
conditions = the_interests.map{|x| "(`interestable_type`=? AND `interestable_id`=?)"}.join(" OR ")
# use ruby magic to make a valid finder and get the other user_ids
user_ids = Interest.all(:select => '`user_id`', :conditions => [conditions, *(the_interests.flatten)]).map(&:user_id).uniq
end
#...
end

Accessing values in a has_many :through join table

I've got users who are members of groups through a membership join table, and one of the attributes of that join table is "administrator". I'm trying to do a check inside of a group's member view, looping through each member to see if they are an administrator.
In the view I tried the following:
for user in #group.users
if user.administrator?
...DO STUFF
end
end
I also tried this in the controller:
#administrators = #group.memberships.find(:all, :conditions => ["administrator = 1"])
But no luck. Any thoughts?
UPDATE - per below, put a method into the user model:
def is_administrator_of(group_id)
Membership.find(:first, :conditions => ['user_id = ? AND group_id = ? AND administrator = ?', self[:id], group_id, true])
end
I think this would be a cleaner way to do this
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
has_many :admins, :through => :memberships, :source => :user,
:conditions => ['memberships.administrator = ?', true]
end
You now have a group.admins list
for user in #group.admins
...DO STUFF
end
Although I think you could setup associations to accomplish this I think the easiest way to do it would be to add a method to your User model that allows you to check for each user (this way it would fit in the loop you have provided). I don't know if it will drop right it, may take a few quick changes but you could start with something like:
User Model
def is_administrator_of(group_id)
Membership.find(:first, :conditions => ['user_id = ? AND group_id = ?', self[:id], group_id]).administrator == 1
end

Resources