I'm adding a counter_cache, and my migrate is getting an error.
def up
add_column :benefits, :benefit_rows_count, :integer, :default => 0
Benefit.reset_column_information
Benefit.find(:all).each do |b|
Benefit.update_counters b.id, :benefit_rows_count => b.benefit_rows.length
end
end
SQL:
UPDATE "benefits" SET "benefit_rows_count" = COALESCE("benefit_rows_count", 0) + 0
WHERE "benefits"."id" IN (SELECT "benefits"."id"
FROM "benefits"
WHERE "benefits"."id" = 1
ORDER BY benefit_types.position, benefit_types.name, id)
This ORDER BY, inside the update is because of the default_scope, and it fails the migration.
Unfortunately, when i update the record, i get the same error when the callback update_counters is executed. I've read some posts that said default_scope should be avoided. I checked Rails 4 source code (i'm using Rails 3) and update_counters has not been fixed. I'm going to reopen ActiveRecord::CounterCache.update_counters and try to unscope it.
As already noted, your default scopes are tripping you up. There's a better way to avoid this sort of issue in your migrations: redefine your model in the scope of the migration:
class MigrateFooToBar < ActiveRecord::Migration
class Foo < ActiveRecord::Base; end
def up
# ...
end
end
When you then reference Foo from within up you reference the void-of-restrictions-and-default-scopes MigrateFooToBar::Foo which keeps you migrations from A) having to know too much about your models and B) confusing everybody else on your team when they run your migrations.
Thx Baldrick, i'm new to rails. And unscoped worked:
Benefit.unscoped.update_counters b.id, :benefit_rows_count => b.benefit_rows.length
Related
I have a migration which adds a boolean column and sets value for some of the rows. The new value doesn't get saved to the database when the model is saved. Here is a simplified version of the code:
class AddSmartToStudent
def change
add_column :students, :smart, :boolean
Student.where(grade: 'A').each do |student|
student.smart = true
student.save!
end
end
end
Thanks in advance!
In your migration, you add a boolean column, and right after that use it in the model. Not sure it is possible - when migration is not ended yet the transaction is not committed. The Student model might not have smart field yet.
As Luis Silva suggests you could use reset_column_information method to refresh info about columns for Student. But the problem is migrations are not for manipulating with data. If you want to change some data it's better to do in a rake task.
If for some reason you HAVE TO do it in migration, you can do it in plain SQL query. For PostgreSQL it will be:
execute "UPDATE students SET smart='t' WHERE grade='A'"
Try to reset the cached information about columns, which will cause them to be reloaded on the next request.
Execute this line before your clause where
Student.reset_column_information
reset_column_information
There are two issues that are clear to me right of the bet.
As stated by others you're trying to use an attribute that you add in that same migration. The safe thing to do is to reset the column information like explained in the answer of Luis Silva.
The second issue has to do with the fact that you use def change where some of the content isn't reversible. Everything in the change method should be reversible. Otherwise def up and def down should be used.
Here are two options that might solve your issue:
Using def up and def down.
class AddSmartToStudent
def up
add_column :students, :smart, :boolean
Student.reset_column_information
Student
.where(grade: 'A')
.find_each { |student| student.update!(smart: true) }
end
def down
remove_column :students, :smart
end
end
Using reversible.
class AddSmartToStudent
def change
add_column :students, :smart, :boolean
reversible do |change|
change.up do
Student.reset_column_information
Student
.where(grade: 'A')
.find_each { |student| student.update!(smart: true) }
end
end
end
end
If you don't care about Rails callbacks, validations, etc. you could also use
Student.where(grade: 'A').update_all(smart: true)
as replacement for
Student.where(grade: 'A').find_each { |student| student.update!(smart: true) }
This updates all the records with a single query but doesn't instantiate the records, meaning Rails callbacks, validations, etc. won't run. For more info see update_all.
I have strange problem. This is my migration code:
class SetDefaultToFalse < ActiveRecord::Migration
def change
MyObject.where('done is ?', nil).each do |a|
a.done = false
a.save
end
end
end
If I run this on database dump from while ago, with all the other, older migrations, after it's done, any of the my_objects don't have done field marked as false. If I do rake db:rollback and db:migrate again, suddenly, it works. Why? Column done is added more then 5 migrations before this one. Changing previous migrations is not the solution i'm looking for.
You should have a look at this:
Setting different default values in rails migrations?
I am not sure why would you do that in a migration, but this might help:
MyObject.where('done is ?', nil).update_all(done: false)
done field added after create. In db schema 'done' field not exist.
Run
rake db:migrate
I suggest you try this code:
class SetDefaultToFalse < ActiveRecord::Migration
def self.up
MyObject.where('done is ?', nil).each do |a|
a.update_attribute(done: false)
end
end
end
I have added monetize and demonetize helpers inside ActiveRecord::Migration, ActiveRecord::ConnectionAdapters::TableDefinition and ActiveRecord::ConnectionAdapters::Table by that pull request.
That file shows usage examples. So you will understand invented changes at glance. (It works)
But I have no idea how to test my helpers. What way can I write specs for them? All my attempts of writing migrations in spec files and running them manually failed. Migration manual run did not change table (or I was unable to detect changes) and did not throw any exception.
Example of my attempt:
describe 'monetize' do
class MonetizeMigration < ActiveRecord::Migration
def change
create_table :items
monetize :items, :price
end
end
class Item < ActiveRecord::Base; end
it 'should monetize items' do
MonetizeMigration.up #=> nil
Item #=> Item(has no table)
end
end
This worked for me in the console:
[4667]foo#bar:~/dev/ror/foo$ rails c
Loading development environment (Rails 3.2.9)
irb(main):001:0> class MyMigration def change
irb(main):003:2> create_table :foo
irb(main):004:2> end
irb(main):005:1> end
=> nil
irb(main):006:0> MyMigration.new.change
-- create_table(:foo)
(4.5ms) select sqlite_version(*)
(133.2ms) CREATE TABLE "foo" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)
-> 0.2362s
=> []
You can execute any migration helper methods right on your database connection:
ActiveRecord::Base.connection.create_table :items
Thanks to #happy_user for showing my mistake in first attempt. I think someone may use my latest solution in the future, so I'll leave it here.
I have model with tag context:
class Product < ActiveRecord::Base
acts_as_taggable_on :categories
end
I'm trying to initialize tags caching:
class AddCachedCategoryListToProducts < ActiveRecord::Migration
def self.up
add_column :products, :cached_category_list, :string
Product.reset_column_information
products = Product.all
products.each { |p| p.save_cached_tag_list }
end
end
But cached_category_list does not initializing. What I'm doing wrong? Does anybody can use caching with this gem (my version is 2.0.6)?
Well, today I had the same problem.
I finally solved it, and my migration cached the desired tags.
The problem with your migration was two-fold:
The ActsAsTaggable code which sets up caching needs to run again after the column information is reset. Otherwise, the caching methods are not created (see https://github.com/mbleigh/acts-as-taggable-on/blob/v2.0.6/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb)
The method you are calling, save_cached_tag_list, does NOT automatically save the record, as it is installed as a before_save hook, and it doesn't want to create an infinite loop. So you must call save.
So, try replacing your migration with the following, and it should work:
class AddCachedCategoryListToProducts < ActiveRecord::Migration
def self.up
add_column :products, :cached_category_list, :string
Product.reset_column_information
# next line makes ActsAsTaggableOn see the new column and create cache methods
ActsAsTaggableOn::Taggable::Cache.included(Product)
Product.find_each(:batch_size => 1000) do |p|
p.category_list # it seems you need to do this first to generate the list
p.save! # you were missing the save line!
end
end
end
That should do it.
If you are using this in combination with owned tags, that might be the problem.
Looking at the code of the gem, it seems that the caching of owned tags isn't support
Hope this helps,
Best,
J
I'd like to know which is the preferred way to add records to a database table in a Rails Migration. I've read on Ola Bini's book (Jruby on Rails) that he does something like this:
class CreateProductCategories < ActiveRecord::Migration
#defines the AR class
class ProductType < ActiveRecord::Base; end
def self.up
#CREATE THE TABLES...
load_data
end
def self.load_data
#Use AR object to create default data
ProductType.create(:name => "type")
end
end
This is nice and clean but for some reason, doesn't work on the lasts versions of rails...
The question is, how do you populate the database with default data (like users or something)?
Thanks!
The Rails API documentation for migrations shows a simpler way to achieve this.
http://api.rubyonrails.org/classes/ActiveRecord/Migration.html
class CreateProductCategories < ActiveRecord::Migration
def self.up
create_table "product_categories" do |t|
t.string name
# etc.
end
# Now populate the category list with default data
ProductCategory.create :name => 'Books', ...
ProductCategory.create :name => 'Games', ... # Etc.
# The "down" method takes care of the data because it
# drops the whole table.
end
def self.down
drop_table "product_categories"
end
end
Tested on Rails 2.3.0, but this should work for many earlier versions too.
You could use fixtures for that. It means having a yaml file somewhere with the data you want to insert.
Here is a changeset I committed for this in one of my app:
db/migrate/004_load_profiles.rb
require 'active_record/fixtures'
class LoadProfiles < ActiveRecord::Migration
def self.up
down()
directory = File.join(File.dirname(__FILE__), "init_data")
Fixtures.create_fixtures(directory, "profiles")
end
def self.down
Profile.delete_all
end
end
db/migrate/init_data/profiles.yaml
admin:
name: Admin
value: 1
normal:
name: Normal user
value: 2
You could also define in your seeds.rb file, for instance:
Grid.create :ref_code => 'one' , :name => 'Grade Única'
and after run:
rake db:seed
your migrations have access to all your models, so you shouldn't be creating a class inside the migration.
I am using the latest rails, and I can confirm that the example you posted definitely OUGHT to work.
However, migrations are a special beast. As long as you are clear, I don't see anything wrong with an ActiveRecord::Base.connection.execute("INSERT INTO product_types (name) VALUES ('type1'), ('type2')").
The advantage to this is, you can easily generate it by using some kind of GUI or web front-end to populate your starting data, and then doing a mysqldump -uroot database_name.product_types.
Whatever makes things easiest for the kind of person who's going to be executing your migrations and maintaining the product.
You should really not use
ProductType.create
in your migrations.
I have done similar but in the long run they are not guaranteed to work.
When you run the migration the model class you are using is the one at the time you run the migration, not the one at the time you created the migration. You will have to be sure you never change your model in such a way to stop you migration from running.
You are much better off running SQL for example:
[{name: 'Type', ..}, .. ].each do |type|
execute("INSERT INTO product_types (name) VALUES ('#{type[:name]} .. )
end