想了解下migration中change是如何判断和执行正向与反向的操作的,于是trace一下

class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    binding.trace_tree(html: true, tmp: ['rails', 'migrate.html']) do
      create_table :products do |t|
        t.string :title
        t.text :description
        t.string :image_url
        t.decimal :price

        t.timestamps
      end
    end
  end
end


因之前已跑过一次migrate,所以现在rollback

bin/rails db:rollback


完整调用栈如下

rollback.html

查找create_table语句,发现它被传到say_with_time和benchmark之中


这两个东西其实就是这么一回事,本来还以为是重定义了方法来拦截的

def say_with_time(message)
  say(message)
  result = nil
  time = Benchmark.measure { result = yield }
  say "%.4fs" % time.real, :subitem
  say("#{result} rows", :subitem) if result.is_a?(Integer)
  result
end


再回头看看rollback时create_table的走向,可见这时调的create_table来自CommandRecorder。byebug一下,发现它是一个connection的wrapper

[840, 849] in /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.0.2/lib/active_record/migration.rb
   840:               (method == :remove_foreign_key && !arguments.second.is_a?(Hash))
   841:               arguments[1] = proper_table_name(arguments.second, table_name_options)
   842:             end
   843:           end
   844:         end
=> 845:         return super unless connection.respond_to?(method)
   846:         connection.send(method, *arguments, &block)
   847:       end
   848:     end
   849:
(byebug) connection
#<activerecord::migration::commandrecorder:0x007f5743342230 @commands="[]," @delegate="#<ActiveRecord::ConnectionAdapters::SQLite3Adapter:0x007f5743281558......</code"></activerecord::migration::commandrecorder:0x007f5743342230>


根据其注释所说就是用于track执行过的migration(以便能在中途失败时(正向/反向)回滚,因为一般数据库是不支持多个schema的rollback的?)

# ActiveRecord::Migration::CommandRecorder records commands done during
# a migration and knows how to reverse those commands. The CommandRecorder
# knows how to invert the following commands:


这样的话,把inverse_of、invert_create_table之类的东西也定义在这里也算很合理

