10. Migrations 数据迁移

优质
小牛编辑
140浏览
2023-12-01

Sequelize v2.0中引入了一个新的CLI(命令行工具),就像使用 Git/SVN 管理代码一样,你可以使用迁移功能(Migrations)来跟踪数据库的更改。通过迁移功能,你可以将现有数据库转移到另一个状态,反之亦然。进行迁移时,状态转换会被保存到迁移文件中,这些文件描述了如何进入新状态以及如何恢复更改以恢复到旧状态。

  1. CLI
    • 1.1 CLI安装
    • 1.2 引导
    • 1.3 创建第一个模型(并迁移)
    • 1.4 执行迁移
    • 1.5 撤消迁移
    • 1.6 创建第一个种子
    • 1.7 执行种子
    • 1.8 撤消种子
    • 1.9 CLI使用帮助
  2. 高级主题
    • 2.1 迁移结构
    • 2.2 .sequelizerc文件
    • 2.3 动态配置
    • 2.4 使用环境变量
    • 2.5 指定方言选项
    • 2.6 生产用途
    • 2.7 存储
    • 2.8 配置连接字符串
    • 2.9 使用SSL连接
    • 2.10 程序化使用
  3. queryInterface对象及功能

迁移需要使用Sequelize CLI,CLI提供了对迁移和项目引导的支持。

1. CLI

1.1 CLI安装

要使用CLI首先要安装相应的包:

$ npm install --save sequelize-cli

当通过-g参数全局安装npm包时,可以在命令行中通过sequelize [command]命令来使用CLI。而没有使全局安装时,就可以通过node_modules/.bin/sequelize [command]来使用CLI。

1.2 引导

在上一步中,我们创建了一个空项目,这里需要执行init命令:

$ node_modules/.bin/sequelize init

以上命令会创建以下文件夹:

  • config:包含配置文件,这些文件会告诉CLI怎样连接数据库
  • models:包含项目中的所有模型(Model)
  • migrations:包含所有迁移文件
  • seeders:包含所有种子文件

注意:以上命令使用到sequelize模块,如果你项目中没有请使用npm命令安装;另外,还需要安装对应的方言模块,以实现底层的数据库操作,如:使用MySQL数据库时就需要安装mysql2模块。

配置

继续后续操作前,我们需要使CLI能够连接到数据库,可以在config/config.json文件中配置数据库。该文件类似如下:

