:counter_cache for total items - ruby-on-rails

I hava a simple set of two related tables of an 'order' that has many 'line_items'. There is also a quantity associated to a line item, e.g.
Order1
line_item a: 'basket weaving for beginners', quantity: 3
line_item b: 'a dummies guide to vampirism', quantity: 1
When I establish the migration I can include the quantity using:
Order.find(:all).each do |o|
o.update_attribute :line_items_count, o.line_items.map(&:quantity).sum
end
which gives me the correct number of items (4), but I don't appear to be able to do it on the Order model because I'm unable to pass in the quantity of line items, and so it just counts the number of line items (2).
So in the line_item model I have:
belongs_to :order, :counter_cache => true
Is there any way I can specify the quantity so that it correctly says 4 instead of 2?

The 'counter_cache` feature to meant to maintain the count(not the sum) of dependent items.
You can easily achieve this by writing few lines of ruby code.
Let us assume that you have a column called line_items_sum in your orders table. The value of this column should default to 0.
class AddLineItemsSumToOrder < ActiveRecord::Migration
def self.up
add_column :orders, :line_items_sum, :integer, :default => 0
end
def self.down
remove_column :orders, :line_items_sum
end
end
class Order < ActiveRecord::Base
has_many :line_items
end
Now add the callback to the LineItem class.
class LineItem < ActiveRecord::Base
validates_numericality_of :quantity
belongs_to :order
after_save :update_line_items_sum
private
def update_line_items_sum
return true unless quantity_changed?
Order.update_counters order.id,
:line_items_sum => (quantity - (quantity_was || 0))
return true
end
end

I think your best bet would be to write your own method for caching the total quantity. If you don't follow the "Rails way" for keeping the counter, you're better off writing your own.

Related

How to traverse associations

