文章转自我的 Blog http://xjz.pw,转载请注明出处
PS: 文笔不好,大家见谅:)
在团队中,大家是否出现,DB 中的字段只有少数人知道这个字段是在做什么。或是折腾数据的同学,拿到我们的数据后,很多数据字段完全不知道是做啥的。
为了让大家更好的理解数据库各字段的意义,通常的做法是为各个字段加上注释。而加注释的方式也很多,不同的方式可以达到不同的效果。比如:
我是喜欢能不修改或是影响原有功能就尽量不去动。所以我通过了创建一张表,专门来存放 comments,表的结构比较简单,如下
create_table(:database_comments) do |t|
t.string :table_name, null: false
t.string :column_name
t.string :comment, null: false
t.tags # split by ','
t.index [:table_name, column_name], unique: true
end
当 column_name 为空时,表示是一个 table 的 comment,否则就是对应 column 的 comment,顺便为每个列打上 tag,比如可以为 password/token 等标记为"secret"
嗯,数据结构就这样定好了,接下来开始折腾了
既然有 MigrationComments 的例子在先,那么,我们也可以使用同样的方式,通过扩展 ActiveRecord 的 migration 来做这件事了。
def change
set_table_coment :table_name, "A table comment"
set_column_comment :table_name, :column_name, "A column comment"
end
Or
def change
change_table :table_name do |t|
t.comment "A table comment"
t.change_comment :column_name, "A column comment"
end
end
那么我们先来了解,ActiveRecord Migration 的实现,显然,我们通过现有的 MigrationComments + Google + ActiveRecord 源代码来了解是最快的
通过 MigrationComment 代码和 Google 结果,了解到,想要扩展这些功能,需要修改 ActiveRecord 的代码的,那么接下来,就先了解下下 migration 是如何工作的吧
以 Rails 4.1.5 activerecord 为例
大家应该都知道运行 migrate 的入口是ActiveRecord::Migrator.migrate('./db/migrations')
,如果不知道的话,现在也知道了:)
这里判断了是要 down 还是 up,也就是执行新的 migration 还是 rollback。
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
# ...
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
def down(migrations_paths, target_version = nil, &block)
migrations = migrations(migrations_paths)
migrations.select! { |m| yield m } if block_given?
self.new(:down, migrations, target_version).migrate
end
而在initialize 做了准备工作,初始了 schema_migrations 表
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 = migrations
validate(@migrations)
Base.connection.initialize_schema_migrations_table
end
initialize_schema_migrations_table 方法最终指向了 SchemaMigration.create_table
def create_table(limit=nil)
unless table_exists?
version_options = {null: false}
version_options[:limit] = limit if limit
connection.create_table(table_name, id: false) do |t|
t.column :version, :string, version_options
end
connection.add_index table_name, :version, unique: true, name: index_name
end
end
之后就是执行每一个Migration#migrate,告诉它是要 up 还是 down
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
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
我们先来看change
method,它直接执行了,而在 Migration 类中并没有定义像create_table
/change_table
这里 method,继续可以看到 Migration 定义了 method_missing
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] = proper_table_name(arguments.first, table_name_options)
arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table
end
end
return super unless connection.respond_to?(method)
connection.send(method, *arguments, &block)
end
end
都直接转发给 connettion(ActiveRecord::Base.connection) 了,而所有的 connection 都是由继承自AbstractAdapter的类的实例
ActiveRecord::Base.connection.method(:create_table).source_location
ActiveRecord::Base.connection.method(:change_table).source_location
ActiveRecord::Base.connection.method(:add_column).source_location
ActiveRecord::Base.connection.method(:change_column).source_location
由上可知道 migration 中的这些 method 都是来自SchemaStatements,或是在各 db 的 adapter 中做了一些修改
同样,由原代码可以看出,像create_table
/ change_table
block 中的 method 都是由一个单独的类来定义的
def create_table(table_name, options = {})
td = create_table_definition table_name, options[:temporary], options[:options], options[:as]
if !options[:as]
unless options[:id] == false
pk = options.fetch(:primary_key) {
Base.get_primary_key table_name.to_s.singularize
}
td.primary_key pk, options.fetch(:id, :primary_key), options
end
yield td if block_given?
end
if options[:force] && table_exists?(table_name)
drop_table(table_name, options)
end
execute schema_creation.accept td
td.indexes.each_pair { |c,o| add_index table_name, c, o }
end
# ...
private
def create_table_definition(name, temporary, options, as = nil)
TableDefinition.new native_database_types, name, temporary, options, as
end
到这里我们已经了解到了,如何去写一个自己的 migration method 了,但是还没完呢,在 migrate 时可以看到,如果以 down 方式运行会 revert { change }
,那么这个 revert 是怎么实现的呢?继续看代码
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
这里又出现新玩意了:CommandRecorder
这里比较迷惑的地方是@connection.revert { yield }
,外面传入的明明是revert { change }
,
change 中的 methods 作用域还是当前 Migration 实例,这样传进来可以执行?,看了好一会才回想起,
method_missing
时会 call @connection.xxx
,而上次的代码,在执行前做了@connection = recorder
了,写成一坨了。。。
然后看了 CommandRecorder 中的代码,相信已经没人会对 change 可以同时支持 migrate 与 rollback 感到神奇了。
# record +command+. +command+ should be a method name and arguments.
# For example:
#
# recorder.record(:method_name, [:arg1, :arg2])
def record(*command, &block)
if @reverting
@commands << inverse_of(*command, &block)
else
@commands << (command << block)
end
end
# Returns the inverse of the given command. For example:
#
# recorder.inverse_of(:rename_table, [:old, :new])
# # => [:rename_table, [:new, :old]]
#
# This method will raise an +IrreversibleMigration+ exception if it cannot
# invert the +command+.
def inverse_of(command, args, &block)
method = :"invert_#{command}"
raise IrreversibleMigration unless respond_to?(method, true)
send(method, args, &block)
end
[:create_table, :create_join_table, :rename_table, :add_column, :remove_column,
:rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps,
:change_column_default, :add_reference, :remove_reference, :transaction,
:drop_join_table, :drop_table, :execute_block, :enable_extension,
:change_column, :execute, :remove_columns, :change_column_null # irreversible methods need to be here too
].each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method}(*args, &block) # def create_table(*args, &block)
record(:"#{method}", args, &block) # record(:create_table, args, &block)
end # end
EOV
end
{ 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
神秘的面纱揭开后,我想大家都不会对真像感到惊奇了...
通过重定义所有的 migration 中的方法,这里方法执行的只是record(:xxx, args, &block)
,
再定义好每个 methods 对应的 invert 操作,在 invert 操作中可以根据正常执行的参数得出反向操作的参数,
最后在Migration#revert中再把这些command执行
def change
add_column_comment :users, :mobile, "user mobile'"
add_table_comment :users, "user table'"
add_comments :table_name do |c|
c.table 'table comments'
c.column :a, 'column comment'
c.column :s, 'comment', sensitive: true
end
end
相信到这里时,上面的的实现方式大家应该已经会了,接下来,就可以随意折腾 ActiveRecord Migration
本来只是想讲讲写一个 migratino commnet 的 gem,想到什么写什么,然后就把 Migration 的实现都讲了, 那么,折腾这事,就交给大家了,我折腾好的代码在公司项目上,就不拉出来了:)