db:migrateすると何が起こるか。ActiveRecordコードリーデイング

Wantedlyでは、Railsによる開発が行われており、社内で有志のコードリーディングを行っています。

## WHY
何故やるのか：コードレベルまで一度追うことで、問題が起こった時の解決までの時間が短縮されます。
また、本質的になにをやっているか理解するとRailsの魔法感を低減できます。

## WHAT
なにをするのか：今回はmigrate周りのコードリーディングです。コマンドからSQL文発行までのソースを追います。

## HOW
どうやってやるか？実際のコードを読みます。

## rake db:migrateを実行するとなにが起こるか

コマンドからSQL文発行までのソースを追いたい。

## source file
今回呼んだソースはRails 4.0.13のものです。自分の環境では以下に入っていました。

~/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/activerecord-4.0.13

## task :migrate (railties/database.rake)
rakeタスクはこのファイルで定義されています。
ここでは、`Migrator.migrate`を呼び出すのが主なシゴト。

```rb
  desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
  task :migrate => [:environment, :load_config] do
    ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
    ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration|
      ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope)
    end
    db_namespace['_dump'].invoke
  end
```

Migratorは何なのかと思いながら、`Migrator.migrate`と引数の`Migrator.migrations_paths`をみる。

## Migrator#migrate (activerecord-4.0.13/lib/active_record/migration.rb)
現在のバージョンによって、バージョンを進めるのか、戻すのか、何も変えないのかを決める。
（このファイル内に`def migrate`が3つある）

`def migrate(migrations_paths, target_version = nil, &block)`

target_versionとcurrent_versionの状態によって、行うアクションを決める。
単純に`rake db:migrate`とした場合は、普通１番上の`up`を呼ぶ。
現在よりも古いバージョンを指定した場合は`down`を呼ぶ。

```ruby
when target_version.nil?
  up(migrations_paths, target_version, &block)
when current_version == 0 && target_version == 0
  []
when current_version > target_version
  down(migrations_paths, target_version, &block)
else
  up(migrations_paths, target_version, &block)
end
```

### target_versionとcurrent_versionはどうやって決まるか？
target_versionは指定しなければnil
current_versionは、以下のメソッドにより、繋がっているDBのSchemaMigration.table_nameのversion数値の最大値が用いられる。schme.rbのバージョン値は関係ないんですね。

```ruby
def current_version
  sm_table = schema_migrations_table_name
  if Base.connection.table_exists?(sm_table)
    get_all_versions.max || 0
  else
    0
  end
end
```

### Migrator.migration_paths
デフォルトでは、`['db/migrate']`が帰る。Arrayに`to_a`してもArrayのまま変化しない。

Wantedlyでは複数のDBをRailsに繋いでいます。
デフォルトではなく、`rake mail_db:migrate`とした場合は以下。

rakeファイルのload_configでmigration_pathsに"mail_db"が入る。
`ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths`

## Migrator.up
現在のmigrationを持ってきて、Migratorオブジェクトを作ってmigrateを呼ぶ。

`migrations.select! { |m| yield m } if block_given?`
migration.scopeとENV['SCOPE']が違うものを実行しないようにしている。

```ruby
def up(migrations_paths, target_version = nil)
  migrations = migrations(migrations_paths)
  migrations.select! { |m| yield m } if block_given?

  self.new(:up, migrations, target_version).migrate
end
```

そもそもmigrationsとは？

### Migrator.migrations
migrateディレクトリをみて、マッチするファイルを持ってくる。

- Dir
	- `ワイルドカードの展開を行い、 パターンにマッチするファイル名を文字列の配列として返します。`

なので、fileには、指定したmigrateディレクトリ以下のマッチしたファイルが文字列で全部入る。
["db/migrate/20110627144734_create_jobs.rb",
 "db/migrate/20110628003714_devise_create_users.rb",
 "db/migrate/20110629071604_remove_applicant_from_jobs.rb",
 "db/migrate/20110629071620_remove_connector_from_jobs.rb",
 ...]みたいな感じ。

ここから更に、`バージョン数_名前_.rb`に一致するものだけを見て、MigrationProxyなるものを作成する。
これは実際のMigrationを呼び出すもの。

```ruby
MigrationProxy is used to defer loading of the actual migration classes until they are needed

delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration
```
  
これがmigrationsになる。

```ruby
def migrations(paths)
  paths = Array(paths)

  files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }]

  migrations = files.map do |file|
    version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/).first

    raise IllegalMigrationNameError.new(file) unless version
    version = version.to_i
    name = name.camelize

    MigrationProxy.new(name, version, file, scope)
  end

  migrations.sort_by(&:version)
end
```

### Migrator#initialize
Migrationを呼び出す人。
migrationsに文字列が一つでもあったらwarnを出してmigrationsを取得し直す。
その後、validateを呼び出してから、`Base.connection.initialize_schema_migrations_table`

```ruby
    def initialize(direction, migrations, target_version = nil)
      raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?

      @direction         = direction
      @target_version    = target_version
      @migrated_versions = nil

      if Array(migrations).grep(String).empty?
        @migrations = migrations
      else
        ActiveSupport::Deprecation.warn "instantiate this class with a list of migrations"
        @migrations = self.class.migrations(migrations)
      end

      validate(@migrations)

      Base.connection.initialize_schema_migrations_table
    end
```