I'm finding it tricky to traverse associations in Active Admin.
In my application I have a SupportSession which appears on an Invoice. Through an association with SupportAllocation I can traverse up the chain to get the SupportRate(s) chargeable on a SupportSession. I can use these rates in calculations for the value of the invoice.
My models are:
class Invoice < ApplicationRecord
has_one :support_session
class SupportSession < ApplicationRecord
belongs_to :invoice, optional: true
belongs_to :support_allocation
class SupportAllocation < ApplicationRecord
has_many :support_sessions
has_many :support_rates
class SupportRate < ApplicationRecord
belongs_to :support_allocation
I've created an Active Admin resource for Invoice where I'd like to do some calculations:
ActiveAdmin.register Invoice do
index do
selectable_column
column 'Reference', :reference, :sortable => 'invoices.reference'
column 'Raised', :created_at, :sortable => 'invoices.created_at' do |invoice|
invoice.created_at.strftime('%Y-%m-%d')
end
column 'Due by', :due_by, :sortable => 'invoices.due_by'
column :value do |invoice|
# The next line retrieves an array of SupportRate(s) associated with this SupportSession on this invoice
invoice.support_session.support_allocation.support_rates.each do |support_rate|
# If the current SupportSession's support_type matches the SupportRate's support_type
if support_rate.support_type == invoice.support_session.support_type
# Do some calculations based on the support_rate.price attribute, plus some other bits I've omitted
end
end
# The next line returns the correct price but obviously it's too clumsy and doesn't do the match on support_type
# invoice.support_session.support_allocation.support_rates[0].price
end
actions
end
end
I can see that data is being retrieved correctly. I can also see it is an array. But if I try to do anything with it, e.g. print out support_rate.price within my 'If' condition, I just get (e.g. for the first record):
[#<SupportRate id: 3, support_type: "mentoring", price: 0.3e2, support_allocation_id: 2>, #<SupportRate id: 13, support_type: "study_skills", price: 0.45e2, support_allocation_id: 2>]
In this specific example, the matching support_type was 'study_skills' - I need to use that support_rate's price for my calculations.
I imagine the solution lies in doing some sort of loop through the array or matching rates, as I've attempted? Active Admin doesn't seem to like it though.
Thank you for your help.
I fixed this by calling a query on the associations available (the find_by method):
Invoice.calculate_value(invoice.support_session.support_allocation.support_rates.find_by(support_type: invoice.support_session.support_type).price
I remembered that an association is an object so inherits all the usual query methods available to it.
In order to do the necessary calculations, I just created a custom function that I hand the values to, it then returns a calculated value:
column :value do |invoice|
Invoice.calculate_value(invoice.support_session.support_allocation.support_rates.find_by(support_type: invoice.support_session.support_type).price, invoice.support_session.rounded_duration_mins)
end

How to display Parent record with total number of its Child record in rails

class Attachment < ActiveRecord::Base
belongs_to :user, foreign_key: :creator_id
belongs_to :deal_task, foreign_key: :relation_id
end
class DealTask < ActiveRecord::Base
has_many :attachments, foreign_key: :relation_id
end
I have parent table called DealTask and child table called Attachment
I want a list of DealTask records with associated total number of attachments
DealTask.all.map do |deal_task|
deal_task.
attributes.
with_indifferent_access.
slice(:id, :name).
merge!(total_attachment: deal_task.attachments.count)
end
Or, if you don't care about indifferent access and you don't mind having all the DealTask attributes, you can write this with on a single line:
DealTask.all.map{|deal_task| deal_task.attributes.merge!(total_attachments: deal_task.attachments.count)}
Breaking it down...
DealTask.all.map do |deal_task|
...
end
Is going to return an array. The array will contain the results of the do block.
deal_task.
attributes.
with_indifferent_access
Gives you the attributes of each deal_task in a hash that can be access with strings or symbols (thus, "indifferent_access").
deal_task.
attributes.
with_indifferent_access.
slice(:id, :name)
Keeps only the :id and :name of the deal_task hash.
merge!(total_attachments: deal_task.attachments.count)
Adds the attachments count to your hash with the key total_attachments.
Results should look something like:
[
{id: 1, name: 'name1', total_attachments: 12},
{id: 2, name: 'name2', total_attachments: 3}
]
I found the best solution for Parent child relationship count
counter_cache: true
because all above queries take too much time to load from database
so you all must prefer to use this
1-> Add one column in Parent table called DealTask
rails g migration AddAttachmentsCountToDealTask attachments_count:integer
2-> Open Migration add Edit it
class AddAttachmentCountToDealTask < ActiveRecord::Migration[5.0]
def up
add_column :deal_tasks, :attachments_count, :integer, default: 0
DealTask.reset_column_information
DealTask.find_each do |deal_task|
DealTask.reset_counters deal_task.id, :attachments
end
end
def down
remove_column :deal_tasks, attachments_count
end
end
So when you rollback the migration it will not raise an error or exception
you can also use any loop instead of using
find_each, DealTask.all.each do...end
but yes, While resetting counter Must use class name like
DealTask.reset_counters
3-> Set Counter cache
class Attachment < ActiveRecord::Base
belongs_to :deal_task, foreign_key: :relation_id, counter_cache: true
end
class DealTask < ActiveRecord::Base
has_many :attachments, foreign_key: :relation_id
end
suppose name of your model is sub_tasks than your counter_cache column must be
sub_tasks_count
if you want your own column name than you have to specify that column name in counter_cache
suppose column name is total_subtasks than
belongs_to :deal_task, foreign_key: :relation_id, counter_cache: :total_subtasks
and make changes accordingly for updating counter_cache
now when you Add any Attachment, attachments_count column increase by 1 and this is done automatically by **counter_cache
one Problem is there
** when you delete any child counter_cache is unable to decrease **
so for that solution make a callback
class Attachment < ActiveRecord::Base
belongs_to :deal_task, foreign_key: :relation_id, counter_cache: true
before_destroy :reset_counter
private
def reset_counter
DealTask.reset_counters(self.relation.id, :attachments)
end
end
so when you delete any attachments it will reset countet_cache for its Parent by relation_id which is parent_id or Foreign_key for attachments
for more info
see video on Railscast counter cache 23
Try this
DealTask.all.map { |deal_task| deal_task.attachments.ids }.count
DealTask.first.attachments.count #This will give count of attachemenets
#To show all records and all the count
DealTask.find_each do |dt|
print dt.inspect
print "\n"
print dt.attachments.count
end
Or
DealTask.joins(:attachments).select("deal_tasks.*, count(attachements.id) as count").group("deal_tasks.id")
For much nicer format
DealTask.joins(:attachments)
.select("deal_tasks.id, deal_tasks.name, count(attachements.id) as attachments")
.group("deal_tasks.id")
.collect(&:attributes)
#This will gve you something like
[
{"id"=>34332630, "name"=>"some name", "attachments"=>1},
{"id"=>71649461, "name"=>"some name", "attachments"=>1}
]
This will be lot faster as you get all data in a single query

Active record group by relation and count

I have these classes
class Challenge
has_many :photos
end
class Photo
belong_to :challenge
has_many :votes
end
class Vote
belongs_to :photo
end
I'm trying to get for every photo how many vote I have.
I try with
#challenge.photos.group_by(&:votes)
But the result is not what I need...
To make it easy to fetch the votes count for each photo, you can introduce a new column in :photos names :votes_count and add counter_cache: true in the belongs_to association in the Vote model.
class Vote
belongs_to :photo, counter_cache: true
end
class AddVotesCountToPhotos < ActiveRecord::Migration
def change
add_column :photos, :votes_count, :integer
end
end
Now you can easily query the votes count for any photo:
#photo.votes_count
One way to find the votes for each photo for a particular challenge:
photos_with_votes = #challenge.photos.each_with_object([]) do |photo, arr|
arr << [photo.id, photo.votes_count]
end
By the way, as your tables might already have been populated with records, you need to reset all the counter caches to their correct values using an SQL count query. We will use reset_counters for the purpose:
Photo.pluck(:id).each { |id| Photo.reset_counters(id, :votes) }

Can I create relationship for count?

I have two models
Post
has_many :comments
Comment
belongs_to :post
When I want display a list of posts and it's comment count. I usually include comments in the post like this .
Post.find(:all,:include => :comments)
To display a number of comment for post.
post.comments.size
Can I create a has_many relation which return count of comments ?
has_one :comments_count
Can I include this relationship like this ?
Post.find(:all,:include => :comments_count)
Rails has a counter cache which will automatically update a columns value based on the count of associated items. This will allow you to include the count when you return the posts object. Just add it to the belongs_to on comment.
Comment
belongs_to :post, :counter_cache => true
You'll need to add a new integer column to posts for the counter:
class AddCounterCacheToPosts < ActiveRecord::Migration
def self.up
add_column :posts, :comments_count, :integer
end
end
To answer you question, yes you can; but this is not the most efficient way to do it. Normally you add a column to Post called comments_count and you updated that column every Comment CRUD action.
Add the column:
rails g migration add_comment_count_to_post
Then in that migration add the following line:
add_column :posts, :comments_count, :integer, :default => 0
Then there are two way to handle it from here.
The first is a custom before_save and before_destroy in Comment model.
app/models/comment.rb
class Comment << ActiveRecord::Base
belongs_to :post
before_save :update_comments_count
before_destroy :update_comments_count
def update_comment_count
post.update_attribute(:comment_count, post.comments.count)
end
end
The second way is to use Rails custom helper for this:
app/models/comment.rb
class Comment << ActiveRecord::Base
belongs_to :post, :counter_cache => true
end

Rails: Getting random product within several categories

I have a question about random entries in Rails 3.
I have two models:
class Product < ActiveRecord::Base
belongs_to :category
self.random
Product.find :first, :offset => ( Product.count * ActiveSupport::SecureRandom.random_number ).to_i
end
end
class Category < ActiveRecord::Base
has_many :products
end
I'm able to get a random product within all products using an random offset castet to int. But I want also be able to get random products WITHIN several given categories. I tried something like this, but this doesn't work, because of the offset index:
class Product < ActiveRecord::Base
belongs_to :category
self.random cat=["Mac", "Windows"]
joins(:categories).where(:categories => { :name => cat }).where(:first, :offset => ( Product.count * ActiveSupport::SecureRandom.random_number ).to_i)
end
end
Anybody here who knows a better solution?
thx!
tux
You could try and simplify this a little:
class Product < ActiveRecord::Base
def self.random(cat = nil)
cat ||= %w[ Mac Windows ]
joins(:categories).where(:categories => { :name => cat }).offset(ActiveSupport::SecureRandom.random_number(self.count)).first
end
end
Rails 3 has a lot of convenient helper methods like offset and first that can reduce the number of arguments you need to pass to the where clause.
If you're having issues with the join, where the number of products that match is smaller than the number of products in total, you need to use ORDER BY RAND() instead. It's actually not that huge a deal performance wise in most cases and you can always benchmark to make sure it works for you.
the offset happens after the order, so you can add .order('rand()') then you will be getting random elements from multiple categories. but since the order is random you also do not need offset anymore. so just:
class Product < ActiveRecord::Base
belongs_to :category
self.random cat=["Mac", "Windows"]
joins(:categories).where(:categories => { :name => cat }).order('rand()').first
end
end

Resources