RailsのMigrationのソースを追ってみたメモ
db:migrateのエントリポイント
おそらくこれ?
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
これがなぜ Rake に含まれるかはなぞとしてActiveRecord::Migratorが大事っぽ
verboseはまぁきっとそののままだろう。
ActiveRecord::Migrator
active_record/migration.rb にあるっぽ。 file名 == Class名じゃないと探しにくい
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/migration.rb#L565
ActiveRecord::Migrator.migrations_paths
とりあえず呼ばれているこれ調べてみる。
def migrations_paths @migrations_paths ||= ['db/migrate'] # just to not break things if someone uses: migration_path = some_string Array(@migrations_paths) end
名前と処理からして、Rails.root からの migrate ファイルを置き場所を指定してるっぽ。
・・場所かえれるのか!
Array()は "a" でも ["a"] でも ["a"] にするため。配列前提なのか。複数いける?
ActiveRecord::Migrator.migrate
いかにも本体っぽい名前
def migrate(migrations_paths, target_version = nil, &block) case 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 end
target_versionはENV["VERSION"]( rake db:migrate VERSION= )を渡した処理のことだろう。飛ばす。
となると up か
ActiveRecord::Migrator.up
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
- migrationを指定したパスからとってきて
- blockで渡されたロジックで選定して
- 実行
っていう風に読めるな。とってくるところから
ActiveRecord::Migrator.migrations
def migrations(paths) paths = Array(paths) # migrations_path下にある migration file っぽい名前のfile("9999_abc999.rb"みたいな)をとってきて files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }] migrations = files.map do |file| # version(先頭の数値部分) # name(その下 "." まで) # scope(その下の "." まで。。なにこれ?) # をファイル名からとる version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?.rb/).first # versionがなければエラー(おそらく大雑把な不正なファイルチェック) raise IllegalMigrationNameError.new(file) unless version version = version.to_i name = name.camelize # proxyのインスタンスにする MigrationProxy.new(name, version, file, scope) end migrations.sort_by(&:version) end
ActiveRecord::MigrationProxy
単純に値を保持しとくためのクラス?
# MigrationProxy is used to defer loading of the actual migration classes # until they are needed class MigrationProxy < Struct.new(:name, :version, :filename, :scope) def initialize(name, version, filename, scope) super @migration = nil end def basename File.basename(filename) end # 何このメソッド # => 調べた # migrate, announce, :write メソッドを migration メソッドで # 取得できるinstanceに委譲する。この仕組みはまた今度調べよう。 delegate :migrate, :announce, :write, :to => :migration private # migration本体のクラスをloadする def migration # nil ガード @migration ||= load_migration end def load_migration # migration fileをrequire require(File.expand_path(filename)) # constantizeは "String" => String にしてくれる超便利Rails拡張メソッド # もはや狂気すら感じるくらいDRY name.constantize.new end end
db:migrateのエントリポイント
またここにもどってきてblockをチェック
ENV["SCOPE"]を渡されていたらそのSCOPEに絞るってことかな
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
ActiveRecord::Migrator#initialzie
ほんでもって、今までの処理でとってきた migrations(<#MigrationProxy>), ENV["VERSION"]を渡して自身のインスタンスをnew
# 引数は(direction(:upか:down?), migrations, target_version(ENV["VERSION"]) self.new(:up, migrations, target_version).migrate
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 # migrationsの中からStringのmigrationがあるかチェックあればwarn 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) # migration情報を管理する scheme_migrationテーブルを無ければ作る。 ActiveRecord::SchemaMigration.create_table end
ActiveRecord::Migrator#migrate
def migrate # 今のコンテキスト(db:migrate)では @target_version が nilのはずだから無視 if !target && @target_version && @target_version > 0 raise UnknownMigrationVersionError.new(@target_version) end # 実行対象のmigrationにしぼる running = runnable # blockでさらにしぼれたみたいだけど deprecationになっているみたい if block_given? ActiveSupport::Deprecation.warn(<<-eomsg) block argument to migrate is deprecated, please filter migrations before constructing the migrator eomsg running.select! { |m| yield m } end # 実行対象 migration(MigrationProxyのインスタンス)のループ running.each do |migration| Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger begin # DDLトランザクションがサポートされていればトランザクションを開始する ddl_transaction do migration.migrate(@direction) record_version_state_after_migrating(migration.version) end rescue => e canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : "" raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace end end end
ActiveRecord::Migrator#runable
def runnable # upの場合は0からmigrations.length - 1まで(要するに全部) runnable = migrations[start..finish] if up? # ran? は排除。要するに実行済みは排除 runnable.reject { |m| ran?(m) } else # skip the last migration if we're headed down, but not ALL the way down runnable.pop if target runnable.find_all { |m| ran?(m) } end end
ActiveRecord::Migrator#run?
def ran?(migration) migrated.include?(migration.version.to_i) end
ActiveRecord::Migrator#migrated
def migrated @migrated_versions ||= Set.new(self.class.get_all_versions) end
ActiveRecord::Migrator.get_all_versions
def get_all_versions # shema_migrationテーブルの全てのレコード SchemaMigration.all.map { |x| x.version.to_i }.sort end
ActiveRecord::Migrator#ddl_transaction
def ddl_transaction # DDLトランザクション(Create tableとかもrollbackできる。PostgreSQLはいけてMySQLは無理)がサポートされていれば if Base.connection.supports_ddl_transactions? # トランザクション内でmigrationする Base.transaction { yield } else yield end end
ActiveRecord::Migration#migrate
MigrationProxy#migrateはproxyの中にある、migration fileに書いてあるClassをnewしたメソッドに委譲されているはずなので、db/migrate 下のファイルをみてみると。
たいていこんな感じで書かれている。
class SomeModels < ActiveRecord::Migration def change add_column :some_models, :some_field, :string end end
この中に migrate メソッドがないのでおそらく Super Classにあるはず
でこれ
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/migration.rb#L402
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| @connection = conn if respond_to?(:change) if direction == :down recorder = CommandRecorder.new(@connection) suppress_messages do @connection = recorder change end @connection = conn time = Benchmark.measure { self.revert { recorder.inverse.each do |cmd, args| send(cmd, *args) end } } else # change メソッドを呼ぶ。migrationの中身だね time = Benchmark.measure { change } end else time = Benchmark.measure { send(direction) } end @connection = nil end case direction when :up then announce "migrated (%.4fs)" % time.real; write when :down then announce "reverted (%.4fs)" % time.real; write end end
ActiveRecord::Migration#method_missing
ほんで、add_columnなんてメソッドがないので当然 method_missingにきて、connectionでとれるインスタンスに委譲されているみたい。
def method_missing(method, *arguments, &block) arg_list = arguments.map{ |a| a.inspect } * ', ' say_with_time "#{method}(#{arg_list})" do unless reverting? unless arguments.empty? || method == :execute 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
ActiveRecord:: ConnectionAdapters:: SchemaStatements#add_column
このクラスの中に migrations ファイル記述する際に使うメソッドが用意されている。
SQLが見れたので今日はここまで。
途中から息切れ。。。
def add_column(table_name, column_name, type, options = {}) add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" add_column_options!(add_column_sql, options) execute(add_column_sql) end