{
  "development": {
    "username": "root",
    "password": null,
    "database": "database_development",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "test": {
    "username": "root",
    "password": null,
    "database": "database_test",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "username": "root",
    "password": null,
    "database": "database_production",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}

编辑该文件,正确设置数据库的验证信息和方言(数据库类型)。

注意:如果你配置的数据库还不存在,调用db:create命令即可自动创建。


1.3 创建第一个模型(并迁移)

正确配置CLI的配置文件后,就可以创建你的第一个迁移。可以通过一个命令来轻松的完成这一操作。

创建模型使用model:generate命令,该命令包含以下两个参数:

  • name-模型名
  • attributes-模型属性列表

接下来我们来创建一个名为User的模型:

node_modules/.bin/sequelize model:generate --name User --attributes firstName:string,lastName:string,email:string

以上命令会做以下两件事:

  • models文件夹下创建模型文件user
  • migrations文件夹下创建名称类似XXXXXXXXXXXXXX-create-user.js的迁移文件

注意:Sequelize只会使用模型文件,模型文件是对数据库中表的表示;迁移文件是对该模型的更改,或者说是CLI所要使用的表;而“迁移”可以认为对数据库修改的一次提交或日志。


1.4 执行迁移

到目前为目,我们尚未在数据库中插入任何内容。刚刚我们为第一个模型User创建了所需的模型和迁移文件。

现在要在数据库中创建该表,这时需要执行db:migrate命令:

$ node_modules/.bin/sequelize db:migrate

以上操作会执行以下步骤:

  • 确保数据库中有一个名为SequelizeMeta的表,此表用于记录在当前数据库上运行的迁移
  • 查找尚示执行的迁移文件,这一步通过SequelizeMeta表来实现。在本例中,将执行上一步中创建的XXXXXXXXXXXXXX-create-user.js迁移文件。
  • 创建一个名为Users的表,其中包含迁移文件中指定的所有列。


1.5 撤消迁移

现在我们的表已在数据库中创建并保存。通过迁移功能,只需运行命令即可恢复到旧状态。

撤消迁移可以使用db:migrate:undo命令,此命令将还原到最近的迁移:

$ node_modules/.bin/sequelize db:migrate:undo

此外,可以通过db:migrate:undo:all命令来撤消所有迁移来恢复到初始状态。还可以通过--to选项来传递迁移文件名称,以恢复到特定迁移。

$ node_modules/.bin/sequelize db:migrate:undo:all --to XXXXXXXXXXXXXX-create-posts.js


1.6 创建第一个种子

有些情况下我们可能需要在一个表中插入一些默认数据。如:在前面的User表中创建一个演示用户。

要管理所有数据迁移,可以使用seeders。种子文件表示数据的一些变化,可用于使用样本数据或测试数据填充数据库表。

创建种子文件可以使用seed:generate命令。现在我们创建一个种子文件,它会添加一个演示用户到User表中:

$ node_modules/.bin/sequelize seed:generate --name demo-user

这个命令会在seeders文件夹中创建一个种子文件,文件名类似XXXXXXXXXXXXXX-demo-user.js。它遵循与迁移文件相同的up/down语义。

现在编辑这个文件,以添加一个演示用户到User表:

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('Users', [{
        firstName: 'John',
        lastName: 'Doe',
        email: 'demo@demo.com'
      }], {});
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('Users', null, {});
  }
};


1.7 执行种子

上一步我们创建了种子文件,但它还未提交到数据库中。这时可以通过db:seed:all命令实现:

$ node_modules/.bin/sequelize db:seed:all

以上命令会执行种子文件,演示用户也会被添加到User表中。

注意:种子执行不像迁移,它不会存储在任何位置(迁移会存储在SequelizeMeta中),如果需要存储请参阅Storage一节。


1.8 撤消种子

使用种子存储后,种子的执行可以撤消。可以通过以下两个命令实现:

db:seed:undo命令撤消最近的一次种子操作:

node_modules/.bin/sequelize db:seed:undo

db:seed:undo:all命令撤消所有种子操作:

node_modules/.bin/sequelize db:seed:undo:all


1.9 CLI使用帮助

可以通过sequelize help查看使用帮助,该命令会输出当前运行环境中的Node版本、CLI版本、ORM版本及当前版本CLI所支持的命令。

类似如下:

Sequelize CLI [Node: 10.14.2, CLI: 5.4.0, ORM: 4.42.0]

sequelize [命令]

命令:
  sequelize db:migrate                        运行待执行的迁移
  sequelize db:migrate:schema:timestamps:add  更新迁移表以获取时间戳
  sequelize db:migrate:status                 列出所有迁移的状态
  sequelize db:migrate:undo                   恢复迁移
  sequelize db:migrate:undo:all               恢复所有迁移
  sequelize db:seed                           运行指定的种子
  sequelize db:seed:undo                      撤消最近执行的种子
  sequelize db:seed:all                       运行所有种子
  sequelize db:seed:undo:all                  撤消所有已执行的种子
  sequelize db:create                         创建配置中指定的数据库
  sequelize db:drop                           删除配置中指定的数据库
  sequelize init                              初始化项目
  sequelize init:config                       初始化配置
  sequelize init:migrations                   初始化迁移
  sequelize init:models                       初始化模型
  sequelize init:seeders                      初始化种子
  sequelize migration:generate                生成新的迁移文件                         [aliases: migration:create]
  sequelize model:generate                    生成一个模模型及期迁移文件                 [aliases: model:create]
  sequelize seed:generate                     生成一个新的种子文件                      [aliases: seed:create]