### Base.connection.initialize_schema_migrations_table
`ActiveRecord::SchemaMigration.create_table`をやると思っている
これは、テーブルが存在しなければ以下のテーブルを作る。

```ruby
connection.create_table(table_name, id: false) do |t|
  t.column :version, :string, version_options
end
```

### Migrator#migrate
各々のmigrationに対して、`execute_migration_in_transaction(migration, @direction)`を呼び出す。


```ruby
    def migrate
      if !target && @target_version && @target_version > 0
        raise UnknownMigrationVersionError.new(@target_version)
      end

      running = runnable

      if block_given?
        message = "block argument to migrate is deprecated, please filter migrations before constructing the migrator"
        ActiveSupport::Deprecation.warn message
        running.select! { |m| yield m }
      end

      running.each do |migration|
        Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger

        begin
          execute_migration_in_transaction(migration, @direction)
        rescue => e
          canceled_msg = use_transaction?(migration) ? "this and " : ""
          raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
        end
      end
    end
```

### execute_migration_in_transaction
migration.migrate(direction)を呼び出したあとに、それを記録する。
ddl_transaction(migration)はTransaction処理が可能（Base.connection.supports_ddl_transactions?）ならTransaction処理の中で行う。

```ruby
    def execute_migration_in_transaction(migration, direction)
      ddl_transaction(migration) do
        migration.migrate(direction)
        record_version_state_after_migrating(migration.version)
      end
    end
```

### Migration#migrate(direction)
ここでは既に、どの変更をどの方向に切り替える、という情報が揃っている。

`exec_migration(conn, direction)`を実行。
`up`であれば"migrating"として、終了後に実行時の経過時間を表示。
"migrated (2.4854)"みたいに表示する。

```ruby
# Execute this migration in the named direction
def migrate(direction)
  return unless respond_to?(direction)

  case direction
  when :up   then announce "migrating"
  when :down then announce "reverting"
  end

  time   = nil
  ActiveRecord::Base.connection_pool.with_connection do |conn|
    time = Benchmark.measure do
      exec_migration(conn, direction)
    end
  end

  case direction
  when :up   then announce "migrated (%.4fs)" % time.real; write
  when :down then announce "reverted (%.4fs)" % time.real; write
  end
end
```

### Migration.exec_migration(connection, direction)

`if respond_to?(:change)`で changeが定義されていれば、
changeかrevertを実行する。
up, downとかしかなければ、upかdownを実行する。

```ruby
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
```

### Migration.change

Migrationファイルのchangeメソッドが呼ばれる。我々が書いているあのファイルだ。
ここでは、add_columnが呼ばれることにする。

add_columnは定義されていない。これはmethod_missingによって実行される。
method='add_column'のときは、ActiveRecord::Base.connectionに`add_column`メソッドがあるかを確かめて、
`connection.send(method, *arguments, &block)`により実行。

実行されたら、`say_with_time`というフランクな名前のメソッドにより、実行時間と共に出力される。

```ruby
def method_missing(method, *arguments, &block)
  arg_list = arguments.map{ |a| a.inspect } * ', '

  say_with_time "#{method}(#{arg_list})" do
    unless @connection.respond_to? :revert
      unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method)
        arguments[0] = Migrator.proper_table_name(arguments.first)
        arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table
      end
    end
    return super unless connection.respond_to?(method)
    connection.send(method, *arguments, &block)
  end
end
```

## 最後：add_columnはなにをやっているか
ここから先は結構、PostgresSQLとかSQLiteとか固有のものと共通のものがあってごちゃごちゃしている印象。

### SchemaStatements.add_column (postgresql/schema_statements.rb)

```ruby
def add_column(table_name, column_name, type, options = {})
  clear_cache!
  super
end
```

### SchemaStatements.add_column (abstract/schema_statements.rb)

```ruby
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
  at = create_alter_table table_name
  at.add_column(column_name, type, options)
  execute schema_creation.accept at
end
```


```ruby
alter_tableというものを作っているっぽい。
AlterTable.new create_table_definition(name, false, {})

add_column(name, type, options)だけの情報を持つクラス
```

```ruby
def schema_creation
  SchemaCreation.new self
end

class SchemaCreation
	def accept(o)
    	m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}"
		send m, o
	end
... 
end
visit_AlterTableが呼ばれる。
```

```ruby
def visit_AlterTable(o)
  sql = "ALTER TABLE #{quote_table_name(o.name)} "
  sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
end

def visit_AddColumn(o)
  sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
  sql = "ADD #{quote_column_name(o.name)} #{sql_type}"
  add_column_options!(sql, column_options(o))
end
```

execute の中で`@connection.async_exec(sql)`が呼ばれてSQLが実行される。

## おわり
実際にコードを読んでみることで、日常的に打っているコマンドが内部で何をやっているかコードレベルで理解しやすくなりました。

特にバージョンの話とか、migrateファイルのadd_columnみたいなメソッドが、実際はmethod_missingで拾われていて、databaseのコネクションにメソッドが追加されたら自動的に使えるので面白い仕組みだなとか。

Wantedlyの場合は、WantedlyのコードベースでRailsを触るのが初めてというメンバーも多く（自分もJavaとPythonの人でした）、こういった読み会は勉強になります。
他のメンバーの読み方とか、考え方とかを共有できるという意味でもお勧めです。