说说 Rails 的 active record migration


[](http://guides.rubyonrails.org/active_record_migrations.html)

对于 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
```

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

#### 修改字段

还有和` remove_column`与` add_column` 命令相似的` change_column`,

```

change_column :products, :part_number, :text
```

这个动作就是将` products` 表里的` part_number` 设置为`: text` 类型, *注意* 这里的操作是不可逆的.
另外还有` change_column_null` 和` change_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_table` 和` column` 两个变量来给每一个外键生成一个以` fk_rails_`开头的10字符名字.同时也可以通过`: name` 选项来指定一个不同的名字.

>Active record只支持单外键,[ execute and structure.sql are required to use composite foreign keys. See Schema Dumping and You.](http://guides.rubyonrails.org/active_record_migrations.html#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 的方法](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html), [创建表的更多](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html), [还有修改表](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html).

#### 修改表

使用 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` 方法和写` up` 和` down` 方法来替换使用` 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` 方法

你可以使用老的方式`up`和` down` 来代替` 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 和种子数据