Non-tech founder’s guide to choosing the right software development partner Download Ebook
Home>Blog>Rails: increment counter cache and return value

Rails: increment counter cache and return value

Small ActiveRecord hack that you can use everyday.

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!

Discover More Reads

Real Stories & Real Success

Do you have a tech idea?

Let’s talk!

By submitting this form, you agree with JetRockets’ Privacy Policy

If you prefer email, write to us at hello@jetrockets.com