之前发过一个帖子「Rails should have data migration」,当时也没太弄清楚App和Schema Migration之间,以及Schema Migration和 Data Migration之间的关系。直到读完这篇「Not All Migrations are Equal: Schema vs. Data」才茅塞顿开。
本质上App依赖Schema Migration,但Schema Migration不应该去依赖App,Schema Migration是可以脱离App独立存在的。
而Data Migration呢,是需要依赖App的业务逻辑的。所以呢,Schema和Data Migration是不能放在一起的。
理解了这两个前提,看一下作者举的例子:
Bad Schema Migration
class ChangeAdminDefaultToFalseOnUsers < ActiveRecord::Migration
def up
change_column_default(:users, :admin, false)
User.reset_column_information # 注:这里Migration依赖App里的User model和reset_column_information方法了。
# Bad: Use of application code that changes over time.
User.update_null_to_false!
end
end
Good Schema Migration
class ChangeAdminDefaultToFalseOnUsers < ActiveRecord::Migration
# Create empty AR model that will attach to the users table,
# and isolate migration from application code.
class User < ActiveRecord::Base; end
def up
change_column_default(:users, :admin, false)
User.where(admin:nil).update_all(admin: false)
end
end
Better Schema Migration
class ChangeAdminDefaultToFalseOnUsers < ActiveRecord::Migration
def up
change_column_default(:users, :admin, false)
execute "UPDATE users SET admin = false WHERE admin IS NULL"
end
end
当然SQL也不是那么好写,作者又讨(批)论(评)了一下流行的one off rake task
方案:
Create a one off rake task? No.
Perhaps, but the code will be difficult to test and won’t have mechanisms in place to roll back to changes. Even if you refactor the logic out of the rake task into a separate ruby >class, you will now have to maintain code that is ephemeral in nature. It merely exists for this one off data migration.
最后作者提出Datafixes的架构,其实本质上是基于one off rake task
的扩充,包括:
Generator:
> rails g datafix AddValidWholesalePriceToProducts
create db/datafixes/20141211143848_add_valid_wholesale_price_to_products.rb
create spec/db/datafixes/20141211143848_add_valid_wholesale_price_to_products_spec.rb
像schema_migration表一样记录data migration是否执行过的机制:
> rake db:datafix
migrating AddValidWholesalePriceToProducts up
> rake db:datafix:status
database: somedatabase_development
Status Datafix ID Datafix Name
--------------------------------------------------
up 20141211143848 AddValidWholesalePriceToProducts
可以为Data Migration写测试:
require "rails_helper"
require Rails.root.join("db", "datafixes", "20141211143848_add_valid_wholesale_price_to_products")
describe Datafixes::AddValidWholesalePriceToProducts do
describe ".up" do
# Fill out the describe block
let!(:product) do
product = FactoryGirl.build(:product, wholesale_price_cents: nil)
product.save(validate: false)
product
end
it "should fix the price and be valid" do
expect(product).to_not be_valid
subject.migrate('up')
expect(product.reload).to be_valid
end
end
end
Rollback机制:
rake db:datafix:rollback
另外又提到了和Datafix类似的nondestructive_migrations gem:
Update 01/26/2015: Check out the nondestructive_migrations gem. It’s similar to dimroc/datafix but simpler because it leverages existing AR code. It does not however generate specs… yet.