How to eliminate a rails n+1 query that uses an association - ruby-on-rails

I have an n+1 query that I would like to eliminate. I use eager loading in a private method, settlements_by_user, to load a settlement along with pay period, provider account, and associated user. However I am noticing that my group_by method is creating an n+1 situation. The block given to group_by is an association, and for each settlement it is firing a DB query to find the user. Why is a query being fired when the users should already be pre-loaded?
Below is my Settlement model with its association:
class Settlement < ApplicationRecord
has_one :user, through: :pay_period
And here is the eager loading AR query, as well as the n+1 created by group_by given the user association as a block.
def build
#data = {}
settlements_by_user.group_by(&:user).each do |user, user_settlements|
(#data[user.id] = {
user: user,
settlements: user_settlements
})
end
self
end
private
def settlements_by_user
settlements = Settlement.unprocessed.positive.where('date(settlements.created_at) = ?', settlements_created_on).
order(total_amount_cents: :desc).
eager_load(pay_period: { provider_account: :user })
settlements
end

As naveed suggested the way the eager load was written wasn't resolving the relation from settlement to user correctly. Changing it to simply eager_load(:user) solved my n+1 problem!

Related

eager loading not working with order() clause in rails

I am using this query to get my data
user = User.includes(:skills).order(user: :id)
it is working fine. but when i try to display skills by alphabetical order like below
user.skills.order(name: :asc)
It shows in logs that it goes in the database as order() is an activerecord method. It seems like eager loading is failing here because what's the point to use eager loading if it has to go in the database anyway.
Can anyone guide me what is a good way to do this.
When you eager load associated records using .includes, you should access the association as it is. Otherwise, if you add more query conditions to the association, that will cause a new DB query.
There are a few ways how you can order the associated eager loaded records.
1. Add order condition to the main scope.
user = User.includes(:skills).order("users.id, skills.name ASC")
In this case, it won't work like include method works by default, making two queries. One query will be performed using 'LEFT OUTER JOIN' to fetch the associated records. This is equivalent to using the eager_load method instead of includes
user = User.eager_load(:skills).order("users.id, skills.name ASC")
2. Add order condition to association when you define it.
In this case whenever you access the association, the associated records will always be ordered by name.
class User < ActiveRecord::Base
has_many :skills, -> { order(:name) }
end
3. Create another association with required order for using only in this particular case.
This allows you to avoid unnecessary conditions on the main association which is skills.
class User < ActiveRecord::Base
has_many :skills_ordered_by_name, -> { order(:name) }, class_name: "Skill"
end
# usage
users = User.includes(:skills_ordered_by_name)
users.each do |user|
# access eager loaded association
user.skills_ordered_by_name
end
4. Set default order for the association model.
This will cause the condition to be applied to every association and query related to the associated model.
class Skill < ActiveRecord::Base
default_scope { order(:name) }
end
5. Sort eager loaded records using Ruby code (not ActiveRecord query methods)
This approach is appropriate when there are not many records to sort.
users = User.includes(:skills)
users.each do |user|
# sorting with Ruby's 'sort_by' method
user.skills.sort_by(&:name)
# or something like
user.skills.sort { |one, another| one.name <=> another.name }
end
You can achieve flexibility by using built-in method ActiveRecord::Associations::Preloader#preload.
It accept three arguments:
preload(records, associations, preload_scope = nil)
The first argument accepts ActiveRecord::Base record or array of records.
Second is one or more associations that you want preload to records specified in the first argument.
Last is Hash or Relation which merged with associations.
Use third argument to get sorted and preloaded associations:
users = User.order(user: :id)
ActiveRecord::Associations::Preloader.new.preload(
users,
:skills,
{ order: :name } # or Skill.order(:name)
)
You can use this:
user = User.includes(:skills).order(user: :id, name: :desc)

How do I find records that don't have an association with given conditions? (ActiveRecord / PostgreSQL)

