eager loading not working with order() clause in rails - ruby-on-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)

Related

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

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!

Rails - get polymorphic children from collection as ActiveRecord::Relation

I need to get all children from a parent as an ActiveRecord::Relation. Thing is, this children are stored in a polymorphic relation. In my case I need it to paginate some search results obtained with pg_search gem.
I've tried the following:
results = PgSearch.multisearch('query').map(&:searchable)
# Horrible solution, N + 1 and returns an array
docs = PgSearch.multisearch('query').includes(:searchable)
results = docs.map(&:searchable)
# Still getting an array
Also thought of things like select or pluck, but they are not intended for retrieving objects, only column data. I could try to search ids for each children type like so
Post.where(id: PgSearch.multisearch('query').where(searchable_type: "Post").select(:searchable_id)
Profile.where(id: PgSearch.multisearch('query').where(searchable_type: "Profile").select(:searchable_id)
But it doesn't scale, since I would need to do this for every object I want to obtain from a search result.
Any help would be appreciated.
EDIT
Here's some basic pseudocode demonstrating the issue:
class Profile < ApplicationRecord
has_one :search_document, :as => :searchable
end
class Post < ApplicationRecord
has_one :search_document, :as => :searchable
end
class Profile < ApplicationRecord
has_one :search_document, :as => :searchable
end
class SearchDocument < ApplicationRecord
belongs_to :searchable, plymorphic: true
end
I want to obtain all the searchable items as an ActiveRecord::Relation, so that I can dynamically filter them, in this specific case, using limit(x).offset(y)
SearchDocument.all.joins(:searchable).limit(10).offset(10)
Generates an error: cannot eagerly load searchable cause of polymorphic relation
SearchDocument.all.includes(:searchable).limit(10).offset(10)
This one does load the searchable items into memory, but does not return them in the query, instead it applies the filters to the SearchDocument items, as expected. This might be a temporary solution, to filter the search documents and then get the searchable items from them, but collides with pagination on the views.
The question here is: Is there a way I can get all searchable items as ActiveRecord::Relation to further filter them?
I'm unfamiliar with this library. However, looking at your code, I'd guess that any attempt to take a CollectionProxy and map some function over it will trigger evaluating the CollectionProxy and return an array.
Having had a quick look at the library GitHub docs, perhaps something like this might work:
post_docs = PgSearch.multisearch('query').where(searchable_type: "Post")
posts = Post.where(pg_search_document: post_docs)

ORDER BY and DISTINCT ON (...) in Rails

I am trying to ORDER by created_at and then get a DISTINCT set based on a foreign key.
The other part is to somehow use this is ActiveModelSerializer. Specifically I want to be able to declare:
has_many :somethings
In the serializer. Let me explain further...
I am able to get the results I need with this custom sql:
def latest_product_levels
sql = "SELECT DISTINCT ON (product_id) client_product_levels.product_id,
client_product_levels.* FROM client_product_levels WHERE client_product_levels.client_id = #{id} ORDER BY product_id,
client_product_levels.created_at DESC";
results = ActiveRecord::Base.connection.execute(sql)
end
Is there any possible way to get this result but as a condition on a has_many relationship so that I can use it in AMS?
In pseudo code: #client.products_levels
Would do something like: #client.order(created_at: :desc).select(:product_id).distinct
That of course fails for reasons that are beyond me.
Any help would be great.
Thank you.
A good way to structure this is to split your query into two parts: the first part manages the filtering of rows so that you get only your latest client product levels. The second part uses a standard has_many association to connect Client with ClientProductLevel.
Starting with your ClientProductLevel model, you can create a scope to do the latest filtering:
class ClientProductLevel < ActiveRecord::Base
scope :latest, -> {
select("distinct on(product_id) client_product_levels.product_id,
client_product_levels.*").
order("product_id, created_at desc")
}
end
You can use this scope anywhere that you have a query that returns a list of ClientProductLevel objects, e.g., ClientProductLevel.latest or ClientProductLevel.where("created_at < ?", 1.week.ago).latest, etc.
If you haven't already done so, set up your Client class with a has_many relationship:
class Client < ActiveRecord::Base
has_many :client_product_levels
end
Then in your ActiveModelSerializer try this:
class ClientSerializer < ActiveModel::Serializer
has_many :client_product_levels
def client_product_levels
object.client_product_levels.latest
end
end
When you invoke the ClientSerializer to serialize a Client object, the serializer sees the has_many declaration, which it would ordinarily forward to your Client object, but since we've got a locally defined method by that name, it invokes that method instead. (Note that this has_many declaration is not the same as an ActiveRecord has_many, which specifies a relationship between tables: in this case, it's just saying that the serializer should present an array of serialized objects under the key `client_product_levels'.)
The ClientSerializer#client_product_levels method in turn invokes the has_many association from the client object, and then applies the latest scope to it. The most powerful thing about ActiveRecord is the way it allows you to chain together disparate components into a single query. Here, the has_many generates the `where client_id = $X' portion, and the scope generates the rest of the query. Et voila!
In terms of simplification: ActiveRecord doesn't have native support for distinct on, so you're stuck with that part of the custom sql. I don't know whether you need to include client_product_levels.product_id explicitly in your select clause, as it's already being included by the *. You might try dumping it.

ActiveRecord includes. Specify included columns

I have model Profile. Profile has_one User. User model has field email. When I call
Profile.some_scope.includes(:user)
it calls
SELECT users.* FROM users WHERE users.id IN (some ids)
But my User model has many fields that I am not using in rendering. Is it possible to load only emails from users? So, SQL should look like
SELECT users.email FROM users WHERE users.id IN (some ids)
Rails doesn't have the facility to pass the options for include query. But we can pass these params with the association declaration under the model.
For your scenario, you need to create a new association with users model under the profile model, like below
belongs_to :user_only_fetch_email, :select => "users.id, users.email", :class_name => "User"
I just created one more association but it points to User model only. So your query will be,
Profile.includes(:user_only_fetch_email)
or
Profile.includes(:user_only_fetch_email).find(some_profile_ids)
If you want to select specific attributes, you should use joins rather than includes.
From this asciicast:
the include option doesn’t really work with the select option as we don’t have control over how the first part of the SELECT statement is generated. If you need control over the fields in the SELECT then you should use joins over include.
Using joins:
Profile.some_scope.joins(:users).select("users.email")
You need extra belongs to in the model.
For simple association:
belongs_to :user_restricted, -> { select(:id, :email) }, class_name: 'User'
For Polymorphic association (for example, :commentable):
belongs_to :commentable_restricted, -> { select(:id, :title) }, polymorphic: true, foreign_type: :commentable_type, foreign_key: :commentable_id
You can choose whatever belongs_to name you want. For the examples given above, you can use them like Article.featured.includes(:user_restricted), Comment.recent.includes(:commentable_restricted) etc.
Rails does not support to select specific columns when includes. You know ,it's just lazy load.
It use the ActiveRecord::Associations::Preloader module to load the associated data before data actually using. By the method:
def preload(records, associations, preload_scope = nil)
records = Array.wrap(records).compact
if records.empty?
[]
else
records.uniq!
Array.wrap(associations).flat_map { |association|
preloaders_on association, records, preload_scope
}
end
end
preload_scope the third params of preload, is a way to select specify columns. But can't lazy load anymore.
At Rails 5.1.6
relation = Profile.where(id: [1,2,3])
user_columns = {:select=>[:updated_at, :id, :name]}
preloader = ActiveRecord::Associations::Preloader.new
preloader.preload(relation, :user, user_columns)
It will select the specify columns you passed in. But, it just for single association. You need create a patch for ActiveRecord::Associations::Preloader to support loading multiple complex associations at once.
Here is a example for patch
The way to use it, example
I wanted that functionality myself,so please use it.
Include this method in your class
#ACCEPTS args in string format "ASSOCIATION_NAME:COLUMN_NAME-COLUMN_NAME"
def self.includes_with_select(*m)
association_arr = []
m.each do |part|
parts = part.split(':')
association = parts[0].to_sym
select_columns = parts[1].split('-')
association_macro = (self.reflect_on_association(association).macro)
association_arr << association.to_sym
class_name = self.reflect_on_association(association).class_name
self.send(association_macro, association, -> {select *select_columns}, class_name: "#{class_name.to_sym}")
end
self.includes(*association_arr)
end
And you will be able to call like: Contract.includes_with_select('user:id-name-status', 'confirmation:confirmed-id'), and it will select those specified columns.
Using Mohanaj's example, you can do this:
belongs_to :user_only_fetch_email, -> { select [:id, :email] }, :class_name => "User"

Rails way to COUNT the nr of records in my case

I am using Rails v2.3.2.
I have a model called UsersCar:
class UsersCar < ActiveRecord::Base
belongs_to :car
belongs_to :user
end
This model mapped to a database table users_cars, which only contains two columns : user_id, car_id.
I would like to use Rails way to count the number of car_id where user_id=3. I konw in plain SQL query I can achieve this by:
SELECT COUNT(*) FROM users_cars WHERE user_id=3;
Now, I would like to get it by Rails way, I know I can do:
UsersCar.count()
but how can I put the ...where user_id=3 clause in Rails way?
According to the Ruby on Rails Guides, you can pass conditions to the count() method. For example:
UsersCar.count(:conditions => ["user_id = ?", 3])
will generates:
SELECT count(*) AS count_all FROM users_cars WHERE (user_id = 3)
If you have the User object, you could do
user.cars.size
or
user.cars.count
Another way would be to do:
UserCar.find(:user_id => 3).size
And the last way that I can think of is the one mentioned above, i.e. 'UserCar.count(conditions)'.
With the belogngs to association, you get several "magic" methods on the parent item to reference its children.
In your case:
users_car = UsersCar.find(1) #=>one record of users_car with id = 1.
users_car.users #=>a list of associated users.
users_car.users.count #=>the amount of associated users.
However, I think you are understanding the associations wrong, based on the fact that your UsersCar is named awkwardly.
It seems you want
User has_and_belongs_to_many :cars
Car has_and_belongs_to_manu :users
Please read abovementioned guide on associations if you want to know more about many-to-many associations in Rails.
I managed to find the way to count with condition:
UsersCar.count(:condition=>"user_id=3")

Resources