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?
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
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.
Related
I have a question about rails 6 and model association with or without migrations.
Let me be more specific.
Let's say we want to place a foreign key inside users table which links to the id column of the courses table.
Most of the tutorials and courses i run into follow this route:
create a migration with add_column :users, :course_id, :integer
place has_one or has_many and belongs_to to the appropriate models.
And that's it.
My thought is, how about this:
create a migration with add_reference :users, :course, foreign_key: true
place the appropriate has_one/has_many and belongs_to to the apropriate models.
or is it the same?
Doesn't the second option create indexes as well whereas the first does not?
Which is a best practice?
add_reference is meant to be a shortcut as part of the rails convention, under the hood, it will call add_column and other options that you specify [you can check the code for rails-6 here]
while foreign keys are not required, they are considered a best practice because they guarantee referential integrity. you can read more about it here https://edgeguides.rubyonrails.org/active_record_migrations.html#foreign-keys
or is it the same? Doesn't the second option create indexes as well whereas the first does not?
having said that, your 2 migrations are not entirely the same even though they work the same (they achieve the same goal) - the second option will add an index by default unless you specify the option not to - I definitely agree with you about using add_reference as this is an easier and more foolproof shortcut
of course, you can also achieve that manually by using your first migration by adding an index and a foreign key as well
add_column :users, :course_id, :integer
add_index :users, :course_id # uniq or not
add_foreign_key :courses, :users
I have a has_many relation in my app. e.g. department has many users.
I want to covert it to a has_and_belongs_to_many relation.
As part of the migration I need to preserve the current relation between users and departments, meaning I have to move all the data to the new connecting table.
This is the migration I created:
class CreateUserDepartment < ActiveRecord::Migration
def change
create_table :users_departments do |t|
t.belongs_to :user
t.belongs_to :department
end
###############################################
# need to move the data to the new table here #
###############################################
remove_column :users, :sub_department_id
end
end
what is the best way to write the missing line?
If you must, you can use execute "your SQL". See this question:
How do I add some inserts in rails migration?
The main value of the "Don't" answer to that question is in explaining why you would not want to use your models to do this. Also, I'd be surprised if you can or would want to do this using change, you would probably need to use self.up.
I am new to the concept of counter caching and with some astronomical load times on one of my app's main pages, I believe I need to get going on it.
Most of the counter caches I need to implement have certain (simple) conditions attached. For example, here is a common query:
#projects = employee.projects.where("complete = ?", true).count
I am stumbling into the N+1 query problem with the above when I display a form that lists the project counts for every employee the company has.
Approach
I don't really know what I'm doing so please correct me!
# new migration
add_column :employees, :projects_count, :integer, :default => 0, :null => false
# employee.rb
has_many :projects
# project.rb
belongs_to :employee, :counter_cache => true
After migrating... is that all I need to do?
How can I work in the conditions I mentioned so as to minimize load times?
With regards to the conditions with counter_cache, I would read this blog post.
The one thing you should do is add the following to the migration file:
add_column :employees, :projects_count, :integer, :default => 0, :null => false
Employee.reset_column_information
Employee.all.each do |e|
Employee.update_counters e.id, :projects_count => e.projects.length
end
So you current projects count can get migrated to the new projects_count that are associated with each Employee object. After that, you should be good to go.
Check counter_culture gem:
counter_culture :category, column_name: Proc.new {|project| project.complete? ? 'complete_count' : nil }
You should not use "counter_cache" but rather a custom column :
rails g migration AddCompletedProjectsCountToEmployees completed_projects_count:integer
(add , :default => 0 to the add_column line if you want)
rake db:migrate
then use callbacks
class Project < ActiveRecord::Base
belongs_to :employee
after_save :refresh_employee_completed_projects_count
after_destroy :refresh_employee_completed_projects_count
def refresh_employee_completed_projects_count
employee.refresh_completed_projects_count
end
end
class Employee
has_many :projects
def refresh_completed_projects_count
update(completed_projects_count:projects.where(completed:true).size)
end
end
After adding the column, you should initialize in the console or in the migration file (in def up) :
Employee.all.each &:refresh_completed_projects_count
Then in your code, you should call employee.completed_projects_count in order to access it
Instead of update_counters i use update_all
You don't need the Employee.reset_column_information line AND it's faster because you are doing a single database call
Employee.update_all("projects_count = (
SELECT COUNT(projects.id) FROM projects
WHERE projects.employee_id = employees.id AND projects.complete = 't')")
I am new to the concept of counter caching and with some astronomical load times on one of my app's main pages, I believe I need to get going on it.
Most of the counter caches I need to implement have certain (simple) conditions attached. For example, here is a common query:
#projects = employee.projects.where("complete = ?", true).count
I am stumbling into the N+1 query problem with the above when I display a form that lists the project counts for every employee the company has.
Approach
I don't really know what I'm doing so please correct me!
# new migration
add_column :employees, :projects_count, :integer, :default => 0, :null => false
# employee.rb
has_many :projects
# project.rb
belongs_to :employee, :counter_cache => true
After migrating... is that all I need to do?
How can I work in the conditions I mentioned so as to minimize load times?
With regards to the conditions with counter_cache, I would read this blog post.
The one thing you should do is add the following to the migration file:
add_column :employees, :projects_count, :integer, :default => 0, :null => false
Employee.reset_column_information
Employee.all.each do |e|
Employee.update_counters e.id, :projects_count => e.projects.length
end
So you current projects count can get migrated to the new projects_count that are associated with each Employee object. After that, you should be good to go.
Check counter_culture gem:
counter_culture :category, column_name: Proc.new {|project| project.complete? ? 'complete_count' : nil }
You should not use "counter_cache" but rather a custom column :
rails g migration AddCompletedProjectsCountToEmployees completed_projects_count:integer
(add , :default => 0 to the add_column line if you want)
rake db:migrate
then use callbacks
class Project < ActiveRecord::Base
belongs_to :employee
after_save :refresh_employee_completed_projects_count
after_destroy :refresh_employee_completed_projects_count
def refresh_employee_completed_projects_count
employee.refresh_completed_projects_count
end
end
class Employee
has_many :projects
def refresh_completed_projects_count
update(completed_projects_count:projects.where(completed:true).size)
end
end
After adding the column, you should initialize in the console or in the migration file (in def up) :
Employee.all.each &:refresh_completed_projects_count
Then in your code, you should call employee.completed_projects_count in order to access it
Instead of update_counters i use update_all
You don't need the Employee.reset_column_information line AND it's faster because you are doing a single database call
Employee.update_all("projects_count = (
SELECT COUNT(projects.id) FROM projects
WHERE projects.employee_id = employees.id AND projects.complete = 't')")
Do I need to re-migrate whenever I change the models in Rails? Can someone tell me what migration really does? I am reading it and I am somewhat confused with the difference between the stuff inside db/migrate and the stuff inside app/models.
For example, if I add a has_one realtionship inside my model, do I need to re-migrate this? Why?
If your database changes, use a migration. If you're just adding methods to your model, no need to have a migration.
Example:
We start out and we just have first_name, last_name. We want to store those in the database, so we have a migration that does:
/app/models/human.rb
# empty
/db/migrate/xxxxx.rb
add_column :humans, :first_name, :string
add_column :humans, :last_name, :string
Then we get married, so we want to track that
/app/models/human.rb
belongs_to :spouse
We need to have a spouse_id field in the database, so we need a migration
/db/migrate/xxxxx.rb
add_column :humans, :spouse_id, :integer
We then have a kid.... In fact, we were all kids at one point, but to keep it simple, we'll have Humans and Offspring
/app/models/offspring.rb
belongs_to :human
/db/migrate/xxxxx.rb
create_table ...
However, no need to add anything to the Human migration, since no tables change here. We do need to add:
/app/models/human.rb
has_many :offspring
If you want to be able to get at, easily, your first born, you'd just add a method to your model. No need for a migration here:
/app/models/human.rb
def first_born
offspring.first
end