ActiveRecord includes. Specify included columns - ruby-on-rails

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"

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)

Preloading of chain of complex/indirect ActiveRecord functions/'associations'

I am working on a plugin for Discourse, which means that I can modify classes with class_eval, but I cannot change the DB schema. To store extra data about the Topic model, I can perform joins with TopicCustomField, which is provided for this purpose.
I am able to store and retrieve all the data I need, but when many Topics are loaded at once, the DB performance is inefficient because my indirect data is loaded once for each Topic by itself. It would be much better if this data were loaded all at once for each Topic, like can happen when using preload or includes.
For example, each Topic has a topic_guid, and a set of parent_guids (stored in a single string with dashes because order is important). These parent_guids point to both other Topic's topic_guids as well as the name of other Groups.
I would love to be able write something like:
has_many :topic_custom_fields
has_many :parent_guids, -> { where(name: 'parent_guids').pluck(:value).first }, :through => :topic_custom_fields
has_many :parent_groups, class_name: 'Group', primary_key: :parent_guids, foreign_key: :name
But this :through complains about not being able to find an association ":parent_guids" in TopicCustomField, and primary_key won't actually take an association instead of a DB column.
I've also tried the following, but the :through clauses are not able to use the functions as associations.
has_many :topic_custom_fields do
def parent_guids
parent_guids_str = where(name: PARENT_GUIDS_FIELD_NAME).pluck(:value).first
return [] unless parent_guids_str
parent_guids_str.split('-').delete_if { |s| s.length == 0 }
end
def parent_groups
Group.where(name: parent_guids)
end
end
has_many :parent_guids, :through => :topic_custom_fields
has_many :parent_groups, :through => :topic_custom_fields
Using Rails 4.2.7.1
Actually, through parameter of rails associations is to set a many-to-many association with a model, passing "through" other model:
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
So you can't do
has_many :parent_guids, :through => :topic_custom_fields
since ParentGuid is not a model related to TopicCustomFields. Also, pass a block to has_many is only to extend the association with new methods as the ones rails already provide you, like topic_custom_fields.create, topic_custom_fields.build, etc.
Why don't you define the methods inside the block at your second example in the Topic class to retrieve the groups? Is there something you want that would not be possible only with the methods?
Update
Well, I don't think it's possible achieve the same improved performance in this case, since the group ids still have to be handled from topic_custom_fields, and the improved performance is reached through JOINs. Maybe a complex combination of preload, where and references could do the trick, but I don't know if it's possible.
You could try to minimize the db calls instead, maybe gathering all the parent_guids before querying the groups.
I hope there is a more elegant solution, but this is what I have done in order to preload my data efficiently. This should be fairly easy to extend to other applications.
I modify Relation's exec_queries, which calls other preloading functions.
ActiveRecord::Relation.class_eval do
attr_accessor :preload_funcs
old_exec_queries = self.instance_method(:exec_queries)
define_method(:exec_queries) do |&block|
records = old_exec_queries.bind(self).call(&block)
if preload_funcs
preload_funcs.each do |func|
func.call(self, records)
end
end
records
end
end
To Topic, I added:
has_many :topic_custom_fields
attr_accessor :parent_groups
def parent_guids
parent_guids_str = topic_custom_fields.select { |a| a.name == PARENT_GUIDS_FIELD_NAME }.first
return [] unless parent_guids_str
parent_guids_str.value.split('-').delete_if { |s| s.length == 0 }
end
And then in order to preload the parent_groups, I do:
def preload_parent_groups(topics)
topics.preload_funcs ||= []
topics.preload_funcs <<= Proc.new do |association, records|
parent_guidss = association.map {|t| t.parent_guids}.flatten
parent_groupss = Group.where(name: parent_guidss).to_a
records.each do |t|
t.parent_groups = t.parent_guids.map {|guid| parent_groupss.select {|group| group.name == guid }.first}
end
end
topics
end
And finally, I add the preloaders to my Relation query:
result = result.preload(:topic_custom_fields)
result = preload_parent_groups(result)

Search for model by multiple join record ids associated to model by has_many in rails