选项:
  --help     显示帮助信息                                                              [布尔]
  --version  显示版本号                                                                [布尔]

2. 高级主题

2.1 迁移结构

所有迁移都在项目顶层一个名为migrations的目录中。

sequelize会生成迁移的结构,以下是一个典型的迁移文件结构:

module.exports = {
  up: function(queryInterface, Sequelize) {
    // 转换为新状态的逻辑
  },
 
  down: function(queryInterface, Sequelize) {
    // 恢复修改的逻辑
  }
}

通过queryInterface对象可用来修改数据库。Sequelize对象中存储了数据类型,如:STRINGINTEGERupdown函数需要返回一个Promise。以下是一些示例代码:

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('Person', {
        name: Sequelize.STRING,
        isBetaMember: {
          type: Sequelize.BOOLEAN,
          defaultValue: false,
          allowNull: false
        }
      });
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('Person');
  }
}

可以通过queryInterface了节查看该对象所支持的方法。

2.2 .sequelizerc文件

.sequelizerc文件一个特殊的配置文件,它允许你指定通常作为参数传递给CLI的各种选项。可以使用它的一些场景:

  • 需要重写默认的migrations, models, seedersconfig文件夹。
  • 需要重命名config.json时。如:重命名为database.json或其它。

除以上说明外,还有更多的应用场景。接下来,我们来看看如何使用这个文件来进行自定义配置。

首先,在项目根目录下创建.sequelizerc文件:

$ touch .sequelizerc

以下是一个示例配置:

const path = require('path');

module.exports = {
  'config': path.resolve('config', 'database.json'),
  'models-path': path.resolve('db', 'models'),
  'seeders-path': path.resolve('db', 'seeders'),
  'migrations-path': path.resolve('db', 'migrations')
}

在这个配置中我们告诉CLI:

  • 使用config/database.json进行配置设置
  • 使用db/models做为模型目录
  • 使用db/seeders做为种子目录
  • 使用db/migrations做为迁移目录

2.3 动态配置

配置文件默认是一个名为config.json的JSON文件,但有时你想执行一些代码或访问环境变量,这些操作在JSON文件中是不可能实现的。

Sequelize CLI可以从JSONJS文件中读取配置,这可以通过.sequelizerc文件配置。

要使用JS格式的文件做为配置文件,可以在.sequelizerc文件中配置如下:

const path = require('path');

module.exports = {
  'config': path.resolve('config', 'config.js')
}

现在Sequelize CLI会加载config/config.js文件获取配置选项,这样通过这个JS文件可以执行任何代码并导出最终的动态配置文件。示例如下:

const fs = require('fs');

module.exports = {
  development: {
    username: 'database_dev',
    password: 'database_dev',
    database: 'database_dev',
    host: '127.0.0.1',
    dialect: 'mysql'
  },
  test: {
    username: 'database_test',
    password: null,
    database: 'database_test',
    host: '127.0.0.1',
    dialect: 'mysql'
  },
  production: {
    username: process.env.DB_USERNAME,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    host: process.env.DB_HOSTNAME,
    dialect: 'mysql',
    dialectOptions: {
      ssl: {
        ca: fs.readFileSync(__dirname + '/mysql-ca-master.crt')
      }
    }
  }
};

2.4 使用环境变量

使用CLI,可以直接访问config/config.js中的环境变量。

如下所示,可以在该文件中直接使用环境变量并导出:

