之前发过一个帖子「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.