module ActiveRecord
  class Migration
    class CommandRecorder

      def inverse_of(command, args, &block)
        method = :"invert_#{command}"
        raise IrreversibleMigration, <<-MSG.strip_heredoc unless respond_to?(method, true)
          This migration uses #{command}, which is not automatically reversible.
          To make the migration reversible you can either:
          1. Define #up and #down methods in place of the #change method.
          2. Use the #reversible method to define reversible behavior.
        MSG
        send(method, args, &block)
      end

      module StraightReversions
        private
        { transaction:       :transaction,
          execute_block:     :execute_block,
          create_table:      :drop_table,
          create_join_table: :drop_join_table,
          add_column:        :remove_column,
          add_timestamps:    :remove_timestamps,
          add_reference:     :remove_reference,
          enable_extension:  :disable_extension
        }.each do |cmd, inv|
          [[inv, cmd], [cmd, inv]].uniq.each do |method, inverse|
            class_eval <<-EOV, __FILE__, __LINE__ + 1
              def invert_#{method}(args, &block)    # def invert_create_table(args, &block)
                [:#{inverse}, args, block]          #   [:drop_table, args, block]
              end                                   # end
            EOV
          end
        end
      end

      include StraightReversions

    end
  end
end


是否调用inverse_of,由@reverting来控制

class CommandRecorder

  def initialize(delegate = nil)
    @commands = []
    @delegate = delegate
    @reverting = false
  end

  def revert
    @reverting = !@reverting
    previous = @commands
    @commands = []
    yield
  ensure
    @commands = previous.concat(@commands.reverse)
    @reverting = !@reverting
  end

  def record(*command, &block)
    if @reverting
      @commands << inverse_of(*command, &block)
    else
      @commands << (command << block)
    end
  end
end


控制@reverting似乎只有revert方法(其实还有attr_accessor :reverting暴露了),但往后的调用栈中并没看到,于是查查往前的

From: /home/z/test_rails/depot/db/migrate/20170405081554_create_products.rb @ line 5 CreateProducts#change:

     2: def change
     3:   binding.pry
     4:   #binding.trace_tree(html: true, tmp: ['rails', 'migrate.html']) do
 =>  5:     create_table :products do |t|
     6:       t.string :title
     7:       t.text :description
     8:       t.string :image_url
     9:       t.decimal :price
    10:
    11:       t.timestamps
    12:     end
    13:   #end
    14: end

[1] pry(#)> _bs_
=> [#<binding:70030881821840 createproducts#change="" home="" z="" test_rails="" depot="" db="" migrate="" 20170405081554_create_products.rb:3="">,
 #<binding:70030887961480 createproducts#block="" in="" exec_migration="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:787="">,
 #<binding:70030888016200 createproducts#block="" (2="" levels)="" in="" revert="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:676="">,
 #<binding:70030888073540 activerecord::migration::commandrecorder#revert="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration="" command_recorder.rb:59="">,
 #<binding:70030881714980 createproducts#block="" in="" revert="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:676="">,
 #<binding:70030888133260 createproducts#suppress_messages="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:823="">,
 #<binding:70030888187940 createproducts#revert="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:675="">,
 #<binding:70030888254640 createproducts#exec_migration="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:787="">,
 #<binding:70030888295740 createproducts#block="" (2="" levels)="" in="" migrate="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:773="">,
 #<binding:70030888313360 benchmark.measure="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" 2.4.0="" benchmark.rb:293="">,
 #<binding:70030888377280 createproducts#block="" in="" migrate="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:772="">,
 #<binding:70030881460780 activerecord::connectionadapters::connectionpool#with_connection="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" connection_adapters="" abstract="" connection_pool.rb:398="">,
 #<binding:70030888445260 createproducts#migrate="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:771="">,
 #<binding:70030888487660 activerecord::migrationproxy#migrate="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:951="">,
 #<binding:70030888539340 activerecord::migrator#block="" in="" execute_migration_in_transaction="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:1214="">,
 #<binding:70030881312180 activerecord::migrator#block="" in="" ddl_transaction="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:1282="">,
 #<binding:70030881291440 activerecord::connectionadapters::sqlite3adapter#block="" in="" transaction="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" connection_adapters="" abstract="" database_statements.rb:232="">,
 #<binding:70030888687820 activerecord::connectionadapters::transactionmanager#within_new_transaction="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" connection_adapters="" abstract="" transaction.rb:189="">,
 #<binding:70030881241360 activerecord::connectionadapters::sqlite3adapter#transaction="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" connection_adapters="" abstract="" database_statements.rb:232="">,
 #<binding:70030888759820 activerecord::base.transaction="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" transactions.rb:211="">,
 #<binding:70030888811440 activerecord::migrator#ddl_transaction="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:1282="">,
 #<binding:70030881133600 activerecord::migrator#execute_migration_in_transaction="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:1213="">,
 #<binding:70030888864360 activerecord::migrator#block="" in="" migrate_without_lock="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:1185="">,
 #<binding:70030888924420 activerecord::migrator#migrate_without_lock="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:1184="">,
 #<binding:70030881025020 activerecord::migrator#migrate="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:1134="">,
 #<binding:70030888986260 activerecord::migrator.down="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:1013="">,
 #<binding:70030889040040 activerecord::migrator.move="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:1094="">,
 #<binding:70030880920880 activerecord::migrator.rollback="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:995="">,
 #<binding:70030889111880 object#block="" (2="" levels)="" in="" <top="" (required)=""> /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.0.2/lib/active_record/railties/databases.rake:144>,
 #<binding:70030880846180 rake::task#block="" in="" execute="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" rake-12.0.0="" lib="" rake="" task.rb:250="">,
......</binding:70030880846180></binding:70030889111880></binding:70030880920880></binding:70030889040040></binding:70030888986260></binding:70030881025020></binding:70030888924420></binding:70030888864360></binding:70030881133600></binding:70030888811440></binding:70030888759820></binding:70030881241360></binding:70030888687820></binding:70030881291440></binding:70030881312180></binding:70030888539340></binding:70030888487660></binding:70030888445260></binding:70030881460780></binding:70030888377280></binding:70030888313360></binding:70030888295740></binding:70030888254640></binding:70030888187940></binding:70030888133260></binding:70030881714980></binding:70030888073540></binding:70030888016200></binding:70030887961480></binding:70030881821840>


从rake到change的调用栈如上,不想细看的话,大致可总结为Rake::Task#block in execute,然后ActiveRecord::Migrator.rollback,直到CreateProducts#exec_migration,然后CreateProducts#revert

exec_migration如下,根据CLI的rake参数,direction就可确定了,于是,是revert还是不revert,也可确定了。

(根据源码来看,change优先于up和down)

def exec_migration(conn, direction)
  @connection = conn
  if respond_to?(:change)
    if direction == :down
      revert { change }
    else
      change
    end
  else
    send(direction)
  end
ensure
  @connection = nil
end


(注意这个revert是Migration的revert,不是CommandRecorder的revert,不过这是在下面trace正向的时候才发现的)

rollback分析暂告一段落。到这里,其实正向的migrate是没什么好看的,不过也顺便trace一下,调用栈如下

migrate.html


与rollback不同的是,这次的connection并没有包裹在CommandRecorder中,为什么呢?



因为@connection是在exec_migration传入的,所以检查下exec_migration之上有些什么操作

From: /home/z/test_rails/depot/db/migrate/20170405081554_create_products.rb @ line 5 CreateProducts#change:

     2: def change
     3:   binding.pry
     4:   #binding.trace_tree(html: true, tmp: ['rails', 'migrate.html']) do
 =>  5:     create_table :products do |t|
     6:       t.string :title
     7:       t.text :description
     8:       t.string :image_url
     9:       t.decimal :price
    10:
    11:       t.timestamps
    12:     end
    13:   #end
    14: end

[1] pry(#)> _bs_
=> [#<binding:70054489213400 createproducts#change="" home="" z="" test_rails="" depot="" db="" migrate="" 20170405081554_create_products.rb:3="">,
 #<binding:70054495916120 createproducts#exec_migration="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activerecord-5.0.2="" lib="" active_record="" migration.rb:789="">,
......</binding:70054495916120></binding:70054489213400>


不过,这下就发现了正向比反向短很多,exec_migration之后,马上就到了change,这才发现反向时exec_migration所调的revert是定义在Migration中的,而且也只有在这里,才会生成CommandRecorder

def revert(*migration_classes)
  run(*migration_classes.reverse, revert: true) unless migration_classes.empty?
  if block_given?
    if connection.respond_to? :revert
      connection.revert { yield }
    else
      recorder = CommandRecorder.new(connection)
      @connection = recorder
      suppress_messages do
        connection.revert { yield }
      end
      @connection = recorder.delegate
      recorder.commands.each do |cmd, args, block|
        send(cmd, *args, &block)
      end
    end
  end
end


而根据rails guide介绍,不支持整批回滚的数据库,在migrate中断时,需要自己手动整好:

On databases that support transactions with statements that change the schema, migrations are wrapped in a transaction. If the database does not support this then when a migration fails the parts of it that succeeded will not be rolled back. You will have to rollback the changes that were made by hand