I have the following ActiveRecord models:
class User < ApplicationRecord
has_many :user_alert_archives
end
class UserAlertArchive < ApplicationRecord
belongs_to :user
belongs_to :alert
end
class Alert < ApplicationRecord
has_many :user_alert_archives
end
When a user archives a given alert, a UserAlertArchive record gets created with that user's id and the alert id. Running a query for the user's archived alerts is fairly trivial.
Alert.joins(:user_alert_archives).where(user_alert_archives: {user_id: current_user.id})
It's querying for the inverse of that scenario that I'm having trouble wrapping my head around. How would I do an efficient query for alert records that don't have a UserAlertArchive with the current user's id associated?
--edit--
This is a little difficult to explain but this is the only way I've been able to get the desired results:
archived_ids = Alert.joins(:user_alert_archives).where(user_alert_archives: {user_id: current_user.id}).pluck(:id)
Alert.where.not(id: archived_ids)
This technically works but it pulls all the ids of archived alerts which is pretty slow for users with thousands of them. I'd like to be able to accomplish it in a single query if possible.
With Rails 5 (where method 'jeft_joins' exits)
Alert.left_joins(:user_alert_archives).where(user_alert_archives: {id: nil})
That will display all alerts which have never been archived by anyone.
If you want all alerts, which haven't been archived by some specific user, then maybe you will need to add some sql:
Alert.where('id NOT IN (SELECT alert_id FROM user_alerts_archives u WHERE u.customer_id = ?)', current_user.id)
If you had a model like UserAlert, which was holding an alert status (like 'pending', 'archived'), then querying them could be more beautiful and smooth.
I like M. Stavnycha's approach of using a sub-select so you don't have to pass a large number of ids to an IN clause. I would recommend putting that in a scope or class method on Alert. i.e.
class Alert < ApplicationRecord
has_many :alert_archives
def self.unviewed(user_id)
where("id NOT IN (SELECT alert_id FROM user_alert_archives WHERE user_id = ?)", user_id)
end
end

Rails: Order a model by the last element of a has_many association

I have two models: User and Message that are connected by a has_many relationship. I want to get a list of users sorted by the timestamp on their last message.
class User < ActiveRecord::Base
has_many :messages
end
class Message < ActiveRecord::Base
belongs_to :user
end
When I do this:
#users = User.includes(:messages).order('messages.created_at DESC').limit(5)
It seems to order the messages, grab the newest 5 messages, and then return the users associated with those. So the number of users can be less than 5. I want to make sure I get the 5 users.
I want the query to get the newest message for each request, order the last messages, and then return the users with the newest messages. So I want something like:
#users = User.includes(:messages).order( <messages.last.created_at DESC> )
Ideally, the solution would do the sorting on the database, because I want to use paginate. If it's relevant I'm using Postgres.
I would probably be preferential to the solution mentioned by phoet of adding an attribute to User such as last_message_posted_at and using touch: true to update that on message creation. That simplifies the query that has to be performed when you need to pull your list of users. This also allows a much more readable (IMO) chain for your application:
#users = User.all.order(:last_message_posted_at)
=> "SELECT \"users\".* FROM \"users\" ORDER BY \"users\".\"last_message_posted_at\" ASC"
This also allows you to add a nice and simple scope to your User model
scope: :by_recent_message, ->{ order(:last_message_posted_at) }
User.by_recent_message.limit(5)
It also depends when and how often this #users scope is being used. Adding a few ms to the message post time is preferable, to me, than a complicated SQL query each time the list of users is pulled.
-- Edit for those who aren't familiar with the touch syntax --
Documentation for touch: http://apidock.com/rails/v4.2.1/ActiveRecord/Persistence/touch
class User < ActiveRecord::Base
has_many :messages
end
class Message < ActiveRecord::Base
belongs_to :user, touch: true
end
And then make my query (to the user with most recent last message):
#user = User.includes(:messages).order(updated_at: :desc )
You can try something along the lines of
Message.group(:user_id).joins(:users).order('max(messages.created_at) desc')
you can use left join instead of includes
#users = User.joins("LEFT JOIN messages on messages.user_id = users.id").order('messages.created_at').limit(5)

Rails/Cancancan: Define ability with a complex/scoped query

