Sometimes you cannot use included in Rails Associations #counter_cache method. E.g. we have a polymorphic model Comment that belongs to its #target , some targets have #comments_count columns, others - not.

# app/models/comment.rb class Comment < ActiveRecord::Base belongs_to :target, polymorphic: true, inverse_of: :comments, counter_cache: true # … end

In this case if #target does not have #comments_count column, ActiveRecord::StatementInvalid exception will be raised. This can be solved with association callbacks or better to move Comment create logic to specific service object where you also will increment counters only in appropriate targets. In both cases you can use #increment_counter method which accepts column name, that should be incremented and id of a record in a database.

# your comment create logic here document = comment.target Document.increment_counter(:comments_count, document.id)

It works pretty well, increments counter in SQL using COALESCE function and + 1 operator. The only problem for me in this case is that I cannot get actual number of comments for selected document, because by default PostgreSQL (I believe that other RDBMS too return number of modified rows). To do this I had either to reload document or find it in database again.

document.reload # or… document = Document.find(document.id)

Both seems weird for me… Fortunately PostgreSQL can return data from modified rows. Bingo! Lets create our custom method that will update #comments_count on Document.all

# app/models/document.rb # … def increment_comments_count! result = ActiveRecord::Base.connection.execute <<~SQL UPDATE "#{self.class.table_name}" SET "comments_count" = COALESCE("comments_count", 0) + 1 WHERE "#{self.class.table_name}"."id" = #{id} RETURNING comments_count; SQL write_attribute(:comments_count, result[0]['comments_count']) clear_attribute_changes(:comments_count) self end # …

As you can see we execute SQL Update query and ask DB to return modified comments_count column. Then we get modified value from result object (it is and instance of PGResult) and assign it to our model. Rails magic, which I personally hate (hello to ROM and all dry-rb stack), begins in clear_attribute_changes , which removes changed comments_count from Dirty attributes, because in this case model is outdated compared to actual value in DB.

# app/services/comment/create_comment.rb # … if form.validate(params) comment = form.sync ActiveRecord::Base.transaction(requires_new: true) do comment.save! document.increment_comments_count! end [:success, [document, comment]] else [:failure, :validation, form] end # …

With this code we can guarantee, that document will have correct comments_count value. Finally, lets move code to separate module, that will extend our models.

# app/models/concerns/increment_counter_with_returning_value.rb module IncrementCounterWithReturningValue def increment_counter_returning_value(counter, id) query = " UPDATE \"#{table_name}\" SET \"#{counter}\" = COALESCE(\"#{counter}\", 0) + 1 WHERE \"#{table_name}\".\"#{primary_key}\" = #{connection.quote(id)} RETURNING \"#{counter}\"; " result = connection.execute(query, "#{class_name.to_s} increment #{counter}") result[0][counter.to_s] end end # app/models/document.rb class Document < ActiveRecord::Base extend IncrementCounterWithReturningValue # … def increment_comments_count! write_attribute(:comments_count, self.class.increment_counter_returning_value(:comments_count, id)) clear_attribute_changes(:comments_count) self end end

Where this can be used? Of course in situations like above, when you have to in/decrement cache column. Also in SaaS platforms and e-commerce projects you need to update user internal balance or update product availability.

Don't hesitate to hack (safely) ActiveRecord!