woshidan's loose leaf

ぼんやり勉強しています

ActiveRecordのmigrationまわり眺めてた

ActiveRecordでいくつかのメソッドでクエリを読もうと思っていたはずが、 なんか迷子になってしまいました。まさにぼんやり勉強しています、だ。

それでmigrationファイルの生成について、

rails g migration filename をしっかり書くと、templateの方で 属性名を書いてくれそうだなーとか、

属性名等が書いてあるmigrationファイルは設定ファイルっぽいけれど、 #changeメソッドの実行みたいに元のMigrationのクラスで書いてあるから、 設定ファイルじゃなくて、ブロックを与えられたrubyメソッドなのかなぁ、

とまで思って力つきて本題に戻る事にしたので、メモだけ投下。

rails/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb

def create_migration_file
  set_local_assigns!
  validate_file_name!
  migration_template @migration_template, "db/migrate/#{file_name}.rb"
end

set_local_assignsの適当なところだけ読み取ってみると、

def set_local_assigns!
  @migration_template = "migration.rb"
  case file_name
  when /^(add|remove)_.*_(?:to|from)_(.*)/
    @migration_action = $1
    @table_name       = normalize_table_name($2)
  when /join_table/
    if attributes.length == 2
      @migration_action = 'join'
      @join_tables      = pluralize_table_names? ? attributes.map(&:plural_name) : attributes.map(&:singular_name)

      set_index_names
    end
  when /^create_(.+)/
    @table_name = normalize_table_name($1)
    @migration_template = "create_table_migration.rb"
  end
end

$1とかは //.matchでなくて//=~などで正規表現のマッチングを行ったとき、 マッチした部分が頭の方から$1, $2として取得できるらしいので、それっぽい。

set_local_assignsの適当なところだけ読み取ってみると、

  • migrationファイルを作る時のファイル名でmigrationファイルのテンプレート名を決める
  • migrationファイルのテンプレートに渡すローカル変数を取得するためのメソッドを呼び出す

set_index_namesなど、indexを追加する場合は、generatorで生成してもらえそうな気配ですが、 列の追加とかは無理そう。

また、テンプレートの一例を見ると、

class <%= migration_class_name %> < ActiveRecord::Migration
<%- if migration_action == 'add' -%>
  def change
<% attributes.each do |attribute| -%>
  <%- if attribute.reference? -%>
    add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
  <%- elsif attribute.token? -%>
    add_column :<%= table_name %>, :<%= attribute.name %>, :string<%= attribute.inject_options %>
    add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
  <%- else -%>
    add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
    <%- if attribute.has_index? -%>
    add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
    <%- end -%>
  <%- end -%>
<%- end -%>
  end
<%- elsif migration_action == 'join' -%>
  def change
    create_join_table :<%= join_tables.first %>, :<%= join_tables.second %> do |t|
    <%- attributes.each do |attribute| -%>
      <%= '# ' unless attribute.has_index? -%>t.index <%= attribute.index_name %><%= attribute.inject_index_options %>
    <%- end -%>
    end
  end
<%- else -%>
  def change
<% attributes.each do |attribute| -%>
<%- if migration_action -%>
  <%- if attribute.reference? -%>
    remove_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
  <%- else -%>
    <%- if attribute.has_index? -%>
    remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
    <%- end -%>
    remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
  <%- end -%>
<%- end -%>
<%- end -%>
  end
<%- end -%>
end

存外ガリゴリ書いてるな! という感じです。 なんか、こういうの見るとちょっとだけ嬉しくなってしまいます。

読みにくいので、テーブルの作成の方を読むと

class <%= migration_class_name %> < ActiveRecord::Migration
  def change
    create_table :<%= table_name %> do |t|
<% attributes.each do |attribute| -%>
<% if attribute.password_digest? -%>
      t.string :password_digest<%= attribute.inject_options %>
<% elsif attribute.token? -%>
      t.string :<%= attribute.name %><%= attribute.inject_options %>
<% else -%>
      t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
<% end -%>
<% end -%>
<% if options[:timestamps] %>
      t.timestamps
<% end -%>
    end
<% attributes.select(&:token?).each do |attribute| -%>
    add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
<% end -%>
<% attributes_with_index.each do |attribute| -%>
    add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
<% end -%>
  end
end

上のmigration.rbを読んでいて、どこでローカル変数を割り当てているのかよく分からなかったけれど、 ActiveRecord::Migrationを読んでみて、どのタイミングSQL発行しているのかな、ということで、

って思ったけど、あれ、createメソッドとかがあって、 設定ファイルっぽいブロックを受け取って、 そのブロックの値を読んで実行しているっぽい?

    #   class FixTLMigration < ActiveRecord::Migration
    #     def change
    #       revert do
    #         create_table(:horses) do |t|
    #           t.text :content
    #           t.datetime :remind_at
    #         end
    #       end
    #       create_table(:apples) do |t|
    #         t.string :variety
    #       end
    #     end
    #   end

に対して、

def run(*migration_classes)
  opts = migration_classes.extract_options!
  dir = opts[:direction] || :up
  dir = (dir == :down ? :up : :down) if opts[:revert]
  if reverting?
    # If in revert and going :up, say, we want to execute :down without reverting, so
    revert { run(*migration_classes, direction: dir, revert: true) }
  else
    migration_classes.each do |migration_class|
      migration_class.new.exec_migration(@connection, dir)
    end
  end
end

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