by 雪隐 from https://juejin.cn/user/1433418895994094
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权
上一章讲了,怎么在NestJS
使用typeORM
,这一章接着上一章的内容,继续来讨论2个点,一个是数据迁移。另外一个是导入所有的表结构。
通过TypeORM
的forRoot/forRootAsync方法里面synchronize
参数设置为true
就能很轻松的把我们的表结构同步到数据库里面。没有老的博客项目,为什么需要数据迁移呢?
synchronize
设置为true
一般只在开发/测试环境中使用。如果在生产环境中使用,会导致数据的丢失。- 一个项目上线以后难免会发生一些需求的变更。当一些需求影响到数据库的时候。Migrations的迁移的用处就非常大了。
Migrations#
Migrations提供了一种增量更新数据库模式的方法,使其与应用程序的数据模型保持同步,同时保留数据库中的现有数据。为了生成、运行和恢复迁移,TypeORM提供了一个专用的CLI。
迁移类与Nest应用程序源代码是分开的。它们的生命周期由TypeORM CLI维护。因此,您无法在迁移中利用依赖项注入和其他Nest特定功能。要了解有关迁移的更多信息,请按照TypeORM文档中的指南进行操作。
一旦进入生产阶段,就需要将模型更改同步到数据库中。通常,在数据库中获得数据后,在生产上使用synchronize:true
进行模式同步是不安全的。这就是迁移的作用所在。
迁移只是一个带有sql查询的文件,用于更新数据库模式并将新的更改应用于现有数据库。
假设您已经有了一个数据库和一个post实体:
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm"
@Entity() export class Post {
@PrimaryGeneratedColumn() id: number
@Column() title: string
@Column() text: string
}
你的实体在生产中工作了几个月,没有任何变化。您的数据库中有数千条数据。
现在,您需要制作一个新版本,并将title重命名为name。你会怎么做?
您需要使用以下SQL查询(postgres方言)创建一个新的迁移:
ALTER TABLE "post" ALTER COLUMN "title" RENAME TO "name";
一旦您运行了这个SQL查询,您的数据库模式就可以使用新的代码库了。TypeORM提供了一个可以编写此类sql查询并在需要时运行它们的地方。这个地方被称为“migrations”。
创建新的迁移
在创建新迁移之前,您需要正确设置数据源选项:
{
type: "mysql",
host: "localhost",
port: 3306,
username: "test",
password: "test",
database: "test",
entities: [/*...*/],
migrations: [/*...*/],
migrationsTableName: "custom_migration_table",
}
在这里我们设置了两个选项:
“migrationsTableName”:“migrations”
-仅当您需要迁移表名称与“migration”不同时,才指定此选项。“migrations”:[/*…*/]
-需要由TypeORM加载的迁移列表
一旦设置了连接选项,就可以使用CLI创建新的迁移:
typeorm migration:create ./path-to-migrations-dir/PostRefactoring
这里,PostRefactoring
是迁移的名称,您可以指定任何您想要的名称。运行该命令后,您可以看到在名为{TIMESTAMP}-PostRefactoring.ts
的“migration”目录中生成的新文件,其中{TIMESTAMP}
是生成迁移时的当前时间戳。现在,您可以打开该文件并在其中添加迁移sql查询。
您应该在迁移中看到以下内容:
import { MigrationInterface, QueryRunner } from "typeorm"
export class PostRefactoringTIMESTAMP implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {}
async down(queryRunner: QueryRunner): Promise<void> {}
}
您必须使用迁移代码填充两种方法:向上up
和向下down
。up必须包含执行迁移所需的代码。无论上行发生什么变化,down
都必须恢复。down
方法用于恢复上一次迁移。
在向上和向下的内部都有一个QueryRunner
对象。所有数据库操作都是使用此对象执行的。了解有关查询运行器的更多信息。
让我们看看Post
更改后的迁移情况:
import { MigrationInterface, QueryRunner } from "typeorm"
export class PostRefactoringTIMESTAMP implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query( `ALTER TABLE "post" RENAME COLUMN "title" TO "name"`, )
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query( `ALTER TABLE "post" RENAME COLUMN "name" TO "title"`, ) // 恢复因up事件导致的变化 }
}
运行和恢复迁移
在生产环境中运行迁移,可以使用CLI命令运行迁移:
typeorm migration:run -- -d path-to-datasource-config
typeorm migration:create
和typeorm migrate:generate
将创建.ts
文件,除非您使用o
标志(请参阅生成迁移中的更多内容)。migration:run
和migration:revert
命令仅适用于.js
文件。因此,在运行命令之前需要编译typescript文件。或者,您可以将ts-node
与typeorm
结合使用来运行.ts
迁移文件。
ts-node
示例:
npx typeorm-ts-node-commonjs migration:run -- -d path-to-datasource-config
ESM项目中ts-node
的示例:
npx typeorm-ts-node-esm migration:run -- -d path-to-datasource-config
npx typeorm-ts-node-esm migration:generate ./src/migrations/update-post-table -d ./src/data-source.ts
此命令将执行所有挂起的迁移,并按时间戳顺序运行它们。这意味着所有在创建的迁移的up方法中编写的sql查询都将被执行。这就是全部!现在,您的数据库架构是最新的。
如果出于某种原因想要恢复更改,可以运行:
typeorm migration:revert -- -d path-to-datasource-config
此命令将在最近执行的迁移中down
执行。如果需要恢复多次迁移,则必须多次调用此命令。
伪造迁移和回滚
您也可以使用--fake
标志(简写-f
)伪造运行迁移。这将在不运行迁移的情况下将迁移添加到迁移表中。这对于在对数据库进行手动更改后创建的迁移或在外部运行迁移(例如通过其他工具或应用程序)时创建的迁移非常有用,并且您仍然希望保持一致的迁移历史记录。
typeorm migration:run --fake
这在回滚时也是可能的。
typeorm migration:revert --fake
Transaction模式
默认情况下,TypeORM将在单个包装事务中运行所有迁移。这对应于--transaction all
标志。如果需要更细粒度的事务控制,可以使用--transaction each
标志单独包装每个迁移,或者使用--transaction none
标志选择不将迁移全部包装在事务中。
除了这些标志之外,您还可以通过将MigrationInterface
上的事务transaction
属性设置为true
或false
来覆盖每次迁移的事务行为。这只适用于每个each
事务或无none
事务模式。
import { MigrationInterface, QueryRunner } from "typeorm"
export class AddIndexTIMESTAMP implements MigrationInterface {
transaction = false
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE INDEX CONCURRENTLY post_names_idx ON post(name)` )
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX CONCURRENTLY post_names_idx`, )
}
}
生成迁移
TypeORM能够根据您所做的架构更改自动生成迁移文件。
假设您有一个带有标题列的Post
实体,并且您已经将名称title
更改为name
。您可以运行以下命令:
typeorm migration:generate PostRefactoring -d path-to-datasource-config
如果您遇到任何错误,它要求您有迁移名称和数据源的路径。你可以试试这个选项
typeorm migration:generate -d <path/to/datasource> path/to/migrations/<migration-name>
它将生成一个名为{TIMESTAMP}-PostRefactoring.ts的新迁移,其内容如下:
import { MigrationInterface, QueryRunner } from "typeorm"
export class PostRefactoringTIMESTAMP implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "post" ALTER COLUMN "title" RENAME TO "name"`,
)
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "post" ALTER COLUMN "name" RENAME TO "title"`,
)
}
}
或者,您也可以使用o
(--outputJs
的别名)标志将迁移输出为Javascript文件。这对于未安装TypeScript附加包的仅限Javascript的项目非常有用。此命令将生成一个新的迁移文件{TIMESTAMP}-PostRefactoring.js
,其中包含以下内容:
const { MigrationInterface, QueryRunner } = require("typeorm")
module.exports = class PostRefactoringTIMESTAMP {
async up(queryRunner) {
await queryRunner.query(
`ALTER TABLE "post" ALTER COLUMN "title" RENAME TO "name"`,
)
}
async down(queryRunner) {
await queryRunner.query(
`ALTER TABLE "post" ALTER COLUMN "name" RENAME TO "title"`,
)
}
}
看,您不需要自己编写查询。生成迁移的经验法则是在对模型进行每次更改后生成迁移。要将多行格式应用于生成的迁移查询,请使用p
(--pretty
的别名)标志。
DateSource选项
如果需要运行/revert/generate/show迁移,请使用-d
(--dataSource
的别名)并将dataSource实例定义为参数的文件的路径传递给
typeorm -d <your-data-source-path> migration:{run|revert}
时间戳选项
如果需要为迁移名称指定时间戳,请使用-t
(--timestamp
的别名)并传递时间戳(应该是非负数)
typeorm -t <specific-timestamp> migration:{create|generate}
您可以从以下位置获取时间戳:
Date.now() /* OR */ new Date().getTime()
使用迁移API编写迁移
为了使用API来更改数据库架构,您可以使用QueryRunner
。详细的QueryRunner的各种方法,这里就不多做介绍了。大家自己参照官网学习。
例子:
import {
MigrationInterface,
QueryRunner,
Table,
TableIndex,
TableColumn,
TableForeignKey,
} from 'typeorm';
export class QuestionRefactoringTIMESTAMP implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'question',
columns: [
{ name: 'id', type: 'int', isPrimary: true },
{ name: 'name', type: 'varchar' },
],
}),
true,
);
await queryRunner.createIndex(
'question',
new TableIndex({ name: 'IDX_QUESTION_NAME', columnNames: ['name'] }),
);
await queryRunner.createTable(
new Table({
name: 'answer',
columns: [
{ name: 'id', type: 'int', isPrimary: true },
{ name: 'name', type: 'varchar' },
{ name: 'created_at', type: 'timestamp', default: 'now()' },
],
}),
true,
);
await queryRunner.addColumn(
'answer',
new TableColumn({ name: 'questionId', type: 'int' }),
);
await queryRunner.createForeignKey(
'answer',
new TableForeignKey({
columnNames: ['questionId'],
referencedColumnNames: ['id'],
referencedTableName: 'question',
onDelete: 'CASCADE',
}),
);
}
async down(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable('answer');
const foreignKey = table.foreignKeys.find(
(fk) => fk.columnNames.indexOf('questionId') !== -1,
);
await queryRunner.dropForeignKey('answer', foreignKey);
await queryRunner.dropColumn('answer', 'questionId');
await queryRunner.dropTable('answer');
await queryRunner.dropIndex('question', 'IDX_QUESTION_NAME');
await queryRunner.dropTable('question');
}
}
在项目中配置Migrations
打开package.json
在scripts
中加入下面的命令:
"scripts": {
"typeorm": "typeorm-ts-node-commonjs -d ./src/common/database/ormconfig.ts",
"migration:generate": "f() { npm run typeorm migration:generate -p \"./src/migrations/$@\"; }; f",
"migration:create": "typeorm-ts-node-commonjs migration:create",
"migration:run": "npm run typeorm migration:run",
"migration:revert": "npm run typeorm migration:revert",
},
这里所有的命令的用法在上文都有提到过,就不多做解释了。
打开/src/common/database/ormconfig.ts
文件,编写配置内容。参照本章创建新的迁移
的内容,写出DataSource
并且export
出来。因为migrations
迁移类与Nest应用程序源代码是分开的。它们的生命周期由TypeORM CLI维护,所以无法取到ConfigService对象,因此直接调用config/configuration
里的getConfig
方法。
import { DataSource, DataSourceOptions } from 'typeorm';
import { getConfig } from '../../config/configuration';
export function buildConnectionOptionsForMigrations() {
const { MYSQL_CONFIG: config } = getConfig();
const logFlag = config['LOG_ON'] === 'true';
const entitiesDir =
process.env.NODE_ENV === 'test'
? [__dirname + `/**/*.${config.type}.entity.ts`]
: [__dirname + `/**/*.${config.type}.entity{.js,.ts}`];
return {
...config,
entities: entitiesDir,
logging: logFlag,
} as TypeOrmModuleOptions;
}
export const connectionParams = buildConnectionOptionsForMigrations();
export default new DataSource({
...connectionParams,
migrations: ['src/common/database/migrations/**'],
subscribers: [],
} as DataSourceOptions);
试运行
- create
npm run migration:create ./src/common/database/migrations/test1
# 运行结果
#> day08@0.0.1 migration:create
#> typeorm-ts-node-commonjs migration:create ./src/common/database/migrations/test1
#Migration /Users/mac/Documents/workspace/nodejs/juejin-shenblog/day09/src/common/database/migrations/1682303306379-test1.ts has been generated successfully.
生成的1682303306379-test1.ts
文件
import { MigrationInterface, QueryRunner } from "typeorm"
export class Test11682303306379 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}
- run 修改刚刚生成的文件,创建一张表试试。
import { MigrationInterface, QueryRunner } from 'typeorm';
export class Test11682303306379 implements MigrationInterface {
name = 'Test11682303306379';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS \`article\`;`);
await queryRunner.query(
`CREATE TABLE \`article\` (\`id\` int(11) NOT NULL AUTO_INCREMENT,\`title\` varchar(100) DEFAULT NULL,\`body\` longtext,\`tag\` varchar(50) DEFAULT NULL COMMENT '每条记录的标签',\`category\` varchar(40) DEFAULT NULL,\`created_at\` date DEFAULT NULL,\`updated_at\` date DEFAULT NULL,\`status\` tinyint(1) DEFAULT '1' COMMENT '1表示正常 0表示删除 ',\`type\` tinyint(3) DEFAULT '1' COMMENT '1:原创; 0:转载',\`views\` int(11) DEFAULT '0',\`markdown\` tinyint(1) DEFAULT NULL,PRIMARY KEY (\`id\`)) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8mb4;`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DROP TABLE IF EXISTS \`article\`;`);
}
}
然后运行下面的命令,可以看到表正常被创建就没问题了
npm run migration:run
并且在数据库里面会创建一个migrations
表,并且添加了一条记录
id | timestamp | name |
---|---|---|
1 | 1682303306379 | Test11682303306379 |
- revert
npm run migration:revert
查看数据库可以看到migrations
表的数据被清空,并且,article
表也不存在了。其他的都不多做介绍了。
导入表
在migrations
文件夹中放了一个init.sql
文件,大家可以试着把里面的表都导入到数据库,并且自己写实体类entities
。重复的工作我就不细说了。如果您觉得这篇文章对您有帮助别忘了点赞/评论。谢谢大家🙏!