I would like to know the best practice when I need a computed attribute that require a call to the database.
If I have a Parent that has many Child, how would I render a children_count attribute in ParentController#index as I don't want to render the children, just the count? what's the best way to do it?
Thank you!
Model:
class Parent < ApplicationRecord
has_many :children
def children_count
children.count # Wouldn't it ask the database when I call this method?
end
end
Controller:
class ParentsController < ApplicationController
def index
parents = Parent.all
render json: parents, only: %i[attr1, attr2] # How do I pass children_count?
end
end
The Rails way to avoid additional database queries in a case like this would be to implement a counter cache.
To do so change
belongs_to :parent
in child.rb to
belongs_to :parent, counter_cache: true
And add an integer column named children_count to your parents database table. When there are already records in your database then you should run something like
Parent.ids.each { |id| Parent.reset_counters(id) }
to fill the children_count with the correct number of existing records (for example in the migration in which you add the new column).
Once these preparations are done, Rails will take care of incrementing and decrementing the count automatically when you add or remove children.
Because the children_count database column is handled like all other attributes you must remove your custom children_count method from your Parent class and can still simple call
<%= parent.children_count %>
in your views. Or you can add it to the list of attributes you want to return as JSON:
render json: parents, only: %i[attr1 attr2 children_count]
children.count will call the database, yes; however, it will do it as a SQL count:
SELECT COUNT(*) FROM "children" WHERE "children"."parent_id" = $1
It doesn't actually load all of the child records. A more efficient method is to use a Rails counter_cache for this specific case: https://guides.rubyonrails.org/association_basics.html#options-for-belongs-to-counter-cache
Related
I am using Rails 5 and I want to be able to filter a one-to-many relationship to only send a subset of the child items to the client. The data model is pretty standard, and looks something like this:
class Parent < ApplicationRecord
has_many :children, class_name: 'Child'
end
class Child < ApplicationRecord
belongs_to :parent
end
When the client makes a call, I only want to return some of the Child instances for a Parent.
This is also complicated because the logic about which Child objects should be returned is absurdly complicated, so I am doing it in Ruby instead of the database.
Whenever I execute something like the following, Rails is attempting to update the database to remove the association. I don't want the database to be updated. I just want to filter the results before they are sent to the client.
parent.children = parent.children.reject { |child| child.name.include?('foo') }
Is there a way to accomplish this?
Add an instance method in Parent model
def filtered_children
children.where.not("name like ?", '%foo%')
end
Call filtered_children wherever required, it doesn't make sense to reset the existing association instance variable. The same queries are cached so it doesn't matter if you call them one time or multiple times. But you can always memoize the output of a method to make sure the the method is not evaluated again second time onwards,
def filtered_children
#filtered_children ||= children.where.not("name like ?", '%foo%')
end
Hope that helps!
DB update is happening because filtered records are being assigned back to parent.children. Instead another variable can be used.
filtered_children = parent.children.reject { |child| child.name.include?('foo') }
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)
I have and object with a nested model. I am currently getting all the nested objects like so:
#no = Parent.find(params[:parent_id]).children
Now, one of these children has an attribute that identifies them as the favorite. How can I get the favorite child from among the children?
In addition, how can I edit the attributes using fields_for for just that single object in the view/update?
I don't know the name of your attribute that identifies the record as the favorite, but let's say it is a boolean named is_favorite. Considering this abose, the following should work:
children = Parent.find(params[:parent_id]).children
#favorited_children = children.where(is_favorite: true) # return 0..N records! not only 0..1 !
To edit its attributes, you can do as following (you will have to translate it in ERB or HAML, depending on what your app uses):
form_for #favorited_children do |form_builder|
form_builder.text_field :name
form_builder.check_box :is_favorite
end
Hope this helps!
You could also look at using an ActiveRecord Association Extension
This basically works by creating instance methods you can chain onto the child association, like so:
#app/models/parent.rb
Class Parent < ActiveRecord::Base
has_many :children do
def favorites
where(is_favorite: true) #-> to use MrYoshi's example
end
end
end
This will allow you to use the following:
#parent = Parent.find params[:id]
#favorites = #parent.children.favorites
I am using Ruby on Rails 3.2.2 and I have the following has_many :through association in order to "order articles in categories":
class Article < ActiveRecord::Base
has_many :category_associations # Association objects
has_many :associated_categories, :through => :category_associations # Associated objects
end
class CategoryAssociation < ActiveRecord::Base
acts_as_list :scope => 'category_id = #{category_id} AND creator_user_id = #{creator_user_id}'
belongs_to :associated_article
belongs_to :creator_user, :foreign_key => 'creator_user_id'
end
On retrieving associated_categories I would like to load category_associations objects created by a user (note: the creator user is identified by the creator_user_id column present in the category_associations database table) because I need to display position values (note: the position attribute, an Integer, is required by the act_as_list gem and it is a column present in the category_associations database table) "near" each article title.
Practically speaking, in my view I would like to make something like the following in a proper and performant way (note: It is assumed that each article in #articles is "category-associated" by a user - the user refers to the mentioned creator user of category_associations):
<% #articles.each do |article| %>
<%= link_to(article.title, article_path(article)) %> (<%= # Display the article position in the given category %>)
<% end %>
Probably, I should "create" and "handle" a custom data structure (or, maybe, I should make some else...), but I do not how to proceed to accomplish what I am looking for.
At this time I am thinking that the eager loading is a good approach for my case because I could avoid the N + 1 queries problem since I have to state further conditions on association objects in order to:
retrieve specific attribute values (in my case those refer to position values) of association objects created by a given user;
"relate" (in some way, so that position values are suitable for displaing) each of those specific attribute values to the corresponding associated object.
I think, you are looking for this
#articles = Article.includes(:associated_categories)
This will eager load all your articles including both of its associations (associated_categories, associated_categories). Thus, it will avoid N+1 problem and wont fire queries when you iterate over #articles and its associations in your view.
Relating to my last question here: Rails: Finding all associated objects to a parent object
Is it possible to sort multiple separate child objects in Rails by creation date, and then list them? Using the previous example I have a resume with two different has_many child objects, I would like to fetch them and then sort them based on creation date and then use that to display them.
I assume that you have two (or more) seperate models for children objects, so your Parent model looks like this:
class Parent < ActiveRecord::Base
has_many :dogs
has_many :cats
end
To sort them and get them generally as children you can write method (similar to #gertas answer):
def children
#children ||= (self.dogs.all + self.cats.all).sort(&:created_at)
end
and put it in Parent model. Then you can use it in controller:
#parent = Parent.find(params[:id])
#children = #parent.children
Now we'll try to display them in a view. I assume that you have created two partials for each model _cat.html.erb and _dog.html.erb. In view:
<h1>Children list:</h1>
<% #parent.children.each do |child| %>
<%= render child %>
<% end %>
It should automaticaly find which partial should be used, but it can be used only if you follow Rails way. If you want to name partials in different way, or store it in different directory, then you would have to write your own methods that will choose correct partial based on type od object.
You can add an accessor method on your parent model:
class Parent < ActiveRecord::Base
def sorted_children
children.scoped( :order => 'created_at DESC' )
# or, in rails3:
# children.order('created_at DESC')
end
end
If the natural order for your child model is the date field and you would like to do that everywhere, then just set a default scope on it:
class Child < ActiveRecord::Base
default_scope :order => 'created_at DESC'
end
As child objects are in different types and they are fetched separately (separate has_many) you have to do sorting in Ruby:
sorted_childs=(#resume.child1_sections.all + #resume.child2_sections.all).sort(&:created_at)
Otherwise you would need to introduce table inheritance with common columns in parent. Then it would be possible to have another has_many for all children with :order.