LoginSignup
0
0

More than 5 years have passed since last update.

说说 Rails 的 active record migration

Last updated at Posted at 2017-02-05

对于 migration 来说,它就是 Active record 编写的一套 DSL 用来修改数据库表的 schema. 不同于直接通过 SQL 来实现, 它能够更加容易的通过 ruby 的代码来实现.

本文主要有下面几个方面:

  • 通过使用生成器来完成创建
  • active record 提供的所有用来维护数据的方法
  • 通过 rails 的内置 task 方法来完成migration
  • migration和schema.rb 之间的关联

migration 的概览

创建migration

创建独立的 migration

migration作为单独的文件存放在db/migration 文件夹下面, 每一个都是独立的 migration 类. 这个文件名有一定的规则就是时间戳加动作, 如YYYYMMDDHHMMSS_create_products.rb, 意思很明了. 就是在某个时间点,下划线加上 migration 的名称.而对于 migration 的类名, 也完全是根据文件名的移形名字来的,只是把它变成了驼峰命名. 如这个移形文件20080906120000_create_products.rb 实际对应就会生成一个CreateProducts类, 而20080906120001_add_details_to_products.rb 同样就是AddDetailsToProducts. 而这里文件名里的时间戳就是用来告诉 rails 如何按时间顺序来完成所有的移形. 所以有时候你图方便直接拷贝了一个移形文件, 这个时候就要注意时间戳了, 如果二者相互没有依赖可能也不会出现问题.

当然这里如果我们每次手动去拿一个时间戳然后创建一个移形文件就无趣了, rails 当然会提供方法的生成器来完成这个动作. 如下面我们创建一个移形:


$ bin/rails generate migration AddPartNumberToProducts

将会生成下面这样一个文件:

class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
  def change
  end
end

这里, 如果在创建这个移形文件的时候, 文件名如果遵循了如AddXXXToYYY 或者RemoveXXXFromYYY, active record 就会很贴心的直接在移形类里添加对应相似的方法.

$ bin/rails generate migration AddPartNumberToProducts part_number:string
class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
  def change
    add_column :products, :part_number, :string
  end
end

我们还可以直接给某个字段添加索引

$ bin/rails generate migration AddPartNumberToProducts part_number:string:index
will generate

class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
  def change
    add_column :products, :part_number, :string
    add_index :products, :part_number
  end
end

当然移除某个字段也不是问题

$ bin/rails generate migration RemovePartNumberFromProducts part_number:string
generates

class RemovePartNumberFromProducts < ActiveRecord::Migration[5.0]
  def change
    remove_column :products, :part_number, :string
  end
end

当然一次添加多个也没有问题:

$ bin/rails generate migration AddDetailsToProducts part_number:string price:decimal
generates

class AddDetailsToProducts < ActiveRecord::Migration[5.0]
  def change
    add_column :products, :part_number, :string
    add_column :products, :price, :decimal
  end
end

当然你可以写成只是创建某个对象CreateXXX, 然后跟多个字段的形式, 完全没有问题(我说, 这样好吗 有必要吗?)


$ bin/rails generate migration CreateProducts name:string part_number:string
generates

class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.string :name
      t.string :part_number
    end
  end
end

其实, 上面各种各样的玩法也是一个开始, 很多时候你大可完全直接去编辑db/migrate 目录下的如YYYYMMDDHHMMSS_add_details_to_products.rb文件来得快.
说到这里migrate 同样支持关联聚合关系的创建.( belongs_to)


$ bin/rails generate migration AddUserRefToProducts user:references
generates

class AddUserRefToProducts < ActiveRecord::Migration[5.0]
  def change
    add_reference :products, :user, index: true, foreign_key: true
  end
end

这个 migrate 将会创建一个user_id 的外键,顺便给它索引. 关于add_reference 还有很多的选项可以看看 API 文档.
当然如果在 migration 的文件名添加了JoinTable的话, 可以直接创建生成jointable 的命令.

$ bin/rails g migration CreateJoinTableCustomerProduct customer product
will produce the following migration:

class CreateJoinTableCustomerProduct < ActiveRecord::Migration[5.0]
  def change
    create_join_table :customers, :products do |t|
      # t.index [:customer_id, :product_id]
      # t.index [:product_id, :customer_id]
    end
  end
end

对象生成器

对于对象生成器或者骨架生成器他们来说, 在使用他们的时候同时已经为你生成了对象的 migration, 这个 migration 已经包含了创建相关表, 如果你告诉 Rails 添加什么字段上面的都会为了一气搞定. 看看下面的例子:


$ bin/rails generate model Product name:string description:text

对于上面的对象生成器的执行结果就是会生成下面的 migration:

class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.string :name
      t.text :description

      t.timestamps
    end
  end
end

这里你可以添加任意多的字段声明

使用类型定义

我们可以在使用生成器的时候直接给字段定义类型定义声明, 通过使用大括号的方式给它一个类型:

$ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}

将会生成这样的定义:

