Multiple counter_cache in Rails model - ruby-on-rails

I'm learning Rails, and got into a little problem. I'm writing dead simple app with lists of tasks, so models look something like that:
class List < ActiveRecord::Base
has_many :tasks
has_many :undone_tasks, :class_name => 'Task',
:foreign_key => 'task_id',
:conditions => 'done = false'
# ... some validations
end
Table for List model has columns tasks_counter and undone_tasks_counter.
class Task < ActiveRecord::Base
belongs_to :list, :counter_cache => true
# .. some validations
end
With such code there is attr_readonly :tasks_counter for List instances but I would like to have a counter for undone tasks as well. Is there any way of having multiple counter cached automagically by Rails.
So far, I've managed to create TasksObserver that increments or decrements Task#undone_tasks_counter, but maybe there is a simpler way.

Have you tried it with a custom-counter-cache column?
The doc here:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
It suggests that you can pass a column-name to the counter_cache option, which you may well be able to call twice eg
belongs_to :list, :counter_cache => true # will setup tasks_count
belongs_to :list, :counter_cache => :undone_tasks_count
Note: not actually tested.

ez way.
1) first counter - will do automatically
2) Manually "correct"
AnotherModelHere
belongs_to :user, counter_cache: :first_friends_count
after_create :provide_correct_create_counter_2
after_destroy :provide_correct_destroy_counter_2
def provide_correct_create_counter_2
User.increment_counter(:second_friends_count, another_user.id)
end
def provide_correct_destroy_counter_2
User.decrement_counter(:second_friends_count, another_user.id)
end

Most probably you will need counter_culture gem, as it can handle counters with custom conditions and will update counter value not only on create and destroy, but for updates too:
class CreateContainers < ActiveRecord::Migration[5.0]
create_table :containers, comment: 'Our awesome containers' do |t|
t.integer :items_count, default: 0, null: false, comment: 'Caching counter for total items'
t.integer :loaded_items_count, default: 0, null: false, comment: 'Caching counter for loaded items'
end
end
class Container < ApplicationRecord
has_many :items, inverse_of: :container
has_many :loaded_items, -> { where.not(loaded_at: nil) },
class_name: 'Item',
counter_cache: :loaded_items_count
# Notice that you can specify custom counter cache column name
# in has_many definition and AR will use it!
end
class Item < ApplicationRecord
belongs_to :container, inverse_of: :items, counter_cache: true
counter_culture :container, column_name: proc { |model| model.loaded_at.present? ? 'loaded_items_count' : nil }
# But this column value will be handled by counter_culture gem
end

I'm not aware of any "automagical" method for this. Observers seems good for this, but I personally prefer using callbacks in model (before_save, after_save).

Related

Prevent from raising ActiveRecord::RecordInvalid or adding twice on has_many association

I want to change has_many association behaviour
considering this basic data model
class Skill < ActiveRecord::Base
has_many :users, through: :skills_users
has_many :skills_users
end
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, validate: true
has_many :skills_users
end
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
end
For adding a new skill we can easily do that :
john = User.create(name: 'John Doe')
tidy = Skill.create(name: 'Tidy')
john.skills << tidy
but if you do this twice we obtain a duplicate skill for this user
An possibility to prevent that is to check before adding
john.skills << tidy unless john.skills.include?(tidy)
But this is quite mean...
We can as well change ActiveRecord::Associations::CollectionProxy#<< behaviour like
module InvalidModelIgnoredSilently
def <<(*records)
super(records.to_a.keep_if { |r| !!include?(r) })
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
to force CollectionProxy to ignore transparently adding duplicate records.
But I'm not happy with that.
We can add a validation on extra validation on SkillsUser
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
validates :user, uniqueness: { scope: :skill }
end
but in this case adding twice will raise up ActiveRecord::RecordInvalid and again we have to check before adding
or make a uglier hack on CollectionProxy
module InvalidModelIgnoredSilently
def <<(*records)
super(valid_records(records))
end
private
def valid_records(records)
records.with_object([]).each do |record, _valid_records|
begin
proxy_association.dup.concat(record)
_valid_records << record
rescue ActiveRecord::RecordInvalid
end
end
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
But I'm still not happy with that.
To me the ideal and maybe missing methods on CollectionProxy are :
john.skills.push(tidy)
=> false
and
john.skills.push!(tidy)
=> ActiveRecord::RecordInvalid
Any idea how I can do that nicely?
-- EDIT --
A way I found to avoid throwing Exception is throwing an Exception!
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, before_add: :check_presence
has_many :skills_users
private
def check_presence(skill)
raise ActiveRecord::Rollback if skills.include?(skill)
end
end
Isn't based on validations, neither a generic solution, but can help...
Perhaps i'm not understanding the problem but here is what I'd do:
Add a constraint on the DB level to make sure the data is clean, no matter how things are implemented
Make sure that skill is not added multiple times (on the client)
Can you show me the migration that created your SkillsUser table.
the better if you show me the indexes of SkillsUser table that you have.
i usually use has_and_belongs_to_many instead of has_many - through.
try to add this migration
$ rails g migration add_id_to_skills_users id:primary_key
# change the has_many - through TO has_and_belongs_to_many
no need for validations if you have double index "skills_users".
hope it helps you.