I have a product model setup like the following:
class Product < ActiveRecord::Base
has_many :product_atts, :dependent => :destroy
has_many :atts, :through => :product_atts
has_many :variants, :class_name => "Product", :foreign_key => "parent_id", :dependent => :destroy
end
And I want to search for products that have associations with multiple attributes.
I thought maybe this would work:
Product.joins(:product_atts).where(parent_id: params[:product_id]).where(product_atts: {att_id: [5,7]})
But this does not seem to do what I am looking for. This does where ID or ID.
So I tried the following:
Product.joins(:product_atts).where(parent_id: 3).where(product_atts: {att_id: 5}).where(product_atts: {att_id: 7})
But this doesn't work either, it returns 0 results.
So my question is how do I look for a model by passing in attributes of multiple join models of the same model type?
SOLUTION:
att_ids = params[:att_ids] #This is an array of attribute ids
product = Product.find(params[:product_id]) #This is the parent product
scope = att_ids.reduce(product.variants) do |relation, att_id|
relation.where('EXISTS (SELECT 1 FROM product_atts WHERE product_id=products.id AND att_id=?)', att_id)
end
product_variant = scope.first
This is a seemingly-simple request made actually pretty tricky by how SQL works. Joins are always just joining rows together, and your WHERE clauses are only going to be looking at one row at a time (hence why your expectations are not working like you expect -- it's not possible for one row to have two values for the same column.
There are a bunch of ways to solve this when dealing with raw SQL, but in Rails, I've found the simplest (not most efficient) way is to embed subqueries using the EXISTS keyword. Wrapping that up in a solution which handles arbitrary number of desired att_ids, you get:
scope = att_ids_to_find.reduce(Product) do |relation, att_id|
relation.where('EXISTS (SELECT 1 FROM product_atts WHERE parent_id=products.id AND att_id=?)', att_id)
end
products = scope.all
If you're not familiar with reduce, what's going on is it's taking Product, then adding one additional where clause for each att_id. The end result is something like Product.where(...).where(...).where(...), but you don't need to worry about that too much. This solution also works well when mixed with scopes and other joins.

Collection Select Multile tables Involved Rails 3.2.1, PostgreSQl

I am trying to get data for my collection_select list.
Details
There are 4 tables involved
volunteers
has_many :signed_posts
has_many :posts, :through=>:signed_posts
signed_posts
belongs_to :volunteer
belongs_to :post
posts
belongs_to :organization
has_many :signed_posts
has_many :volunteers, :through=>:signed_posts
organizations
has_many :post
If I would have to write in plain SQL, it would be like below.This sql is for postgreSQL. I would like to write in rails 3.2.1 syntax.
Select sp.id,p.title ||'-'|| o.name AS project from signed_posts sp
inner join volunteers v on v.id=sp.volunteer_id
inner join posts p on p.id=sp.post_id
inner join organizations o on o.id=p.organization_id
where v.id=1
As a result, I want to get signed_posts.id and posts.title - organizations.name for a volunteer id 1
which I would like to use on dropdown list.
Thank you very much for your help
My Solution
I solved the problem using hash table like this one
#signed_projects=Volunteer.find(1).signed_posts.joins(:post)
#ddl_test=Hash.new
#signed_projects.each do |signed_project|
ddl_test[signed_project.post.title+"-"+signed_project.post.organization.name]=signed_project.id
end
On View
<%=select_tag 'ddl_test',options_for_select(#ddl_test.to_a),:prompt => 'Please select the project'%>
I am not completely satisfied. I think there must be some elegant solution. If any body has better idea, please let me know
There are a couple ways to do this. In your controller you would want a statement such as:
#signed_posts = Volunteer.find(1).signed_posts.includes({:post => :organization})
After that, one way would be to create a method in your SignedPost model to return the data you need, something like the following
def post_name_with_organization
"#{id} #{post.title} - #{post.organization.name}"
end
Then, in your collection_select, you can use (this is partially guessing, since I don't know the specifics of your form):
form.collection_select(:signed_post_id, #signed_posts, :id, :post_name_with_organization)
Edit based on question updates
If you want to use select_tag and options_for_select as you did in your solution, a more elegant way would be to create this variable in your controller:
#signed_posts_options = #signed_posts.map {|sp| [sp.post_name_with_organization, sp.id]}
Then in your view:
<%=select_tag 'ddl_test',options_for_select(#signed_posts_options),:prompt => 'Please select the project'%>

Ruby on Rails Associations

I am starting to create my sites in Ruby on Rails these days instead of PHP.
I have picked up the language easily but still not 100% confident with associations :)
I have this situation:
User Model
has_and_belongs_to_many :roles
Roles Model
has_and_belongs_to_many :users
Journal Model
has_and_belongs_to_many :roles
So I have a roles_users table and a journals_roles table
I can access the user roles like so:
user = User.find(1)
User.roles
This gives me the roles assigned to the user, I can then access the journal model like so:
journals = user.roles.first.journals
This gets me the journals associated with the user based on the roles. I want to be able to access the journals like so user.journals
In my user model I have tried this:
def journals
self.roles.collect { |role| role.journals }.flatten
end
This gets me the journals in a flatten array but unfortunately I am unable to access anything associated with journals in this case, e.g in the journals model it has:
has_many :items
When I try to access user.journals.items it does not work as it is a flatten array which I am trying to access the has_many association.
Is it possible to get the user.journals another way other than the way I have shown above with the collect method?
Hope you guys understand what I mean, if not let me know and ill try to explain it better.
Cheers
Eef
If you want to have user.journals you should write query by hand. As far as I know Rails does has_many :through associations (habtm is a kind of has_many :through) one level deep. You can use has_many with finder_sql.
user.journals.items in your example doesn't work, becouse journals is an array and it doesn't have items method associated. So, you need to select one journal and then call items:
user.journals.first.items
I would also modify your journals method:
def journals
self.roles(:include => :journals).collect { |role| role.journals }.flatten.uniq
end
uniq removes duplicates and :inlcude => :journals should improve sql queries.
Similar question https://stackoverflow.com/questions/2802539/ruby-on-rails-join-table-associations
You can use Journal.scoped to create scope with conditions you need. As you have many-to-many association for journals-roles, you need to access joining table either with separate query or with inner select:
def journals
Journal.scoped(:conditions => ["journals.id in (Select journal_id from journals_roles where role_id in (?))", role_ids])
end
Then you can use user.journals.all(:include => :items) etc

Resources