module.exports = {
  development: {
    username: 'database_dev',
    password: 'database_dev',
    database: 'database_dev',
    host: '127.0.0.1',
    dialect: 'mysql'
  },
  test: {
    username: process.env.CI_DB_USERNAME,
    password: process.env.CI_DB_PASSWORD,
    database: process.env.CI_DB_NAME,
    host: '127.0.0.1',
    dialect: 'mysql'
  },
  production: {
    username: process.env.PROD_DB_USERNAME,
    password: process.env.PROD_DB_PASSWORD,
    database: process.env.PROD_DB_NAME,
    host: process.env.PROD_DB_HOSTNAME,
    dialect: 'mysql'
  }

2.5 指定方言选项

有时你需要指定dialectOption选项,一般你可以在config/config.json中添加它。而当需要使用代码来获取该选项时,可以使用动态配置文件来处理。

{
  "production": {
    "dialect":"mysql",
    "dialectOptions": {
      "bigNumberStrings": true
    }
  }
}

备注:Sequelize中方言(dialect)选项用于指定所使用的数据库型。

2.6 生产用途

以下是一些有关在生产环境中使用CLI和迁移设置的提示。

1)使用环境变量进行配置设置,使用动态配置可以更好地实现。以下是一个在生产环境中使用安全配置的设置示例:

const fs = require('fs');

module.exports = {
  development: {
    username: 'database_dev',
    password: 'database_dev',
    database: 'database_dev',
    host: '127.0.0.1',
    dialect: 'mysql'
  },
  test: {
    username: 'database_test',
    password: null,
    database: 'database_test',
    host: '127.0.0.1',
    dialect: 'mysql'
  },
  production: {
    username: process.env.DB_USERNAME,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    host: process.env.DB_HOSTNAME,
    dialect: 'mysql',
    dialectOptions: {
      ssl: {
        ca: fs.readFileSync(__dirname + '/mysql-ca-master.crt')
      }
    }
  }
};

2.7 存储

CLI支持三种存储方式:sequelize,json,none

  • sequelize : 在sequelize数据库的表中存储迁移和种子
  • json : 在json文件中存储迁移和种子
  • none : 不存储任何迁移、种子

迁移存储

默认情况下,CLI会在数据库中创建一个名为SequelizeMeta的表,其中包含每个已执行迁移的条目。要更改此行为,可以将三个选项添加到配置文件中。使用,您可以选择。 如果选择json,则可以使用migrationStoragePath指定文件的路径,或者CLI将写入文件sequelize-meta.json。 如果要使用sequelize将信息保留在数据库中,但希望使用其他表,则可以使用migrationStorageTableName更改表名。

  • migrationStorage - 用于指定要用于存储迁移的类型,默认为:sequelize,如果使用json则需要指定migrationStoragePath
  • json - 使用json做为迁移存储时,指定JSON文件的路径。默认为:sequelize-meta.json
  • migrationStorageTableName - 使用sequelize做为迁移存储时,用于指定存储表名。默认为:migrationStorageTableName
{
  "development": {
    "username": "root",
    "password": null,
    "database": "database_development",
    "host": "127.0.0.1",
    "dialect": "mysql",

    // 指定存储类型,默认: sequelize
    "migrationStorage": "json",

    // 指定存储文件名,默认: sequelize-meta.json
    "migrationStoragePath": "sequelizeMeta.json",

    // 指定存储表名,默认: SequelizeMeta
    "migrationStorageTableName": "sequelize_meta"
  }
}

备注:不建议使用none用作迁移存储。如果决定使用,应该充分考虑未记录迁移执行或未运行的影响。

种子存储

默认情况下,CLI不存储种子的执行记录。如果需要存储,可以使用seederStorage选项来配置,种子存储的各设置选项与迁移存储类似。当使用json存储时,可以使用seederStoragePath来指定路径,或者CLI使用默认的sequelize-data.json;如果需要存储在数据库中,则可以使用sequelize选项,这时可以通过seederStorageTableName来指定表名,默认将使用SequelizeData

{
  "development": {
    "username": "root",
    "password": null,
    "database": "database_development",
    "host": "127.0.0.1",
    "dialect": "mysql",
    // Use a different storage. Default: none
    "seederStorage": "json",
    // Use a different file name. Default: sequelize-data.json
    "seederStoragePath": "sequelizeData.json",
    // Use a different table name. Default: SequelizeData
    "seederStorageTableName": "sequelize_data"
  }
}

2.8 配置连接字符串

可以使用--config选项来替代数据库配置文件,可以使用--url选项传入连接字符串。例如:

