Counter Cache on a condition not on association - ruby-on-rails

I've read on counter caches on associations.
Is there a way to easily implement (via a gem that will do the heavy lifting) of a counter cache for some condition? for example:
usually a counter cache would be
class User
has_many :messages
class Message
belongs_to :user, counter_cache: true
however, lets say that I don't want to count how many messages, but to count the total number of characters in all of the messages from Joe
so lets say I have a method count_chars_from(user) that returns the number of chars from a user
I want to update a specific column when many thing occur in the system (when joe sends a message to several people - all of those users need to be updated, when joe edits a message to one person, etc)
This could be done with observers I guess, but I see myself creating ugly code very quickly.
Is there a way to have something like the above?

Currently, there is no special methods/helpers for this. Rails doesn't support conditional counters.
However, you can simply define your own.
class Message
belongs_to :user
after_create :inc_counters
after_destroy :dec_counters
private
def inc_counters
if user && your_conditions
user.increment(:conditional_counter)
end
end
def dec_counters
if user && your_conditions
user.decrement(:conditional_counter)
end
end
end
I'd suggest take a look at counter_cache implementation. It looks pretty complex, but simple implementation works in one of my project pretty well.
Also consider to use update_column to not trigger validations and callbacks on User model.
And test you code when you delete parent model if it has dependent: :destroy on association.

Related

Rails: Tight coupling between deeply nested models: how to uncouple them for tests and DB efficiency?

I have the following models and associations:
SuccessCriterion
has_many :requirements
has_many :findings, through: :requirements
Requirement
belongs_to :success_criterion
has_many :findings
Finding
belongs_to :requirement
has_one :success_criterion, through: :requirement
Each finding can have a status critical
If a requirement has at least one critical finding, its status is critical, too
And if a success criterion has at least one critical requirement, its status is critical, too
So the critical status is inherited.
When showing a list of success criteria, I want to show whether its status is critical or not. To do that, I calculate the status by iterating through all requirements and there again iterating through all findings and searching for at least one critical finding.
This takes a lot of DB queries, so I'm thinking about caching the status in the success criterion, and every time I'm adding/modifying/deleting a finding, the status should be updated in the success criterion.
What's the best way to do that? I thought about something like an after_save filter like this:
model Finding
belongs_to :requirement
has_one :success_criterion, through: :requirement
after_save :update_success_criterion_status
private
def update_success_criterion_status
if status.critical?
success_criterion.update_attribute :status, :critical
else
success_criterion.calculate_status! # Iterate through all findings and look for at least one critical
end
end
end
In development, this would work well, I guess.
But what about testing? For unit testing the success criterion, I would have to provide the necessary associations for each and every single test, otherwise the after_save filter would crash. This means a lot of overhead for each test.
For sure, I could do something hacky like
after_save :update_success_criterion_status, unless: -> { Rails.env.test? }
but I don't think this is a good way.
Is there a better way to do this? Or am I going completely the wrong route? Maybe there are even gems that handle stuff like this (modifying attributes of associated resources)?
PS
A similar requirement would be the possibility to counter cache a deeply associated element. Let's say, we have another model, e.g.
Project
has_many :success_criteria
When we want to know how many findings there are in a project, we have to count them through both success criteria, requirements, and findings. A counter cache would save a lot of queries here, too. But there would need to be taken care of a lot of create/update/delete stuff of all the associated models to update the counter cache...
I think the best way would be to create another model that all associated models can efficiently look at. You can create a Status model with boolean columns for the different status types—at least one of them being the critical column, obviously—in which you store the count.
You will be able to retrieve any model's status with <model_instance>.status and with that, you can find out if it's critical or not. For example, finding.status.critical?, or a_success_criterion.status.critical? or even a_success_criterion.critical?.
model Finding
belongs_to :requirement
has_one :success_criterion, through: :requirement
has_one :status
def critical?
status.critical? # Note: Rails should define this method for you on the status because it's a boolean column
end
end
Similarly, to retrieve a Requirements's status you would just do so with requirement.status and requirement.critical? to figure out if the requirement is critical or not.
model Requirements
belongs_to :success_criterion
has_many :findings
has_many :statuses
def critical?
return true if statuses.where(critical: true).any?
end
end
And to retrieve a SuccessCriterion's status you would just do so with success_criterion.status because:
model SuccessCriterion
has_many :requirements
has_many :findings, through: :requirements
has_many :statuses
def critical?
return true if statuses.where(critical: true).any?
end
end
A critical part (no pun intended—initially:) is when you create a finding and therefore a status: you must or should give the status the ids of the the finding, requirement, and success criterion that it belongs to, so you might want to add a validation for their presence. Additionally, you might want to add a before_validation upon creating a status, either in Status or in Finding, which would look something like this (in Status):
model Status
belongs_to :finding, :requirement, success_criterion
before_validation :populate_ids
validates_presence_of :finding_id, :requirement_id, :success_criterion_id
def populate_ids
self.finding_id = finding.id
self.requirement_id = finding.requirement.try(:id)
self.success_criterion_id = finding.requirement.try(:success_criterion).try(:id)
end
end
In your tests, you just have to provide integers for the ids but they don't have to be of actual related models if you don't want to test for such relations—this is the beauty of it, aside from efficient querying :).
You get the general idea. I hope this helps!

counter_cache in Rails on a scoped association