Polymorphic association confusion, get resource through association

I might be totally confused and off target here.
I have an Event model, which has_one a Timeline that belongs_to Event. The Timeline has_many TimelineItems that belong_to Timeline.
def Event
has_one :timeline
end
def Timeline
belongs_to :event
has_many :timeline_items
end
def TimelineItem
belongs_to :timeline
end
create_table :admin_timeline_items do |t|
t.references :admin_timeline
t.references :user
t.references :resource, :polymorphic => true
t.string :method
end
So through from a TimelineItem object I would be able to generate for example this output:
eml created the Post "Hi this is from eml" on [DATE]
The title "Hi this is from eml" would come from the Post object's .title. So eg. this TimelineItem has these settings
item.user = User.where(:username => "eml")
item.resource_type = "Post"
item.resource_id = 1
item.created_at = 27/082...13:37
So the problem is fetching said Post object, or rather the correct syntax for this particular association. Seems terribly simple to me, but I am not finding the information I need to properly write it.
Thanks for your help & time.
Sorry... I was just confusing myself because I had set resource_id in my own test item to a faulty value. My first guess was correct. It is quite simple:
class TimelineItem < ActiveRecord::Base
belongs_to :timeline
belongs_to :user
belongs_to :resource, :polymorphic => true
end
Access resource like so:
TimelineItem.first.resource
=> Post(....)

Rails joins or preload belongs_to association from polymorphic model

my problem is following. How can I joins belongs_to association from polymorphic model
There is situation
opinion.rb
class Opinion < ActiveRecord::Base
belongs_to :opinionable, :polymorphic => true
belongs_to :category
end
answer.rb
class Answer < ActiveRecord::Base
has_many :opinions, :as => :opinionable
end
How can i do following
Opinion.joins(:opinionabe).all
it will throw
ArgumentError: You can't create a polymorphic belongs_to join without specifying the polymorphic class!
How can i specific which class i want to join?
Second question. How to preload it?
Opinion.preload(:opinionable).all
works fine. It will do query for each class in belongs_to.
But. if i want to do something like
Opinion.preload(:opinionable => :answer_form).all
there is problem because one model has this association and second hasn't. So it will throw exception.
So how i can do something like
Opinion.preload(:answer => :answer_form, :another_belongs_to_model).all
?
Thanks, David!
Actually if you just do
belongs_to :opinionable_answer, :foreign_key => :opinionable_id, :class_name => "Answer", conditions: { opinions: { opinionable_type: "Answer"}}
then you can do
Opinion.joins(:opinionable_answer).where(answers: { awesome: true})
It looks like you have not specified opinionable_type:string column for your Opinion model.
Try to update your migration in this manner:
class CreateOpinions < ActiveRecord::Migration
def self.up
create_table :opinions do |t|
t.integer :opinionable_id
t.string :opinionable_type
# ... other fields
t.timestamps
end
end
def self.down
drop_table :opinions
end
end
This will solve your second question and Opinion.preload(:opinionable).all should work well.
You cann't do joins on polymorphic association because they can be located in different tables, which are detected after Opinion model is loaded. That why model needs column opinionable_type.
If you try to do this you'll get next exception
ActiveRecord::EagerLoadPolymorphicError: Can not eagerly load the polymorphic association :opinionable
UPD: Added magic join ^_^
class Opinion < ActiveRecord::Base
belongs_to :opinionable, :polymorphic => true
belongs_to :opinionable_answer, :foreign_key => :opinionable_id, :class_name => "Answer"
scope :by_type, lambda { |type| joins("JOIN #{type.table_name} ON #{type.table_name}.id = #{Opinion.table_name}.opinionable_id AND #{Opinion.table_name}.opinionable_type = '#{type.to_s}'") }
end
Example:
Opinion.by_type(Answer).to_sql
=> "SELECT \"opinions\".* FROM \"opinions\" JOIN answers ON answers.id = opinions.opinionable_id AND opinions.opinionable_type = 'Answer'"
I know this question is old but I just spent an hour looking for the solution to a similar problem (Rails 3) and the only way I got it to work was the solution stated here: https://stackoverflow.com/a/25966630/6878997
Which, in your case would be:
class Opinion < ActiveRecord::Base
# The true polymorphic association
belongs_to :opinionable, polymorphic: true
# The trick to solve this problem
has_one :self_ref, :class_name => self, :foreign_key => :id
has_one :answer, :through => :self_ref, :source => :opinionable, :source_type => Answer
end
Seems tricky but this way you will be able to do multiple chained joins such as:
joins(answer: :other_model).
And whenever opinion.opinionable is not an Answer, opinion.answer will return nil.
Hope it helps somebody!

