Active Record - 数据库迁移(Migration)

    Migrations(数据库迁移)可以让你用 Ruby 程式来修改数据库结构。相较于直接进数据库系统使用 SQL 修改结构(例如使用 phpMyAdmin 工具来修改),使用 Migrations 可以让我们有记录地进行数据库修改,每次变更就是一笔 Migration 记录。在没有 Migration 之前,如果你手动修改了数据库,那么你就必须通知其他开发者也进行一样的修改步骤。另外,在正式布署的服务器上,你也必须追踪并执行同样的变更才行。而这些步骤如果没有记录下来,就很容易出错。

    Migrations 会自动追踪哪些变更已经执行过了、那些还没有,你只要新增 Migration 档案,然后执行 rake db:migrate 就搞定了。它会自己搞清楚该跑哪些 migrations,如此所有的开发者和正式布署的服务器上,就可以轻易的同步最新的数据库结构。另外一个优点是: Migration 是独立于数据库系统的,所以你不需要烦恼各种数据库系统的语法差异,像是不同型态之类的。当然,如果要针对某个特定数据库系统撰写专属功能的话,还是可以透过直接写 SQL 的方式。

    执行以下指令,就会在 db/migrate/ 目录下产生如 20110203070100_migration_name.rb 的档案

    让我们打开这个档案看看:

    1. class MigrationName < ActiveRecord::Migration[5.1]
    2. def change
    3. end
    4. end

    在这个类别中,包含了一个方法是change,这会在执行这个 migration 时执行。

    Migration 可用的方法

    在上述change方法里,我们有以下方法可以使用:

    对资料表做修改:

    • create_table(name, options) 新增资料表
    • drop_table(name) 移除资料表
    • rename_table(old_name, new_name) 修改资料表名称
    • change_table 修改资料表字段

    个别修改资料表字段:

    • add_column(table, column, type, options) 新增一个字段
    • rename_column(table, old_column_name, new_column_name) 修改字段名称
    • change_column(table, column, type, options) 修改字段的型态(type)
    • remove_column(table , column) 移除字段

    新增、移除索引:

    • add_index(table, columns, options) 新增索引
    • remove_index(table, index) 移除索引

    options 可为空,或是:unique => true表示这是唯一。

    新增、移除外部键限制:

    • add_foreign_key(from_table, to_table, options)

    执行 rails g model 时,Rails就会顺便新增对应的 Migration 档案。以上一章产生的categories migration为例:

    1. class CreateCategories < ActiveRecord::Migration[5.1]
    2. def change
    3. create_table :categories do |t|
    4. t.string :name
    5. t.integer :position
    6. t.timestamps
    7. end
    8. add_column :events, :category_id, :integer
    9. add_index :events, :category_id
    10. end

    其中的 timestamps 会建立叫做 created_at 和 updated_at 的时间字段,这是Rails的常用惯例。它会自动设成资料新增的时间以及会后更新时间。

    我们来试着新增一个字段吧:

    1. rails g migration add_description_to_categories

    打开 db/migrate/20110411163049_add_description_to_categories.rb

    完成后,执行bin/rake db:migrate便会实际在数据库新增这个字段。

    数据库的字段定义

    为了能够让不同数据库通用,以下是Migration中的资料型态与实际数据库使用的型态对照:

    另外,字段也还有一些参数可以设定:

    • :null 是否允许NULL,默认是允许,即true
    • :default 默认值
    • :limit 用于stringtextintegerbinary指定最大值
    • :index => true 直接加上索引
    • :index => { :unique => true } 加上唯一索引
    • :foreign_key => true 加上外部键限制

    例如:

    1. create_table :events do |t|
    2. t.string :name, :null => false, :limit => 60, :default => "N/A"
    3. t.references :category # 等同于 t.integer :category_id
    4. end

    参考资料:

    我们已经介绍过了 timestamps 方法会自动新增两个时间字段,Rails 还保留了几个名称作为惯例之用:

    字段名称用途
    id默认的主键字段名称
    {tablename}_id默认的外部键字段名称
    created_at如果有这个字段,Rails便会在新增时设定时间
    updated_at如果有这个字段,Rails便会在修改时设定时间
    created_on如果有这个字段,Rails便会在新增时设定时间
    updated_on如果有这个字段,Rails便会在修改时设定时间
    {tablename}_count如果有使用 Counter Cache 功能,这是默认的字段名称
    type如果有这个字段,Rails便会启动STI功能(详见ActiveRecord章节)
    lock_version如果有这个字段,Rails便会启动Optimistic Locking功能(详见ActiveRecord章节)
    • rake db:create 依照目前的 RAILS_ENV 环境建立数据库
    • rake db:create:all 建立所有环境的数据库
    • rake db:drop 依照目前的 RAILS_ENV 环境删除数据库
    • rake db:drop:all 删除所有环境的数据库
    • rake db:migrate 执行Migration动作
    • rake db:rollback STEP=n 回复上N个 Migration 动作
    • rake db:migrate:up VERSION=20080906120000 执行特定版本的Migration
    • rake db:seed 执行 db/seeds.rb 加载种子资料
    • rake db:version 目前数据库的Migration版本
    • rake db:migrate:status 显示目前 migrations 执行的情况

    种子资料 Seed

    种子资料Seed的意思是,有一些资料是应用程式跑起来必要基本资料,而这些资料的产生我们会放在db/seeds.rb这个档案。例如,让我们打开来,加入一些基本的Category资料:

    1. # This file should contain all the record creation needed to seed the database with its default values.
    2. # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
    3. #
    4. # Examples:
    5. #
    6. # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
    7. # Mayor.create(name: 'Emanuel', city: cities.first)
    8. Category.create!( :name => "Science" )
    9. Category.create!( :name => "Art" )

    输入rake db:seed就会执行这个档案了。通常执行的时机是第一次建立好数据库和跑完Migration之后。

    资料 Migration

    不过,如果你在Migration中修改了资料表字段,随即又使用这个Model来做资料更新,那么因为Rails会快取资料表的字段定义,所以会无法读到刚刚修改的资料表。这时候有几个办法可以处理:

    第一是呼叫 reset_column_information 重新读取资料表定义。

    第二是在 Migration 中用 ActiveReocrd::Base 定义一个新的空白 Model 来暂时使用。

    第三是用 execute 功能来执行任意的 SQL。

    当有上万笔资料的时候,如果有修改数据库表格ALTER TABLE的话,他会Lock table无法写入,可能会跑好几个小时很难事前预估。建议用staging server用接近production的资料来先测试会跑多久。

    :bulk => true可以让变更数据库字段的Migration更有效率的执行,如果没有加这个参数,或是直接使用addcolumnrename_columnremove_column等方法,那么_Rails会拆开SQL来执行,例如:

    1. change_table(:users) do |t|
    2. t.string :company_name
    3. t.change :birthdate, :datetime
    4. end

    会产生:

    加上:bulk => true之后:

    1. change_table(:users, :bulk => true) do |t|
    2. t.string :company_name
    3. t.change :birthdate, :datetime
    4. end

    会合并产生一行SQL

    1. ALTER TABLE `users` ADD COLUMN `im_handle` varchar(255), ADD COLUMN `company_id` int(11), CHANGE `updated_at` `updated_at` datetime DEFAULT NULL

    这对已有不少资料量的数据库来说,会有不少执行速度上的差异,可以减少数据库因为修改被Lock锁定的时间。

    Schema档案的格式

    db/schema.rb这个档案是根据Migrations迁移最后的结果,自动产生出来的终极数据库纲要档案。这样如果要全新建立一个数据库,就不需要用Migrations一个一个从头跑到尾(一个年久的Rails专案也非常有可能没办法顺利跑完),可以用bundle exex rake db:schema:load这个指令直接加载纲要进空的数据库。

    另外,每次跑自动化测试的时候,为了节省建立数据库的时间,也会使用这个Schema纲要。这个纲要默认的格式是:ruby,也因此没办法表达出特定数据库所专属的功能,像是触发(triggers)或是预存程序(stored procedures)等等。所以如果你的 Migration 中有自定的 SQL 陈述句,需要把schema的格式设定成:sql。请修改config/application.rb加上

    1. # Use SQL instead of Active Record's schema dumper when creating the database.
    2. # This is necessary if your schema can't be completely dumped by the schema dumper,
    3. # like if you have constraints or database-specific column types
    4. # config.active_record.schema_format = :sql

    更多线上资源