Problem with counter_cache implementation
Asked Answered
S

2

44

I'm getting 'rake aborted! ... posts_count is marked readonly' errors.

I have two models: user and post.

users has_many posts.

posts belongs_to :user, :counter_cache => true

I have a migration which adds the posts_count column to the users table and then calculates and records the current number of posts per user.

self.up
  add_column :users, :posts_count, :integer, :default => 0

  User.reset_column_information
  User.all.each do |u|
    u.update_attribute( :posts_count, u.posts.count)
  end
end

when I run the migration I get the error. This is pretty clear-cut, of course and if I remove the :counter_cache declaration from the posts model, e.g.

belongs_to :user

the migration runs fine. This obviously, does not make sense because you couldn't really implement it this way. What am I missing?

Strawflower answered 28/11, 2010 at 4:38 Comment(0)
S
98

You should be using User.reset_counters to do this. Additionally, I would recommend using find_each instead of each because it will iterate the collection in batches instead of all at once.

self.up
  add_column :users, :posts_count, :integer, :default => 0

  User.reset_column_information
  User.find_each do |u|
    User.reset_counters u.id, :posts
  end
end
Smedley answered 28/11, 2010 at 5:14 Comment(6)
It appears that the preferred approach is: "User.reset_counters u.id, :posts_count". See api.rubyonrails.org/classes/ActiveRecord/CounterCache.htmlWolfgang
Slight correction to above (not able to edit that comment any more): "User.reset_counters u.id, :posts"Wolfgang
@Mike Fischer good catch, looks like reset_counters uses fewer SQL calls.Smedley
@gwho I don't see any deprecation notice on either method in either 4-1-stable or master. What makes you think that? github.com/rails/rails/blob/master/activerecord/lib/…Smedley
apidock said that. And it wasn't working for me. i'm mistakenHomesick
@gwho apidock calls anything "deprecated" if the code moves locations. I generally avoid that site, it is often confusing like that.Smedley
W
3

OK, the documentation states:

Counter cache columns are added to the containing model’s list of read-only attributes through attr_readonly.

I think this is what happens: you declare the counter in the model's definition, thus rendering the "posts_count" attribute read-only. Then, in the migration, you attempt to update it directly, resulting in the error you mention.

The quick-and-dirty solution is to remove the counter_cache declaration from the model, run the migration (in order to add the required column to the database AND populate it with the current post counts), and then re-add the counter_cache declaration to the model. Should work but is nasty and requires manual intervention during the migration - not a good idea.

I found this blog post which suggests altering the model's list of read-only attributes during the migration, it's a bit oudated but you might want to give it a try.

Westerfield answered 28/11, 2010 at 4:46 Comment(4)
I appreciate your help. I am using the workaround you suggested, but as you agree, it is not a good way to get things into production. Thanks also for the blog article, but that hack doesn't seem to work anymore (I am using rails 3.0.1 currently.) I may just have to roll my own cache counters and maintain them in the app.Strawflower
That blog post is misleading or outdated. See this one.Smedley
This was the problem for me. counter_cache: true was setting doing some behind the scene attr_readonly magic behind the scenes and blocking my migrationNephron
This way poses a problem when trying to do it on a remote repository like heroku. Upload a version with countercache off, then migrate, then upload a verson with countercache implemented? ew.Homesick

© 2022 - 2024 — McMap. All rights reserved.