I am building an app in Rails 4 using Cancancan for role authorization. I have an ability defined that works for individual records but not the :index action.
I have a user role called 'RVSP'. RVSPs are assigned WorkOrders, from which the RVSP will create a Record.
class WorkOrder < ActiveRecord::Base
has_one :record
belongs_to :rvsp, class_name: "User"
end
class Record < ActiveRecord::Base
belongs_to :work_order
end
Here's my problem: I want RVSP users to be able to read or update Records where the WorkOrder was assigned to that RVSP. Here's the ability definition so far:
can [:read, :update], Record, Record.for_rvsp(user.id) do |record|
work_order = record.work_order
user.id == work_order.rvsp_id
end
The scope Record.for_rvsp is this monstrosity:
scope :for_rvsp, -> (rvsp_id) { where( work_order: { rvsp: { id: rvsp_id } } ) }
So: How do I define the ability and/or query for all Records where the Record's WorkOrder's rvsp_id matches the current user's id? I suspect it's a join of some kind but I'm new to this and haven't done that before.
P.S. I thought of just adding a created_by column to Record, but I already have that through PaperTrail, which I'm using for an audit trail. But Record.versions.first.whodunnit is about the same as Record.work_order.rvsp_id as far as difficulty of the query is concerned. Thanks for any advice you can offer!
P.P.S. I'm using Postgres but I'd rather not do any database-specific SQL if I can avoid it.
I started banging on joins and it seems to be working! I replaced the scope above with the following class method:
def self.for_rvsp(rvsp_id)
joins(:work_order).where(work_orders: {rvsp_id: rvsp_id})
end
I also tidied up the ability in Ability.rb:
can [:read, :update], Record, Record.for_rvsp(user.id) do |record|
user.id == record.work_order.rvsp_id
end
So, as far as index actions are concerned, it appears that you need to do the same thing in two places: you define a scope that fetches the records you need, and then you define the ability in the block. The scope ensures that you load all the right records, and the ability block authorizes them. Seems to work for me anyway!
I know this is super late, but you can traverse the associations in CanCanCan.
can [:read, :update], Record, work_order: { rsvp_id: user.id }

Getting an array for user ids from an array of users

I am using the amistad gem to handle friend relationships. Users have events associated with them. I would like to provide a feed of events for the a given user based on who they are friends with.
I have used the following code from http://ruby.railstutorial.org for follow relationships. However with amistad i don't have a user.friend_ids method only a user.friends method.
How can I get a similar feed type of result (that can be paged and all that) with the user.friends call that gives me a list of user objects and not just the ids?
class Micropost < ActiveRecord::Base
default_scope :order => 'microposts.created_at DESC'
# Return microposts from the users being followed by the given user.
scope :from_users_followed_by, lambda { |user| followed_by(user) }
private
# Return an SQL condition for users followed by the given user.
# We include the user's own id as well.
def self.followed_by(user)
following_ids = %(SELECT followed_id FROM relationships
WHERE follower_id = :user_id)
where("user_id IN (#{following_ids}) OR user_id = :user_id",
{ :user_id => user })
end
end
This is mostly pseudocode as it doesn't work, but here's what I think I'm trying to accomplish in code:
class Event< ActiveRecord::Base
default_scope :order => 'event.created_at DESC'
# Return events from friends of a user.
scope :from_friends, lambda { |user| friends_of(user) }
private
# Return an SQL condition for users followed by the given user.
# We include the user's own id as well.
def self.friends_of(user)
friend_ids = %(SELECT friendIDs FROM friendships)
where("user_id IN (#{friend_ids})")
end
end
You can manually add friend_ids method to the User model.
def friend_ids
self.friends.map(&:id)
//Here, I'm iterating over user.friends array and getting an array of ids
end
EDIT: As per your comment, I'm assuming you have the associations between user and events built up properly.
e.g A user has many events and an event belongs to a user/multiple users (depending on your requirements.)
Once you have the associations setup correctly, you can simply lookout for events with the user_ids which you got from above friend_ids method.

Resources