$ node_modules/.bin/sequelize db:migrate --url 'mysql://root:password@mysql_host.com/database_name'

2.9 使用SSL连接

使用SSL连接时,应确保在dialectOptions和基本配置中都指定了ssl

{
  "production": {
    "dialect":"postgres",
    "ssl": true,
    "dialectOptions": {
      "ssl": true
    }
  }
}

2.10 程序化使用

Sequelize有一个姐妹库:umzug,可以用编程方式处理迁移任务的执行和记录。

3. queryInterface对象及功能

QueryInterfaceSequelize的内置类之一,可以通过Sequelize对象(Sequelize实例)的getQueryInterface()方法获取该类的实例(即:queryInterface对象):

var queryInterface = sequelize.getQueryInterface();

通过queryInterface对象中的方法可以对数据进行修改,其中一些方法可以修改数据库的schema,以下是该类中的部分方法说明:

createTable(tableName, attributes, options)

通过传入的表名tableName、属性attributes及其它选项options创建新表。

createTable()方法用于在数据库中创建新表。创建表时可以通过attributes属性定义一些简单或复杂的属性;而options参数,可以用于定义表所使用的编码及引擎等。

queryInterface.createTable(
  'nameOfTheNewTable',
  {
    id: {
      type: Sequelize.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    createdAt: {
      type: Sequelize.DATE
    },
    updatedAt: {
      type: Sequelize.DATE
    },
    attr1: Sequelize.STRING,
    attr2: Sequelize.INTEGER,
    attr3: {
      type: Sequelize.BOOLEAN,
      defaultValue: false,
      allowNull: false
    },
    //foreign key usage
    attr4: {
        type: Sequelize.INTEGER,
        references: {
            model: 'another_table_name',
            key: 'id'
        },
        onUpdate: 'cascade',
        onDelete: 'cascade'
    }
  },
  {
    engine: 'MYISAM', // default: 'InnoDB'
    charset: 'latin1' // default: null
  }
)

dropTable(tableName, options)

删除已存在的表

queryInterface.dropTable('nameOfTheExistingTable')

dropAllTables(options)

删除数据库中所有已存在的表

queryInterface.dropAllTables()

renameTable(before, after, options)

重命名已存在的表

queryInterface.renameTable('Person', 'User')
showAllTables(options)

返回数据库中所有已存在的表的表名

queryInterface.showAllTables().then(function(tableNames) {})

describeTable(tableName, options)

返回表描述,即:返回的一个包含所有属性信息的哈希表

queryInterface.describeTable('Person').then(function(attributes) {
  /*
    attributes 结构类似如下:
 
    {
      name: {
        type:         'VARCHAR(255)', // this will be 'CHARACTER VARYING' for pg!
        allowNull:    true,
        defaultValue: null
      },
      isBetaMember: {
        type:         'TINYINT(1)', // this will be 'BOOLEAN' for pg!
        allowNull:    false,
        defaultValue: false
      }
    }
  */
})

addColumn(tableName, attributeName, dataTypeOrOptions, options)

向已存在的表中添加新列,数据类型可以简单或详细定义

queryInterface.addColumn(
  'nameOfAnExistingTable',
  'nameOfTheNewAttribute',
  Sequelize.STRING
)

// or

queryInterface.addColumn(
  'nameOfAnExistingTable',
  'nameOfTheNewAttribute',
  {
    type: Sequelize.STRING,
    allowNull: false
  }
)

// or with an explicit schema:

queryInterface.addColumn({
    tableName: 'Person',
    schema: 'public'
  },
  'signature',
  Sequelize.STRING
)

removeColumn(tableName, attributeName, options)

移除已存在的表中指定的列

queryInterface.removeColumn('Person', 'signature')

// or with an explicit schema:

queryInterface.removeColumn({
  tableName: 'Person',
  schema: 'public'
}, 'signature');

changeColumn(tableName, attributeName, dataTypeOrOptions, options)

修改属性的元数据。可以修改默认值、是否允许空或数据类型,修改时应该确保完全描述了新的数据类型。

queryInterface.changeColumn(
  'nameOfAnExistingTable',
  'nameOfAnExistingAttribute',
  {
    type: Sequelize.FLOAT,
    allowNull: false,
    defaultValue: 0.0
  }
)

renameColumn(tableName, attrNameBefore, attrNameAfter, options)

重命令属性(修改列名)

queryInterface.renameColumn('Person', 'signature', 'sig')

addIndex(tableName, attributes, options)

通过指定的属性向表中索引。当没有传入options时,会自动创建索引名。

// This example will create the index person_firstname_lastname
queryInterface.addIndex('Person', ['firstname', 'lastname'])

// This example will create a unique index with the name SuperDuperIndex using the optional 'options' field.
// Possible options:
// - indicesType: UNIQUE|FULLTEXT|SPATIAL
// - indexName: The name of the index. Default is __
// - parser: For FULLTEXT columns set your parser
// - indexType: Set a type for the index, e.g. BTREE. See the documentation of the used dialect
// - logging: A function that receives the sql query, e.g. console.log
// - where: A hash of attributes to limit your index(Filtered Indexes - MSSQL & PostgreSQL only)
queryInterface.addIndex(
  'Person',
  ['firstname', 'lastname'],
  {
    indexName: 'SuperDuperIndex',
    indicesType: 'UNIQUE'
  }
)

queryInterface.addIndex(
  'Person',
  ['firstname', 'lastname'],
  {
    where: {
      lastname: {
        $ne: null
      }
    }
  }
)

removeIndex(tableName, indexNameOrAttributes, options)

移除表中已存在的索引

queryInterface.removeIndex('Person', 'SuperDuperIndex')

// or

queryInterface.removeIndex('Person', ['firstname', 'lastname'])

addConstraint(tableName, attributes, options)

V4.0.0+

添加一个新约束。

  • tableName - 要添加约束的表的表名
  • attributes - 应用约束的列名数组
  • options - 一个包含约束定义的对象,如:约束名、类型等

options中可用的选项有:

  • type - 约束类型。可用的约束类型有:
    • UNIQUE
    • DEFAULT (MSSQL only)
    • CHECK (MySQL - Ignored by the database engine )
    • FOREIGN KEY
    • PRIMARY KEY
  • name - 约束名。如果不指定,sequelize将根据约束类型、表名、列名自动命名
  • defaultValue - 约束默认值
  • where - 约束检查条件
  • references - Object。指定目标表、列名以创建外键约束
  • references.table - 目标表名或表
  • references.field - 目录列名 Available constraints:
//UNIQUE
queryInterface.addConstraint('Users', ['email'], {
  type: 'unique',
  name: 'custom_unique_constraint_name'
});

//CHECK
queryInterface.addConstraint('Users', ['roles'], {
  type: 'check',
  where: {
    roles: ['user', 'admin', 'moderator', 'guest']
  }
});

//Default - MSSQL only
queryInterface.addConstraint('Users', ['roles'], {
  type: 'default',
  defaultValue: 'guest'
});

//Primary Key
queryInterface.addConstraint('Users', ['username'], {
  type: 'primary key',
  name: 'custom_primary_constraint_name'
});

//Foreign Key
queryInterface.addConstraint('Posts', ['username'], {
  type: 'FOREIGN KEY',
  references: { //Required field
    table: 'target_table_name',
    field: 'target_column_name'
  },
  onDelete: 'cascade',
  onUpdate: 'cascade'
});

removeConstraint(tableName, constraintName, options)

V4.0.0+

移除表中已经存在的约束

queryInterface.removeConstraint('Users', 'my_constraint_name');

showConstraint(tableName, options)

返回表中已经存在的约束列表

V4.0.0+

queryInterface.showConstraint('Users');
// Returns array of objects/constraints

变更记录

  • [2017-06-10] 基于v4.1.0首次发布
  • [2019-01-21] 基于v4.42.0更新