Efficient way to count associated objects in Rails 4 - ruby-on-rails

I am looking for a way to show a count of how many images there are for a category but obtained through a has_many association. I have been reading a little on counter_cache but as yet no joy on an implementation
class Category < ActiveRecord::Base
has_many :image_categories
has_many :images, through: :image_categories
end
class ImageCategory < ActiveRecord::Base
# Holds image_id and category_id to allow multiple categories to be saved per image, as opposed to storing an array of objects in one DB column
belongs_to :image
belongs_to :category
end
class Image < ActiveRecord::Base
# Categories
has_many :image_categories, dependent: :destroy
has_many :categories, through: :image_categories
end
Controller
#categories = Category.all
View
<% #categories.each do |c| %>
<li>
<%= link_to '#', data: { :filter => '.' + c.name.delete(' ') } do %>
<%= c.name %> (<%= #count here %>)
<% end %>
</li>
<% end %>

A couple important things to consider with counter_cache:
Certain Rails methods can update the database while bypassing callbacks (for instance update_column, update_all, increment, decrement, delete_all, etc.) and can cause inconsistent values for a counter cache. Same applies to any database changes outside of Rails.
Creating/deleting a child model always requires updating the parent. To ensure consistency of the counter cache Rails uses an additional DB transaction during this update. This usually isn't a problem but can cause database deadlocks if your child model is created/deleted frequently, or if the parent model is updated frequently. (http://building.wanelo.com/2014/06/20/counter-cache-a-story-of-counting.html)
These problems will be exacerbated since you're using a counter cache across a join table.
If you want to do an efficient dynamic count, that's always up to date, then you can use a custom select with a grouped join:
#categories = Category.select("categories.*, COUNT(DISTINCT images.id) AS images_count").joins(:images).group("categories.id")
<% #categories.find_each do |c| %>
<li>
<%= link_to '#', data: { :filter => '.' + c.name.delete(' ') } do %>
<%= c.name %> (<%= c.images_count # <- dynamic count column %>)
<% end %>
</li>
<% end %>
The cost of this grouped join should be very small provided your foreign keys are indexed, and I'd strongly consider taking this approach if you need images_count to always be consistent with the true value, or if images are frequently being created or destroyed. This approach may also be easier to maintain in the long run.

Since you are looking for an efficient way, i would suggest using counter_cache
Here is how your models should look like:
class Category < ActiveRecord::Base
has_many :image_categories
has_many :images, through: :image_categories
end
class ImageCategory < ActiveRecord::Base
# Holds image_id and category_id to allow multiple categories to be saved per image, as opposed to storing an array of objects in one DB column
belongs_to :image, counter_cache: :category_count
belongs_to :category, counter_cache: :image_count
end
class Image < ActiveRecord::Base
# Categories
has_many :image_categories, dependent: :destroy
has_many :categories, through: :image_categories
end
You'll need to add image_count field to your categories table and category_count in images table.
Once you are done adding the counters and fields, you'd need to reset the counters so that the fields are updated with the correct count values for the records already present in your db.
Category.find_each { |category| Category.reset_counters(category.id, :images) }
Image.find_each { |image| Image.reset_counters(image.id, :categories) }

Related

Rails display name attribute from different table along with count from join table

I have the following tables: Products, Categories, ProductCategory.
I'm trying to display in a branch structure something like:
Accessories - 5,000
Men's Watches - 2,000
Tag Huer - 1,000
Samsung - 1,000
Women's Watches - 3,000
The issue I'm getting into is that my query is slowing down the application. I have the following query:
def category_level
Connector::Category.includes(:products).group_by(&:parent_category_id)
end
Then in my views I have the following:
<ul class="list-style-upper">
<% category_level[nil].each do |root| %>
<%= render 'products/submenu/category_item', category: root %>
<% end %>
</ul>
Which loads into:
<li class="list-one">
<%= category.name %><p><%= category.products.count %></p>
<% if category_level[category.id].present? %>
<ul class="list-style-upper-sub">
<% category_level[category.id].each do |child| %>
<%= render 'products/submenu/category_item', category: child %>
<% end %>
</ul>
<% end %>
</li>
It displays but takes a long time due to it hitting the Products table and looping through all the products. So to make it easier I thought I'd just hit the ProductCategory page to get a count with the following:
def level_up
#category_counts = Connector::ProductCategory.group(:category_id).count
end
This will actually just display the following:
{54 => 11, 29 => 14, 51 => 19, 10 => 3202}
Although yes 10 would represent Accessories I'd rather see Accessories - 3,202.
Any advice on cleaning this up to pull in the attribute of name?
Provided that your models look something like:
class Category < ApplicationRecord
has_many :product_categories
has_many :products, through: :product_categories
end
class ProductCategory < ApplicationRecord
belongs_to :product
belongs_to :category
end
class Product < ApplicationRecord
has_many :product_categories
has_many :categories, through: :product_categories
end
You can get a count of the associated items by joining and selecting a count of the joined table:
Category.left_outer_joins(:product_categories)
.select('categories.*, count(product_categories.*) AS product_count')
.group('categories.id')
.left_outer_joins was added in Rails 5. For earlier versions use:
.join('LEFT OUTER JOIN product_categories ON product_categories.category_id = category.id')
You can also use .joins(:product_categories) if you don't care about categories without any products.
The count will be available as .product_count on each record.
This can also be achieved by adding a counter cache:
class ProductCategory < ApplicationRecord
belongs_to :product
belongs_to :category, counter_cache: true
end
class AddProductCategoriesCountToCategories < ActiveRecord::Migration[5.2]
def change
add_column :categories, :product_categories_count, :integer, default: 0
end
end
This will store a count in categories.product_categories_count. Counter caches are best used when you have more read than write operations.

How to loop through a joined table

The models I have:
Category:
class Category < ApplicationRecord
has_many :categorizations
has_many :providers, through: :categorizations
accepts_nested_attributes_for :categorizations
end
Provider:
class Provider < ApplicationRecord
has_many :categorizations
has_many :categories, through: :categorizations
accepts_nested_attributes_for :categorizations
end
Categorization:
class Categorization < ApplicationRecord
belongs_to :category
belongs_to :provider
has_many :games, dependent: :destroy
accepts_nested_attributes_for :games
end
Game:
class Game < ApplicationRecord
belongs_to :categorization
end
I need to display the games, that belongs to a specific provider. I tried to do it like:
<% #provider.categorizations.joins(:games).each do |game| %>
<%= game.title %>
<% end %>
It gives me an error: NoMethodError: undefined method 'title' for #<Categorization:0x007f2cf6ee49e8>. So, it loops through the Categorization. What is the best way to loop through the joined games table? Thanks.
First, you should do the request in your controller, or even better call a scope (defined in a model) from the controller.
Do not forget that Active Record is just an ORM, a tool allowing you to manipulate SQL.
With #provider.categorizations.joins(:games) you are not asking for games. You are asking for the categorizations and you do a JOIN with the games table. This joins is usually to allow to filter by games attributes.
To do what you want you should do the following :
#games = Game.joins(:categorization).where('categorization.provider_id = ?',#provider.id)
As you can see, the join do not return categorization, it allow me to use categorization as a filter.
You should always be aware of the SQL generated by Active Record. Look at the SQL query generated in your server's traces.
I'm guessing 'title' is an attribute of games and not categorization, so you either need to return an array of games, or add a select on the end to pull the title attribute into the categorization object, like so:
<% #provider.categorizations.joins(:games).select('dba.games.title').each do |game| %>
<%= game.title %>
<% end %>
Just to add- you shouldn't really be doing this in the view file. I'd go as far as not even doing this in the controller. I tend to encapsulate this sort of logic into a service class, which is instantiated in the controller to return a set of results. The controller should only be passing the result set on, which is then presented by the view.
class Provider < ActiveRecrord::Base
# this could be a scope instead, or in a seperate class which
# the provider model delegates to- whatever floats you boat
def get_games
# you could use pluck instead, which would return an array of titles
categorizations.joins(:games).select('dba.games.title')
end
end
class ProviderController < ApplicationController
def show
provider = Provide.find(params[:id])
#games = provider.get_games
end
end
<% #games.each do |game| %>
<%= game.title %>
<% end %>

Rails calculated value not working on checkboxes

The extra costs column in the Reservation model doesn't get calculated and saved on creating a new reservation. It's getting calculated and saved when the reservation is edited (even without changing any values in the form). It seems the checkboxes values are not being received by the calculate method or something.
Reservation has_many :bookings, has_many :extras, :through => :bookings
Booking belongs_to :extra, belongs_to :reservation
Extra has_many :bookings, has_many :reservations, :through => :bookings
before_save :calculate_extras_cost
def calculate_extras_cost
self.extras_cost = self.extras.sum(:daily_rate) * total_days
end
<%=hidden_field_tag "reservation[extra_ids][]", nil %>
<%Extra.all.each do |extra|%>
<%= check_box_tag "reservation[extra_ids][]", extra.id, #reservation.extra_ids.include?(extra.id), id: dom_id(extra)%>
<% end %>
Use the form collection helpers instead of creating the inputs manually:
<%= form_for(#reservation) do |f| %>
<%= f.collection_check_boxes(:extra_ids, Extra.all, :id, :name) %>
<% end %>
Also make sure you are whitelisting the :extra_ids property.
One other thing to bear in mind when using callbacks is that the parent record must be inserted into the database before the child records! That means you cannot use self.extras.sum(:daily_rate) in the callback because it relies on the child records being in the database.
You could use self.extras.map(&:daily_rate).sum to sum the values of the associated models from memory in Ruby. Another option would be to use association callbacks.

Rails, how to avoid the "N + 1" queries for the totals (count, size, counter_cache) in associations?

I have a these models:
class Children < ActiveRecord::Base
has_many :tickets
has_many :movies, through: :tickets
end
class Movie < ActiveRecord::Base
has_many :tickets
has_many :childrens, through: :tickets
belongs_to :cinema
end
class Ticket < ActiveRecord::Base
belongs_to :movie, counter_cache: true
belongs_to :children
end
class Cinema < ActiveRecord::Base
has_many :movies, dependent: :destroy
has_many :childrens, through: :movies
end
What I need now is in the page of "Cinemas" I wanna print the sum (count, size?) of the childrens just for the movies of that cinemas, so I wrote this:
in the cinemas_controller.rb:
#childrens = #cinema.childrens.uniq
in the cinemas/show.html.erb:
<% #childrens.each do |children| %><%= children.movies.size %><% end %>
but obviously I have bullet gem that alert me for Counter_cache and I don't know where to put this counter_cache because of different id for the movie.
And also without the counter_cache what I have is not what I want because I want a count for how many childrens in that cinema taking them from the tickets from many days in that cinema.
How to?
UPDATE
If in my view I use this code:
<% #childrens.each do |children| %>
<%= children.movies.where(cinema_id: #cinema.id).size %>
<% end %>
gem bullet don't say me anything and every works correctly.
But I have a question: this way of querying the database is more heavy because of the code in the views?
This might help you.
#childrens_count = #cinema.childrens.joins(:movies).group("movies.children_id").count.to_a
You can use includes to load all associations ahead of time. For example:
#childrens = #cinema.childrens.includes(:movies).uniq
This will load all of the children's movies in the controller, preventing the view from needing access to the database in your loop.
You might agree, that the number of movies belongs to a child equals the number of tickets they bought.
That's why you could just cache the number of tickets and show it on the cinemas#show.
You can even create a method to make it more clear.
class Children < ActiveRecord::Base
has_many :tickets
has_many :movies, through: :tickets
def movies_count
tickets.size
end
end
class Ticket < ActiveRecord::Base
belongs_to :movie, counter_cache: true
belongs_to :children, counter_cache: true
end
class Movie < ActiveRecord::Base
belongs_to :cinema
has_many :tickets
has_many :childrens, through: :tickets
end
class Cinema < ActiveRecord::Base
has_many :movies, dependent: :destroy
has_many :childrens, through: :movies
end
And then:
<% #childrens.each do |children| %><%= children.tickets.size %><% end %>
Or
<% #childrens.each do |children| %><%= children.movies_count %><% end %>
But if you want to show the number of tickets for every movie, you definitely need to consider the following:
#movies = #cinema.movies
Then:
<% #movies.each do |movie| %><%= movie.tickets.size %><% end %>
Since you have belongs_to :movie, counter_cache: true, tickets.size won't make a count query.
And don't forget to add tickets_count column. More about counter_cache...
P.S. Just a note, according to conventions we name a model as Child and an association as Children.
Actually is much more simpler than the remaining solutions
You can use lazy loading:
In your controller:
def index
# or you just add your where conditions here
#childrens = Children.includes(:movies).all
end
In your view index.hml.erb:
<% #childrens.each do |children| %>
<%= children.movies.size %>
<% end %>
The code above won't make any extra query if you use size but if you use count you will face the select count(*) n + 1 queries
I wrote a little ActiveRecord plugin some time ago but haven't had the chance to publish a gem, so I just created a gist:
https://gist.github.com/apauly/38f3e88d8f35b6bcf323
Example:
# The following code will run only two queries - no matter how many childrens there are:
# 1. Fetch the childrens
# 2. Single query to fetch all movie counts
#cinema.childrens.preload_counts(:movies).each do |cinema|
puts cinema.movies.count
end
To explain a bit more:
There already are similar solutions out there (e.g. https://github.com/smathieu/preload_counts) but I didn't like their interface/DSL. I was looking for something (syntactically) similar to active records preload (http://apidock.com/rails/ActiveRecord/QueryMethods/preload) method, that's why I created my own solution.
To avoid 'normal' N+1 query issues, I always use preload instead of joins because it runs a single, seperate query and doesn't modify my original query which would possibly break if the query itself is already quite complex.
In You case You could use something like this:
class Ticket < ActiveRecord::Base
belongs_to :movie, counter_cache: true
belongs_to :children
end
class Movie < ActiveRecord::Base
has_many :tickets
has_many :childrens, through: :tickets
belongs_to :cinema
end
class Children < ActiveRecord::Base
has_many :tickets
has_many :movies, through: :tickets
end
class Cinema < ActiveRecord::Base
has_many :movies, dependent: :destroy
has_many :childrens, through: :movies
end
#cinema = Cinema.find(params[:id])
#childrens = Children.eager_load(:tickets, :movies).where(movies: {cinema_id: #cinema.id}, tickets: {cinema_id: #cinema.id})
<% #childrens.each do |children| %>
<%= children.movies.count %>
<% end %>
Your approach using counter_cache is in right direction.
But to take full advantage of it, let's use children.movies as example, you need to add tickets_count column to children table firstly.
execute rails g migration addTicketsCountToChildren tickets_count:integer,
then rake db:migrate
now every ticket creating will increase tickets_count in its owner(children) by 1 automatically.
then you can use
<% #childrens.each do |children| %>
<%= children.movies.size %>
<% end %>
without getting any warning.
if you want to get children count by movie, you need to add childrens_count to movie table:
rails g migration addChildrensCountToMovies childrens_count:integer
then rake db:migrate
ref:
http://yerb.net/blog/2014/03/13/three-easy-steps-to-using-counter-caches-in-rails/
please feel free to ask if there is any concern.
Based on sarav answer if you have a lot of things(requests) to count you can do:
in controller:
#childrens_count = #cinema.childrens.joins(:movies).group("childrens.id").count.to_h
in view:
<% #childrens.each do |children| %>
<%= #childrens_count[children.id] %>
<% end %>
This will prevent a lot of sql requests if you train to count associated records

How can I create dynamic form field to store/update hash sets in Rails?

In my reservations table I have a rooms (text) field to store hash values such (1 => 3) where 1 is roomtype and 3 corresponds to the amount of rooms booked by the same agent.
My Reservation model
serialize reserved_rooms, Hash
Here is my nested resource
resources :hotels do
resources :roomtypes, :reservations
end
RoomType stores a single room type which belongs to Hotel model. Though I can enlist roomtypes within my reservation form I do not know how I can create a dynamic hash via form to create/update this hash.
I have this but I am looking for a way to create a dynamic hash "key, value" set. Meaning, if Hotel model has two RoomType my hash would be {12 = > 5, 15 => 1} (keys corresponds to the roomtype_ids while values are the amount}
<%= f.fields_for ([:roomtypes, #hotel]) do |ff| %>
<% #hotel.roomtypes.each do |roomtype| %>
<%= ff.label roomtype.name %>
<%= f.select :reserved_rooms, ((0..50).map {|i| [i,i] }), :include_blank => "" %>
<% end %>
<% end %>
What I want is what this website has in the availability section (nr. of rooms):
specs: rails 4.1, ruby 2.1
Note: If you think there is a design problem with this approach (storing reserved_room in a serialized field) I can follow another path by creating another table to store the data.
Might need tweaking but i used similar code with check-boxes and it worked!
<% #hotel.roomtypes.each do |roomtype| %>
<%= f.label roomtype.name %>
<%= f.select :"reserved_rooms[roomtype.id]", ((0..50).map {|i| [i,i] }), :include_blank => "" %>
<% end %>
This gets messy enough that I would probably consider going with a separate models as you mentioned. I would simply do:
class Hotel < ActiveRecord::Base
has_many :room_types
has_many :rooms, :through => :room_types
end
class RoomType < ActiveRecord::Base
has_many :rooms
end
class Room < ActiveRecord::Base
has_many :reservations
belongs_to :room_type
end
class Reservation < ActiveRecord::Base
belongs_to :room
belongs_to :agent
end
class Agent < ActiveRecord::Base
has_many :reservations
end
Then just use a generic form to submit the # Rooms integer, and let your controller handle making multiple reservations...? Maybe I'm not understanding your objective well enough...
Rails 4 has a new feature called Store you would love. You can easily use it to store a hash set which is not predefined. You can define an accessor for it and it is recommended you declare the database column used for the serialized store as a text, so there's plenty of room. The original example:
class User < ActiveRecord::Base
store :settings, accessors: [ :color, :homepage ], coder: JSON
end
u = User.new(color: 'black', homepage: '37signals.com')
u.color # Accessor stored attribute
u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
# There is no difference between strings and symbols for accessing custom attributes
u.settings[:country] # => 'Denmark'
u.settings['country'] # => 'Denmark'

Resources