class AddDetailsToProducts < ActiveRecord::Migration[5.0]
  def change
    add_column :products, :price, :decimal, precision: 5, scale: 2
    add_reference :products, :supplier, polymorphic: true, index: true
  end
end

有很多玩法了, 看看 API 文档好了.

怎么写一个migration

当我们通过生成器生成了 migration 之后,才刚开始.

创建表

通过create_table 我们可以创建表,算是最基本的函数了. 但是通常我们通过大部分的生成器就可以完成这个步骤, 看上去就像下面这样的:


create_table :products do |t|
  t.string :name
end

这个将会创建一个products 的表, 然后一个name的字段.(同时还有一个id 的字段). 默认得create_table 命令将会创建一个id 的主键. 你可以通过:primary_key 的选项来修改这个默认值, 但是这个情况下就需要修改对应的 model. 当然你可以不要这个主键, 只要给id:false 的参数就 ok. 如果你需要设置数据库特定的参数的时候直接在option的参数里指定就可以. 如下面的例子:


create_table :products, options: "ENGINE=BLACKHOLE" do |t|
  t.string :name, null: false
end

这个字符串会追加到创建数据库表的SQL 语句后,(在使用MySQL或者MariaDB的时候, 默认的引擎是InnoDB)

同时你也完全可以通过: comment 的选项给数据库表添加任何的注释.这个动作也就是通过追加选项注释到实际的 SQL 文实现.

创建 Join 表

migration 通过create_join_table 来创建HABTM 的关联表, 通常是这样的:

create_Join_table :products, :categories

这个将会创建一个categories_products 表, 有两个字段category_id 还有product_id, 这些字段都被设置的: null 的选项,默认为false.


create_join_table :products, :categories, column_options: { null: true }

默认最后这个关联表的名字将是来自于创建 migration 的时候给的2个表明通过字母排序得到的. 同时可以通过: table_name 的参数来指定表名.


create_join_table :products, :categories, table_name: :categorization

它将会创建一个categorization的表.

create_join_table 还可以接受代码块,这样就可以添加任意的设置.或者额外的字段.

create_join_table :products, :categories do |t|
  t.index :product_id
  t.index :category_id
end

修改表

create_table 的表兄就是change_table 了. 用来修改现有的表结构,通常它和创建表的构成一样.但是代码块里yeild 出来的对象有更多神奇的用法.


change_table :products do |t|
  t.remove :description, :name
  t.string :part_number
  t.index :part_number
  t.rename :upccode, :upc_code
end

上面的定义是分别移除了descriptionname字段,添加了part_time 的字段并且给它添加了索引, 还有将upccode重命名为upc_code.

修改字段

还有和remove_columnadd_column 命令相似的change_column,


change_column :products, :part_number, :text

这个动作就是将products 表里的part_number 设置为: text 类型, 注意 这里的操作是不可逆的.
另外还有change_column_nullchange_column_default 方法, 他们用来修改特定字段的约束和默认值.


change_column_null :products, :name, false
change_column_default :products, :approved, from: true, to: false

上面的动作就是将: name 字段添加不可空的约束,然后给approved 的默认值是false.

注意上面这个动作可以通过change_column 来完成, 但是那样就变成了不可逆的操作

字段修饰符

字段的类型定义可以在创建或者修改的时候设置.

  • limit 可以给string/text/binary/integer类型的字段设置最大值.
  • precision 定义了decimal 字段的precision, 也就是多大.
  • scale,定义了 decimal 字段的精度, 也就是小数点后面的位数.
  • polymorphic 给字段添加belong_to 的关联.
  • null, 字段是否可为空
  • default 给字段添加默认值, 注意 如果你使用动态值(如日期)默认值将会在第一次使用的时候指定.
  • index 就是给某个字段添加索引
  • comment 就是给字段添加注释

有些特定的适配器可能会针对特定的数据库有特定的选项可以使用. 查阅 API.

外键

虽然不是要求的, 但是你可能会通过添加外键约束来 确保引用一致性.


add_foreign_key :articles, :authors

这个动作的结果就是会给articles 这个表添加一个外键author_id, 它就是来自author 表的主键id, 如果仅是通过上面的命令而不能够得到外键名,可以通过:column: primary_key 选项来指定.

Rails 通常会通过from_tablecolumn 两个变量来给每一个外键生成一个以fk_rails_开头的10字符名字.同时也可以通过: name 选项来指定一个不同的名字.

Active record只支持单外键, execute and structure.sql are required to use composite foreign keys. See Schema Dumping and You.

移除外键也是很轻松的:

# let Active Record figure out the column name
remove_foreign_key :accounts, :branches

# remove foreign key for a specific column
remove_foreign_key :accounts, column: :owner_id

# remove foreign key by name
remove_foreign_key :accounts, name: :special_fk_name

当帮助方法不够用的情况

如果发现active record 提供的帮助方法不够用的时候可以直接执行SQL 来实现.

Product.connection.execute("UPDATE products SET price = 'free' WHERE 1=1")

