counter_cache in Rails on a scoped association - ruby-on-rails

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.

Related

Model column that depends on other columns

I have a gamification app that has four types of points, and the sum of all these kinds is the total points for a user, I want to be able to do sum and scopes on that column, so I think I should have it as a column in the DB.
scope :points_rank, -> { order(points: :desc) }
I was using a before_save for adding all four point types and storing it in points, but now I'm using a gem that does increment to these types of points, so when it updates those values, the before_save is not called, hence not updating the points value as expected.
What is the correct ActiveRecord callback to be using instead of before_save, or what else could I be doing to keep the column updated.
Try using the after_touch callback instead.
after_touch callback is triggered whenever an object is touched.
So, whenever point type changes, it should update the points.
First of all, counter_culture seems to be a way to enhance the counter_cache functionality of rails...
Used to cache the number of belonging objects on associations. For example, a comments_count column in a Post class that has many instances of Comment will cache the number of existent comments for each post.
It might not be exactly what you want, judging from your question.
Okay I get it. You're using points in your User model to create a "cached" column which can be used for wider application functionality. Okay that's cool...
--
Your setup, then, will look something like this (you were manually setting the counter_cache column, and now the gem handles it):
#app/models/user.rb
class User < ActiveRecord::Base
counter_cache :points
end
#app/models/point.rb
class Point < ActiveRecord::Base
belongs_to :user, counter_cache: true
end
The question is then that when you update the points model, you need to be able to update the "cached" column in the users model, now without any callbacks.
What is the correct ActiveRecord callback to be using instead of before_save
I'm presuming you're calling before_save on your User model (IE adding the associated data and putting the points column?
If so, you should try using a callback on the Point model, perhaps something like this:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :points
end
#app/models/point.rb
class Point < ActiveRecord::Base
belongs_to :user, inverse_of: :points
after_commit :update_user
private
def update_user
if user?
user.update(points: x + y + z)
end
end
end
--
Oberservers
If you have real problems, you could look at ActiveRecord observers.
Here's an answer I wrote about it: Ruby On Rails Updating Heroku Dynamic Routes
Whether this will trigger without any callbacks is another matter, but what I can say is that it will work to give you functionality you may not have had access to otherwise:
#config/application.rb (can be placed into dev or prod files if required)
config.active_record.observers = :point_observer
#app/models/point_observer.rb
class PointObserver < ActiveRecord::Observer
def before_save(point)
#logic here
end
end
A good way to test this would be to use it (you'll have to use the rails-observers gem) with different methods. IE:
#app/models/point_observer.rb
class PointObserver < ActiveRecord::Observer
def initialize(point)
#if this fires, happy days
end
end

Get particular object from association with conditions

I have next association for user:
has_many :sent_requests, foreign_key: :requester_id, class_name: 'FriendRequest'
Here is FriendRequest model:
class FriendRequest < ActiveRecord::Base
belongs_to :requester, class_name: 'User'
belongs_to :target, class_name: 'User'
end
I include this association when query requests.
I want to get request to particular user if it exists. Next approach initiates request to the DB:
def friend_request_sent_to(user)
self.sent_requests.where(target: user).first
end
I need to call this method for the list of user. How can I get request to user from self without going to database on every call?
ActiveRecord caching (please be aware of this)
You should be aware that ActiveRecord automatically caches associations. If you evaluate self.sent_requests twice, only one database query will be executed (note, there are some caveats).
That means you can use Enumerable methods multiple times and the association will only be loaded once. E.g.
self.sent_requests.find { |friend_request| friend_request.target == user }
Note that this will not work if you add additional calls to AR query interface methods, like sent_requests.where(...), or sent_requests.joins(...).
Also, ActiveRecord caches query results. So executing self.sent_requests.where(target: user).first multiple times may only result in a single database query.
Memoization
You can also use memoization as suggested by another user. The tricky part about memoizing results is handling nil or false cases. There is a nice gem which handles this for you.
An unsophisticated approach with memoization which avoids nil issues:
class User
has_many :sent_requests, foreign_key: :requester_id, class_name: 'FriendRequest'
def friend_request_sent_to(user)
#sent_requests ||= self.sent_requests.includes(:user).to_a # Will always result in an Array (no need to worry about nil)
#sent_requests.find {|sent_request| sent_request.user == user }
end
This method also has the advantage of allowing you to use more complex queries which would not be cached by ActiveRecord.
You can cache the result in instance variable:
def friend_request_sent?(user)
return #friend_request_sent if defined?(#friend_request_sent)
#friend_request_sent = self.sent_requests.where(target: user).first
end
Also note that it is a ruby convention for methods ending with ? to return true or false. Most likely you rather want to do:
def friend_request_sent?(user)
return #friend_request_sent if defined?(#friend_request_sent)
#friend_request_sent = self.sent_requests.exists?(target: user)
end

Counter Cache on a condition not on association

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.

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

Model association changes in production environment, specifically converting a model to polymorphic?

I was hoping I could get feedback on major changes to how a model works in an app that is in production already.
In my case I have a model Record, that has_many PhoneNumbers.
Currently it is a typical has_many belongs_to association with a record having many PhoneNumbers.
Of course, I now have a feature of adding temporary, user generated records and these records will have PhoneNumbers too.
I 'could' just add the user_record_id to the PhoneNumber model, but wouldn't it be better for this to be a polymorphic association?
And if so, if you change how a model associates, how in the heck would I update the production database without breaking everything? >.<
Anyway, just looking for best practices in a situation like this.
Thanks!
There's two approaches that might help you with this.
One is to introduce an intermediate model which handles collections of phone numbers. This way your Record and UserRecord can both belong_to this collection model and from there phone numbers and other contact information can be associated. You end up with a relationship that looks like this:
class Record < ActiveRecord::Base
belongs_to :address_book
delegate :phone_numbers, :to => :address_book
end
class UserRecord < ActiveRecord::Base
belongs_to :address_book
delegate :phone_numbers, :to => :address_book
end
class AddressBook < ActiveRecord::Base
has_many :phone_numbers
end
This kind of re-working can be done with a migration and a bit of SQL to populate the columns in the address_books table based on what is already present in records.
The alternative is to make UserRecord an STI derived type of Record so you don't need to deal with two different tables when defining the associations.
class Record < ActiveRecord::Base
has_many :phone_numbers
end
class UserRecord < Record
end
Normally all you need to do is introduce a 'type' string column into your schema and you can use STI. If UserRecord entries are supposed to expire after a certain time, it is easy to scope their removal using something like:
UserRecord.destroy_all([ 'created_at<=?', 7.days.ago ])
Using the STI approach you will have to be careful to scope your selects so that you are retrieving only permanent or temporary records depending on what you're intending to do. As UserRecord is derived from Record you will find they get loaded as well during default loads such as:
#records = Record.find(:all)
If this causes a problem, you can always use Record as an abstract base class and make a derived PermanentRecord class to fix this:
class PermanentRecord < Record
end
Update during your migration using something like:
add_column :records, :type, :string
execute "UPDATE records SET type='PermanentRecord'"
Then you can use PermanentRecord in place of Record for all your existing code and it should not retrieve UserRecord entries inadvertently.
Maintenance page is your answer.
Generate migration which updates table structure and updates existing data. If you're against data updates in migrations - use rake task.
Disable web access (create maintenance page)
Deploy new code
Run pending migrations
Update data
Enable web access (remove maintenance page).

Resources