I have User model which has_many :notifications. Notification has a boolean column seen and a scope called :unseen which returns all notifications where seen is false.
class User < ApplicationRecord
has_many :notifications
has_many :unseen_notifications, -> { unseen }, class_name: "Notification"
end
I know that I can cache the number of notifications if I add a column called notifications_count to users and add counter_cache: true to my belongs_to call in Notification.
But what if I want to cache the number of unseen notifications a user has? I.e. cache unseen_notifications.size instead of notifications.size? Is there a built-in way to do this with counter_cache or do I have to roll my own solution?
According to this blog post, there is no built-in way to use counter caches for scoped associations:
ActiveRecord’s counter cache callbacks only fire when creating or destroying records, so adding a counter cache on a scoped association won’t work. For advanced cases, like only counting the number of published responses, check out the counter_culture gem.
Other than using an external gem, you could roll your own solution by adding callbacks to your Notification model and adding an integer column (e.g., unseen_notifications_count) to the User model:
# Notification.rb
after_save :update_counter_cache
after_destroy :update_counter_cache
def update_counter_cache
self.user.unseen_notifications_count = self.user.unseen_notifications.size
self.user.save
end
Also see the answers to Counter Cache for a column with conditions? for additional implementation examples of this approach.

Creating reminders/notifications in Rails

We're building an intranet in Ruby on Rails and now we want to add functionality for having reminders when you should have done something but haven't.
For example, say you have a model, Meeting, I want to send reminders to everyone who has had a meeting but where the meeting report is empty (meeting.date < Date.today && meeting.report == ""). And a project model where you have some other criteria (maybe project.last_report.date < lastMonday && !project.closed)
Now, I can imagine creating these reminders with some kind of rake task and then removing them by some event trigger when you update Meeting or whatever, but that seems like I'll get spaghetti code everywhere.
My second idea is to make a separate module that, on each page load, fetches all the entries that could be related and runs all these checks, and then returns Reminders, however, this will probably be hella slow to hit the database like this. (Maybe caching would be an option but still)
So, anybody done something like this and have any ideas on how to solve our problem?
Thanks!
I can't see any issue with spaghetti code if you let each object that requires a Reminder to manage it's own reminders. If you want to be an OOP purist you could probably create a separate class (e.g., MeetingReminderManager in your example) to manage the reminders but that seems like overkill here. Consider...
class Reminder
belongs_to :source, polymorphic: true
belongs_to :user
end
class Meeting
has_many :reminders, as: :source
has_many :users
after_create :build_reminders, if: ->{|meeting| meeting.report.blank? }
after_update :destroy_reminders, if: ->{|meeting| !meeting.report.blank? }
private
def build_reminders
users.each{|user| self.reminders.create user_id: user.id, due_on: self.date }
end
def destroy_reminders
self.reminders.delete_all
end
end
I don't see a problem with spaghetti and background job in ruby on rails. I think making them is the path to go. Check whatever is suit you: http://railscasts.com/?tag_id=32

Ruby on Rails Increment Counter in Model

I'm attempting to increment a counter in my User table from another model.
class Count < ActiveRecord::Base
belongs_to :user
after_create :update_count
def update_count
user = User.find(self.user_id)
user.increment(:count)
end
end
So when count is created the goal would be to increment a counter column for that user. Currently it refuses to get the user after creation and I get a nil error.
I'm using devise for my Users
Is this the right (best practice) place to do it? I had it working in the controllers, but wanted to clean it up.
I'm very inexperienced with Model callbacks.
If User has many Counts and Count belongs to User (like it seems to be), then you might want to use a counter cache. It does exactly what you want to do, and it is built-in into ActiveRecord.
I think a better place for this would be using an observer that listens for the on_create for User objects, and then runs this logic.
Something like:
class UserObserver < ActiveRecord::Observer
def after_create(user)
Counter.find_by_name("user_count").increment
end
end
If you would like more extensible counter caches, check out counter_culture. It supports basic counter cache functionality, but also allows you to create counters of records that meet various conditions. For example, you could easily create an inactive user count with code like this:
class Product < ActiveRecord::Base
belongs_to :category
counter_culture :category, :column_name => \
Proc.new {|model| model.inactive? ? 'inactive_count' : nil }
end

Removing the :has_many when the :belongs_to is updated/destroyed if the :has_many is now empty

I have states who have many cities (belongs_to :state) who have many businesses (belongs_to :city).
State also… has_many :businesses, :through => :cities
On my site everything is managed from the Business perspective. When a new Business is created/updated the state/city is created if it doesn't already exist. This happens in a :before_save call.
I'm having problems removing States/Cites when a Business gets updated. If the state/city that a business is in gets changed (again this happens from an edit business form) and the old state/city no longer has any businesses I want to destroy it. I've tried doing this in after_save calls but they're wrapped in a transaction and even if I assign variables to the names of the old state/city, they seem to get changed to the new state/city sometime during the transaction. It's crazy! I used "puts" calls to print the vars in some spots in my Business model and watched the vars change during a save. It was frustrating.
So, right now I'm handling this from the controller but it feels hackish.
Here's some of my code.
http://pastie.org/648832
Also, I'd love any input on how better to structure this whole thing.
Thanks
You want after_destroy callbacks to destroy the has many side of a relationship if it has none.
To ensure this behaviour after an update, we need to use the ActiveRecord::Dirty methods. Which are built into rails as of 2.1. If you're running an older version you'll need the Dirty plugin
class Business < ActiveRecord::Base
...
after_update :destroy_empty_city
after_destroy :destroy_empty_city
protected
def destroy_empty_city
c = city_changed? ? city_was : city
c.destroy if c.businesses.empty?
end
end
class City < ActiveRecord::Base
...
after_destroy :destroy_empty_state
protected
def destroy_empty_state
state.destroy if state.businesses.empty?
end
end
You might need to check if city/state.businesses == [self] instead of city/state.businesses.empty? if your associations are eager loaded. I can't remember how rails treats associations after destroy. I'm assuming that if they're eager loaded than the code above won't work and you will need the alternate check. Otherwise it should be fine.

Resources