Accessing singular_association_ids from model in Rails

I've been using the association_collection method "other_ids" throughout my Rails app with no issues. However whenever I try to access it from within the model defining the association, Rails has no idea what I'm taking about. For example:
class Membership < ActiveRecord::Base
belongs_to :course, :touch => true
belongs_to :person, :touch => true
end
class Day < ActiveRecord::Base
belongs_to :course, :touch => true, :counter_cache => true
has_many :presents, :dependent => :delete_all
has_many :people, :through => :presents
before_destroy :clear_attendance
def clear_attendance
mems = Membership.where(:course_id => course.id, :person_id => person_ids)
mems.update_all(["attendance = attendance - ?", (1 / course.days.size.to_f)])
end
end
In this case, person_ids is always null. I've tried self.person_ids, people.ids, etc. All nothing. I have used day.person_ids elsewhere with no issues, so why can't I use it here?
I am using Ruby 1.9.1 and Rails 3.0.3. Here is the SQL call from my log:
[1m[36mAREL (0.0ms)[0m [1mUPDATE "memberships" SET attendance = attendance - 0.3333333333333333 WHERE ("memberships"."course_id" = 4) AND ("memberships"."person_id" IN (NULL))[0m
edit: added more code to clarify question
What you really want there is:
def a_method
self.people.all
end
But to answer your question, person_ids is the correct method, and it should return an empty array, not nil. I just tried an association like that out in 2.3.10. Maybe you can post some more of your code, rails version, etc.
Thanks for your help - I figured it out myself. The problem was the order of my callbacks. I was trying to call person_ids after the association had been deleted. Changing the order to this solved my issues.
class Day < ActiveRecord::Base
before_destroy :clear_attendance
belongs_to :course, :touch => true, :counter_cache => true
has_many :presents, :dependent => :delete_all
has_many :people, :through => :presents

transitive rails associations and magical count

In Rails, to automatically count associations, you do:
class Script
has_many :chapters
end
class Chapter
belongs_to :script
end
and you add a chapters_count column into the Script model.
Now, what if you want to count the number of paragraphs in a Script without having a script_id key in the paragraph model ?
class Script
has_many :chapters
has_many :paragraphs # not complete
end
class Chapter
has_many :paragraphs
belongs_to :script
end
class Paragraph
belongs_to :chapter
end
How do you automatically associate script to paragraph and count them using the automatic count of Rails ?
You're on the right track. But first you've got to address a small error. Rails won't update a counter cache unless you instruct it to.
class Chapter
belongs_to :script, :counter_cache => true
end
Will automatically update #script.chapter_count before creation and after destruction of all associated Chapters.
Unfortunately things aren't so simply when dealing :through relationships. You will need to update the associated script's paragraph counter through callbacks in the Paragraph model.
N.B.: The following assumes you want to keep a paragraph counter in Chapter as well.
Start by applying the same theory to the Chapter model, and a paragraphs count column to the Script table.
class PrepareForCounterCache < ActiveRecord::Migration
def self.up
add_column :scripts, :paragraphs_count, :integer, :default => 0
add_column :chapters, :paragraphs_count, :integer, :default => 0
Chapter.reset_column_information
Script.reset_column_information
Chapter.find(:all).each do |c|
paragraphs_count = c.paragraphs.length
Chapter.update_counters c.id, :paragraphs_count => paragraphs_count
Script.update_counters c.script_id, :paragraphs_count => paragraphs_count
end
end
def self.down
remove_column :scripts, :paragraphs_count
remove_column :chapters, :paragraphs_count
end
end
Now to set up the relationships:
class Script
has_many: chapters
has_many: paragraphs, :through => :chapters
end
class Chapter
has_many: paragraphs
belongs_to :script, :counter_cache => true
end
class Paragraph
belongs_to :chapter, :counter_cache => true
end
All that's left is to tell Paragraph to update the paragraph counters in script as a callback.
class Paragraph < ActiveRecord::Base
belongs_to :chapter, :counter_cache => true
before_save :increment_script_paragraph_count
after_destroy, :decrement_script_paragraph_count
protected
def increment_script_paragraph_count
Script.update_counters chapter.script_id, :paragaraphs_count => 1
end
def decrement_script_paragraph_count
Script.update_counters chapter.script_id, :paragaraphs_count => -1
end
end
The quick and simple way, without using a cache is to do:
class Script
has_many :chapters
has_many :paragraphs, :through => :chapters
end
script = Script.find(1)
puts script.paragraphs.size #get the count

Resources