更多详情和单独的方法, 可以通过确认 API 文档来学习了解, 这里提供了 change up 和 down 的方法, 创建表的更多, 还有修改表.

修改表

使用 migration 的时候,更多的就是通过change 方法. 这个会对于大多数 case 管用. 因为 active record 都可以轻松 hold 住,当前支持这些方法:

add_column
add_foreign_key
add_index
add_reference
add_timestamps
change_column_default (must supply a :from and :to option)
change_column_null
create_join_table
create_table
disable_extension
drop_join_table
drop_table (must supply a block)
enable_extension
remove_column (must supply a type)
remove_foreign_key (must supply a second table)
remove_index
remove_reference
remove_timestamps
rename_column
rename_index
rename_table

change_table 只要不在代码块里使用change, 或者chagne_default或者remove 方法. 都是可以回滚撤销的. remove_column 也是可以回滚的, 只要在执行移除的时候告诉 rails 当前要移除的字段的类型就可以回滚.

remove_column :posts, :slug, :string, null: false, default: '', index: true

当然你在使用其他方法的时候,可以通过reversible 方法和写updown 方法来替换使用change 方法.

使用reversible

复杂的 migration 可能Active Record并不知道如何来回滚操作,但是可以通过reversible方法 来表明当你想要 migration 或者回滚的时候怎么办.

class ExampleMigration < ActiveRecord::Migration[5.0]
  def change
    create_table :distributors do |t|
      t.string :zipcode
    end

    reversible do |dir|
      dir.up do
        # add a CHECK constraint
        execute <<-SQL
          ALTER TABLE distributors
            ADD CONSTRAINT zipchk
              CHECK (char_length(zipcode) = 5) NO INHERIT;
        SQL
      end
      dir.down do
        execute <<-SQL
          ALTER TABLE distributors
            DROP CONSTRAINT zipchk
        SQL
      end
    end

    add_column :users, :home_page_url, :string
    rename_column :users, :email, :email_address
  end
end

通过使用reversible 可以来确保正确的 SQL 执行顺序,正如上面的例子里的目的, 如果 example 被回滚了, 这个down 块就会被执行, 结果就是移除home_page_url, 然后drop 掉distrubutors 表.

当时通常呢, migration 是会做一点不可恢复的事情, 例如往往会删除掉一些数据的. 这样的情况下,将会在 down 的方法里报ActiveRecord::IrreversibleMigration 的错误.

使用up/down 方法

你可以使用老的方式updown 来代替change 方法.也就是说up 方法会描述如何转换当前的 schema, 还有down 方法会将up 方法的一切撤销. 换句话说, 数据库的 schema 会在 up 和 down 之后什么都没有发生一样. 举个例子, 在 up 方法里创建了一张表, 而在 down 的方法里就要 drop 掉这个表.应该很明确的表达回滚的顺序. 下面的写法和 reversible的写法是等效的:

class ExampleMigration < ActiveRecord::Migration[5.0]
  def up
    create_table :distributors do |t|
      t.string :zipcode
    end

    # add a CHECK constraint
    execute <<-SQL
      ALTER TABLE distributors
        ADD CONSTRAINT zipchk
        CHECK (char_length(zipcode) = 5);
    SQL

    add_column :users, :home_page_url, :string
    rename_column :users, :email, :email_address
  end

  def down
    rename_column :users, :email_address, :email
    remove_column :users, :home_page_url

    execute <<-SQL
      ALTER TABLE distributors
        DROP CONSTRAINT zipchk
    SQL

    drop_table :distributors
  end
end

如果操作是不可逆的,应该报ActiveRecord::IrreversibleMigration这个错误异常, 如果某人尝试回滚操作,需要提示异常.

回滚

通过使用revert 方法可以让 active record 回滚操作.

require_relative '20121212123456_example_migration'

class FixupExampleMigration < ActiveRecord::Migration[5.0]
  def change
    revert ExampleMigration

    create_table(:apples) do |t|
      t.string :variety
    end
  end
end

这个revert 方法可以通过块来指定到底怎么如何回滚,这个方式可以来回滚特定部分的操作,例如,已经提交了ExampleMigration 然后却想要通过使用 active record 的校验来代替CHECK 的约束, 来达到校验zipcode.

class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration[5.0]
  def change
    revert do
      # copy-pasted code from ExampleMigration
      reversible do |dir|
        dir.up do
          # add a CHECK constraint
          execute <<-SQL
            ALTER TABLE distributors
              ADD CONSTRAINT zipchk
                CHECK (char_length(zipcode) = 5);
          SQL
        end
        dir.down do
          execute <<-SQL
            ALTER TABLE distributors
              DROP CONSTRAINT zipchk
          SQL
        end
      end

      # The rest of the migration was ok
    end
  end
end

执行 migration

回滚

设置 db

重置 db

执行特定的 migration

通过RAILS_ENV 来在不同的环境里执行 migration

修改 migration 的输出格式

修改现有的 migration

导出 db schema

schema 文件是什么

不同的导出

schema 导出和数据源控制

active record 的一致性

migration 和种子数据

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0