TypeORM 如何播种数据库
TypeORM how to seed database
我是 运行 我的 Node JS 后端使用 typeorm ORM。
来自 Entity Framework,用
等几行代码很容易为数据库设定种子
Database.SetInitializer(new DbInitializer());
其中 DbInitializer class 将包含所有播种信息。
是否有类似的方法在 TypeOrm 中播种数据库?
如果没有,推荐的方法是什么?
1) 使用数据插入语句创建新迁移?
2) 创建一个实例化和保存实体的任务?
我也很想看到这样的功能(而且we're not alone), but at the moment,官方没有播种功能。
由于缺少这样的内置功能,我认为下一个最好的办法是创建一个名为 0-Seed
的迁移脚本(因此它位于您可能拥有的任何其他迁移脚本之前)并拥有种子那里填充了数据。
@bitwit has created a snippet 可能对你有用;它是一个从 yaml 文件中读取数据的函数,您可以将其合并到种子迁移脚本中。
然而,经过一些研究,我发现了另一种有趣的方法:将 after_create
事件绑定到 table,并在监听器中绑定 initialize the data。
我还没有实现这个,所以我不确定它是否可以直接用 TypeORM 完成。
不幸的是,TypeORM(在发布此答案时)没有正式发布的解决方案。
但是我们可以使用一个很好的解决方法:
- 在
ormconfig.js
文件中创建另一个连接并指定另一个
"migrations" 的文件夹 - 实际上是我们的 seeds
- 使用
-c <connection name>
生成并 运行 你的种子。就是这样!
样本ormconfig.js:
module.exports = [
{
...,
migrations: [
'src/migrations/*.ts'
],
cli: {
migrationsDir: 'src/migrations',
}
},
{
name: 'seed',
...,
migrations: [
'src/seeds/*.ts'
],
cli: {
migrationsDir: 'src/seeds',
}
}
]
样本package.json:
{
...
scripts: {
"seed:generate": "ts-node typeorm migration:generate -c seed -n ",
"seed:run": "ts-node typeorm migration:run -c seed",
"seed:revert": "ts-node typeorm migration:revert -c seed",
},
...
}
对于那些将 TypeORM 与 Nest.js 一起使用的人,这里有一个解决方案可以从您的代码中以编程方式执行播种。
大致思路:
- 我们创建了一个专门的 "seeding module",其中包含一个 "seeding middleware",负责执行播种并确保在回答任何请求之前完成所有播种。
- 对于任何到达的请求,播种中间件都会拦截它并推迟它直到确认播种完成。
- 如果数据库已经播种,"seeding middleware" 将请求传递给下一个中间件。
- 为了加快速度,"seeding middleware" 在内存中保留一个 "seeding complete" 标志作为状态,以避免在播种发生后进行任何进一步的数据库检查。
实施:
为此,首先创建一个模块来注册一个侦听所有传入请求的中间件:
// file: src/seeding/SeedingModule.ts
@Module({})
export class SeedingModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(SeedingMiddleware)
.forRoutes('*')
}
}
现在创建中间件:
// file: src/seeding/SeedingMiddleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
import { EntityManager } from 'typeorm';
import { SeedingLogEntry } from './entities/SeedingLogEntry.entity';
@Injectable()
export class SeedingMiddleware implements NestMiddleware {
// to avoid roundtrips to db we store the info about whether
// the seeding has been completed as boolean flag in the middleware
// we use a promise to avoid concurrency cases. Concurrency cases may
// occur if other requests also trigger a seeding while it has already
// been started by the first request. The promise can be used by other
// requests to wait for the seeding to finish.
private isSeedingComplete: Promise<boolean>;
constructor(
private readonly entityManager: EntityManager,
) {}
async use(req: Request, res: Response, next: Function) {
if (await this.isSeedingComplete) {
// seeding has already taken place,
// we can short-circuit to the next middleware
return next();
}
this.isSeedingComplete = (async () => {
// for example you start with an initial seeding entry called 'initial-seeding'
// on 2019-06-27. if 'initial-seeding' already exists in db, then this
// part is skipped
if (!await this.entityManager.findOne(SeedingLogEntry, { id: 'initial-seeding' })) {
await this.entityManager.transaction(async transactionalEntityManager => {
await transactionalEntityManager.save(User, initialUsers);
await transactionalEntityManager.save(Role, initialRoles);
// persist in db that 'initial-seeding' is complete
await transactionalEntityManager.save(new SeedingLogEntry('initial-seeding'));
});
}
// now a month later on 2019-07-25 you add another seeding
// entry called 'another-seeding-round' since you want to initialize
// entities that you just created a month later
// since 'initial-seeding' already exists it is skipped but 'another-seeding-round'
// will be executed now.
if (!await this.entityManager.findOne(SeedingLogEntry, { id: 'another-seeding-round' })) {
await this.entityManager.transaction(async transactionalEntityManager => {
await transactionalEntityManager.save(MyNewEntity, initalSeedingForNewEntity);
// persist in db that 'another-seeding-round' is complete
await transactionalEntityManager.save(new SeedingLogEntry('another-seeding-round'));
});
}
return true;
})();
await this.isSeedingComplete;
next();
}
}
最后是我们用来在我们的数据库中记录发生了某种类型的播种的实体。确保在您的 TypeOrmModule.forRoot
调用中将其注册为实体。
// file: src/seeding/entities/Seeding.entity.ts
import { Entity, PrimaryColumn, CreateDateColumn } from 'typeorm';
@Entity()
export class Seeding {
@PrimaryColumn()
public id: string;
@CreateDateColumn()
creationDate: Date;
constructor(id?: string) {
this.id = id;
}
}
另一种使用生命周期事件的播种解决方案:
使用 Nest.js,您还可以实现 OnApplicationBootstrap
接口(参见 lifecycle events),而不是寻求基于中间件的解决方案来处理您的播种。 onApplicationBootstrap
方法将 "called once the application has fully started and is bootstrapped"。然而,与中间件解决方案相比,这种方法不允许您在多租户环境中播种数据库,在多租户环境中,将在运行时为不同租户创建数据库模式,并且需要在运行时多次进行播种创建后的不同租户。
看起来正在为此构建一个模块,typeorm-seeding。
虽然使用初始迁移进行播种也有效,但它对于测试可能需要新播种的数据库才能通过测试的地方不是很有用。一旦您开始创建更多迁移,您就无法无误地删除、同步和 运行 迁移。这可以通过对单个迁移文件 运行 migration:run
来解决,但目前不能使用 CLI。我的解决方案是一个轻量级脚本,它通过 typeorm 连接访问 QueryRunner 对象:
// testSeed.ts
import { ConnectionOptions, createConnection, QueryRunner } from "typeorm";
import { config } from "../config";
import { DevSeed } from "./DevSeed";
createConnection(config.typeOrmConfig as ConnectionOptions).then(async connection => {
let queryRunner = connection.createQueryRunner("master");
// runs all seed SQL commands in this function.
await DevSeed(queryRunner);
await queryRunner.release();
return connection.close();
});
然后运行node ./dist/path/to/testSeed.js
所以这就是我如何使用插入语句从 sql 文件播种数据。
这是添加种子后我的整个迁移文件
import { MigrationInterface, QueryRunner } from 'typeorm';
import * as path from 'path';
import * as fs from 'fs';
let insertPermissionQueries = fs
.readFileSync(path.resolve(__dirname, '../../scripts/sql/insert.sql'))
.toString()
.replace(/(\r\n|\n|\r)/gm, ' ') // remove newlines
.replace(/\s+/g, ' '); // excess white space
export class init1591103087130 implements MigrationInterface {
name = 'init1591103087130';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "public"."RoleTemp" ("idx" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" text, "created_on" TIMESTAMP DEFAULT now(), "is_active" boolean DEFAULT true, "role_type" text, "created_by" uuid NOT NULL, "status" text, "alias" text, "operation" text, "rejection_reason" text, "id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "role_id" integer, CONSTRAINT "UQ_835baad60041a3413f9ef95bc07" UNIQUE ("idx"), CONSTRAINT "PK_a76dd0012be252eefbdd4a2a589" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "RoleTemp_idx_key" ON "public"."RoleTemp" ("idx") `,
);
await queryRunner.query(
`CREATE TABLE "public"."PermissionRoleTemp" ("idx" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "permission_base_name" text, "id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "role_id" integer NOT NULL, "permission_id" integer NOT NULL, CONSTRAINT "PK_c1f2648a18ac911e096f08c187d" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "public"."Permission" ("idx" uuid NOT NULL DEFAULT uuid_generate_v4(), "base_name" text NOT NULL, "url" text NOT NULL, "method" text NOT NULL, "created_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "is_active" boolean NOT NULL DEFAULT true, "permission_type" text, "alias" text NOT NULL, "id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "PK_28657fa560adca66b359c18b952" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "public"."PermissionRole" ("idx" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "is_active" boolean NOT NULL DEFAULT true, "permission_base_name" text NOT NULL, "id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "role_id" integer NOT NULL, "permission_id" integer NOT NULL, CONSTRAINT "PK_b5e2271c229f65f17ee93677a0f" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "public"."UserRole" ("idx" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "is_active" boolean NOT NULL DEFAULT true, "id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "role_id" integer NOT NULL, "company_user_id" integer NOT NULL, CONSTRAINT "PK_431fc1ec3d46ac513ef3701604e" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "public"."UsersTemp" ("idx" uuid DEFAULT uuid_generate_v1(), "username" text, "first_name" text, "middle_name" text, "last_name" text, "password" text, "email" text, "address" text, "phone_number" text, "phone_ext" text, "company_idx" uuid, "is_superadmin" boolean NOT NULL DEFAULT false, "operation" text, "created_by" text, "status" text, "rejection_reason" text, "created_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "is_active" boolean NOT NULL DEFAULT true, "id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "role_id" integer, "user_id" integer, CONSTRAINT "PK_9d3fbcec3cc0b054324f93da038" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "public"."Role" ("idx" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" text NOT NULL, "alias" text NOT NULL, "created_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "is_active" boolean NOT NULL DEFAULT true, "role_type" text, "created_by" uuid NOT NULL, "id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "UQ_c9a53325a7642edb5f9bd44f5aa" UNIQUE ("idx"), CONSTRAINT "PK_422113329ddec949e76c7943c56" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "Role_idx_key" ON "public"."Role" ("idx") `,
);
await queryRunner.query(
`CREATE TABLE "public"."Users" ("idx" uuid NOT NULL DEFAULT uuid_generate_v4(), "username" text NOT NULL, "first_name" text NOT NULL, "middle_name" text, "last_name" text NOT NULL, "password" text NOT NULL, "email" text, "address" text, "phone_number" text, "phone_ext" text, "company_idx" uuid, "created_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "is_active" boolean NOT NULL DEFAULT true, "is_superadmin" boolean NOT NULL DEFAULT false, "id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "role_id" integer NOT NULL, CONSTRAINT "PK_ac3c96e3c912cbda773b7c7edc9" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "public"."CompanyUser" ("id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "idx" uuid NOT NULL DEFAULT uuid_generate_v4(), "company_idx" uuid, "created_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "is_active" boolean NOT NULL DEFAULT true, "user_id" integer, CONSTRAINT "PK_4a915d69bf079a8e5dd10784cc3" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "public"."RoleTemp" ADD CONSTRAINT "FK_d304588d17c9349ca6e7ebee5d3" FOREIGN KEY ("role_id") REFERENCES "public"."Role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."PermissionRoleTemp" ADD CONSTRAINT "FK_7e7cdde853500f56b3db43fc258" FOREIGN KEY ("role_id") REFERENCES "public"."RoleTemp"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."PermissionRoleTemp" ADD CONSTRAINT "FK_0068d3de1c59050561d35f17544" FOREIGN KEY ("permission_id") REFERENCES "public"."Permission"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."PermissionRole" ADD CONSTRAINT "FK_5b57492441a568bc7562fbbaa5b" FOREIGN KEY ("role_id") REFERENCES "public"."Role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."PermissionRole" ADD CONSTRAINT "FK_1951a810af06342fcd4530ec61c" FOREIGN KEY ("permission_id") REFERENCES "public"."Permission"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."UserRole" ADD CONSTRAINT "FK_fb09d73b0dd011be81a272e1efa" FOREIGN KEY ("role_id") REFERENCES "public"."Role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."UserRole" ADD CONSTRAINT "FK_b221977a41587e58d7c58e16db0" FOREIGN KEY ("company_user_id") REFERENCES "public"."CompanyUser"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."UsersTemp" ADD CONSTRAINT "FK_6d74dfaddaa94e1bba0c8c12a2f" FOREIGN KEY ("role_id") REFERENCES "public"."Role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."UsersTemp" ADD CONSTRAINT "FK_e5b2930fe35042dab17945bb131" FOREIGN KEY ("user_id") REFERENCES "public"."Users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."Users" ADD CONSTRAINT "FK_34be125e29cee0e71d58456aed7" FOREIGN KEY ("role_id") REFERENCES "public"."Role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."CompanyUser" ADD CONSTRAINT "FK_1354e3e408b5ffdebe476a6fbd2" FOREIGN KEY ("user_id") REFERENCES "public"."Users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(insertPermissionQueries);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "public"."CompanyUser" DROP CONSTRAINT "FK_1354e3e408b5ffdebe476a6fbd2"`,
);
await queryRunner.query(
`ALTER TABLE "public"."Users" DROP CONSTRAINT "FK_34be125e29cee0e71d58456aed7"`,
);
await queryRunner.query(
`ALTER TABLE "public"."UsersTemp" DROP CONSTRAINT "FK_e5b2930fe35042dab17945bb131"`,
);
await queryRunner.query(
`ALTER TABLE "public"."UsersTemp" DROP CONSTRAINT "FK_6d74dfaddaa94e1bba0c8c12a2f"`,
);
await queryRunner.query(
`ALTER TABLE "public"."UserRole" DROP CONSTRAINT "FK_b221977a41587e58d7c58e16db0"`,
);
await queryRunner.query(
`ALTER TABLE "public"."UserRole" DROP CONSTRAINT "FK_fb09d73b0dd011be81a272e1efa"`,
);
await queryRunner.query(
`ALTER TABLE "public"."PermissionRole" DROP CONSTRAINT "FK_1951a810af06342fcd4530ec61c"`,
);
await queryRunner.query(
`ALTER TABLE "public"."PermissionRole" DROP CONSTRAINT "FK_5b57492441a568bc7562fbbaa5b"`,
);
await queryRunner.query(
`ALTER TABLE "public"."PermissionRoleTemp" DROP CONSTRAINT "FK_0068d3de1c59050561d35f17544"`,
);
await queryRunner.query(
`ALTER TABLE "public"."PermissionRoleTemp" DROP CONSTRAINT "FK_7e7cdde853500f56b3db43fc258"`,
);
await queryRunner.query(
`ALTER TABLE "public"."RoleTemp" DROP CONSTRAINT "FK_d304588d17c9349ca6e7ebee5d3"`,
);
await queryRunner.query(`DROP TABLE "public"."CompanyUser"`);
await queryRunner.query(`DROP TABLE "public"."Users"`);
await queryRunner.query(`DROP INDEX "public"."Role_idx_key"`);
await queryRunner.query(`DROP TABLE "public"."Role"`);
await queryRunner.query(`DROP TABLE "public"."UsersTemp"`);
await queryRunner.query(`DROP TABLE "public"."UserRole"`);
await queryRunner.query(`DROP TABLE "public"."PermissionRole"`);
await queryRunner.query(`DROP TABLE "public"."Permission"`);
await queryRunner.query(`DROP TABLE "public"."PermissionRoleTemp"`);
await queryRunner.query(`DROP INDEX "public"."RoleTemp_idx_key"`);
await queryRunner.query(`DROP TABLE "public"."RoleTemp"`);
}
}
对于 NestJS,您还可以使用 nestjs-console
包来执行任务。这样您就可以访问实体、服务、存储库等。与@B12Toaster 提出的中间件解决方案相比,我更喜欢这种解决方案,因为您不需要将其作为生产代码进行维护。
如下图创建一个seed
命令,然后简单的:yarn console seed
.
这里有一个工作示例(在 CI 中运行):https://github.com/thisismydesign/nestjs-starter/tree/ee7abf6d481b1420708e87dea3cb99ca110cc168
按照这些思路:
src/console.ts
import { BootstrapConsole } from 'nestjs-console';
import { AppModule } from 'src/server/app/app.module';
const bootstrap = new BootstrapConsole({
module: AppModule,
useDecorators: true,
});
bootstrap.init().then(async (app) => {
try {
await app.init();
await bootstrap.boot();
app.close();
process.exit(0);
} catch (e) {
app.close();
process.exit(1);
}
});
src/console/seed.service.ts
import { Inject } from '@nestjs/common';
import { Console, Command } from 'nestjs-console';
import { UsersService } from 'src/users/users.service';
@Console()
export class SeedService {
constructor(
@Inject(UsersService) private usersService: UsersService,
) {}
@Command({
command: 'seed',
description: 'Seed DB',
})
async seed(): Promise<void> {
await this.seedUsers();
}
async seedUsers() {
await this.usersService.create({ name: 'Joe' });
}
}
package.json
{
"scripts": {
"console": "ts-node -r tsconfig-paths/register src/console.ts",
在 Nest.js 中,这就是 B12Toaster 使用 OnApplicationBootstrap
的替代解决方案。
src/seeding.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { UserEntity} from 'src/entities/user.entity';
import { RoleEntity } from 'src/entities/role.entity';
import { userSeeds } from 'src/seeds/user.seeds';
import { roleSeeds } from 'src/seeds/role.seeds';
@Injectable()
export class SeedingService {
constructor(
private readonly entityManager: EntityManager,
) {}
async seed(): Promise<void> {
// Replace with your own seeds
await Promise.all([
this.entityManager.save(UserEntity, userSeeds),
this.entityManager.save(RoleEntity, roleSeeds),
]);
}
}
src/app.module.ts
import { Module, OnApplicationBootstrap } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm';
import { getConnectionOptions } from 'typeorm';
@Module({
imports: [
TypeOrmModule.forRootAsync({
useFactory: async () =>
Object.assign(await getConnectionOptions(), {
autoLoadEntities: true,
}),
}),
TypeOrmModule.forFeature([
CompanyOrmEntity,
ProductOrmEntity,
]),
],
providers: [
SeedingService,
...
],
...
})
export class AppModule implements OnApplicationBootstrap {
constructor(
private readonly seedingService: SeedingService,
) {}
async onApplicationBootstrap(): Promise<void> {
await this.seedingService.seed();
}
}
我已经修改了@B12Toaster 的答案(用于在 NestJs 中播种到数据库)以便能够获取一组对象来播种。他的回答有很大帮助,我也在寻找一种方法让它一次获取多个数据库对象。这是对 seedingMiddleware.ts
所做的小修改
// file: src/seeding/SeedingMiddleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
import { TxnCategory } from 'src/txn-categories/entities/txn-category.entity';
import { init_categories } from 'src/txn-categories/entities/txn_cat-seed-data';
import { init_services } from 'src/txn-services/entities/txn-serv-seed-data';
import { TxnService } from 'src/txn-services/entities/txn-service.entity';
import { EntityManager } from 'typeorm';
import { Seeding } from './entities/seeding.entity';
@Injectable()
export class SeedingMiddleware implements NestMiddleware {
// to avoid roundtrips to db we store the info about whether
// the seeding has been completed as boolean flag in the middleware
// we use a promise to avoid concurrency cases. Concurrency cases may
// occur if other requests also trigger a seeding while it has already
// been started by the first request. The promise can be used by other
// requests to wait for the seeding to finish.
private isSeedingComplete: Promise<boolean>;
constructor(private readonly entityManager: EntityManager) {}
async use(req: Request, res: Response, next: any) {
if (await this.isSeedingComplete) {
// seeding has already taken place,
// we can short-circuit to the next middleware
return next();
}
this.isSeedingComplete = (async () => {
// for example you start with an initial seeding entry called 'initial-seeding'
// if 'init-txn-cats' and 'init-txn-serv' already exists in db, then this
// part is skipped
// MODIFIED
if (
!(await this.entityManager.findOne(Seeding, {
id: 'init-txn-cats',
}))
) {
await this.entityManager.transaction(
async (transactionalEntityManager) => {
for (let i = 0; i < init_categories.length; i++) {
await transactionalEntityManager.save(
TxnCategory,
init_categories[i],
);
}
await transactionalEntityManager.save(new Seeding('init-txn-cats'));
},
);
}
// MODIFIED
if (
!(await this.entityManager.findOne(Seeding, {
id: 'init-txn-serv',
}))
) {
await this.entityManager.transaction(
async (transactionalEntityManager) => {
for (let i = 0; i < init_services.length; i++) {
await transactionalEntityManager.save(
TxnService,
init_services[i],
);
}
await transactionalEntityManager.save(new Seeding('init-txn-serv'));
},
);
}
return true;
})();
await this.isSeedingComplete;
next();
}
}
那么引用的DB对象数组就是这样的:
// file: src/txn-categories/entities/txn_cat-seed-data.ts
export const init_categories = [
{
id: 1,
category_name: 'name 1',
category_code: 'cat_code_1',
enabled: true,
},
{
id: 2,
category_name: 'name 2',
category_code: 'cat_code_2',
enabled: true,
},
{
id: 3,
category_name: 'name 3',
category_code: 'cat_code_3',
enabled: true,
},
// etc
];
src/txn-services/entities/txn-serv-seed-data.ts 文件的格式相同。
B12Toaster 的答案中的所有其他内容都保持不变,因此您仍将拥有如下的模块和实体文件:
播种模块:
// file: src/seeding/SeedingModule.ts
@Module({})
export class SeedingModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(SeedingMiddleware)
.forRoutes('*')
}
}
播种实体:
// file: src/seeding/entities/Seeding.entity.ts
import { Entity, PrimaryColumn, CreateDateColumn } from 'typeorm';
@Entity()
export class Seeding {
@PrimaryColumn()
public id: string;
@CreateDateColumn()
creationDate: Date;
constructor(id?: string) {
this.id = id;
}
}
干杯!
我使用了一种更简单的迁移方法,这是我的代码,我相信它应该更简单,所以 运行 在你的迁移中使用这个
import { MigrationInterface, QueryRunner } from 'typeorm';
const tableName = 'foo';
const columnName = 'foo_column';
const features = ['foo_content_1', 'foo_content_2'];
export class seedIntoPermissionsTable1638518166717 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await Promise.all(features.map((feature) => queryRunner.query(`INSERT INTO ${tableName} (${columnName}) VALUES ('${feature}')`)));
}
public async down(queryRunner: QueryRunner): Promise<void> {
await Promise.all(features.map((feature) => queryRunner.query(`DELETE FROM ${tableName} WHERE ${columnName}='${feature}';`)));
}
}
这就是我倾向于使用的播种机。
我是 运行 我的 Node JS 后端使用 typeorm ORM。
来自 Entity Framework,用
等几行代码很容易为数据库设定种子Database.SetInitializer(new DbInitializer());
其中 DbInitializer class 将包含所有播种信息。
是否有类似的方法在 TypeOrm 中播种数据库? 如果没有,推荐的方法是什么?
1) 使用数据插入语句创建新迁移? 2) 创建一个实例化和保存实体的任务?
我也很想看到这样的功能(而且we're not alone), but at the moment,官方没有播种功能。
由于缺少这样的内置功能,我认为下一个最好的办法是创建一个名为 0-Seed
的迁移脚本(因此它位于您可能拥有的任何其他迁移脚本之前)并拥有种子那里填充了数据。
@bitwit has created a snippet 可能对你有用;它是一个从 yaml 文件中读取数据的函数,您可以将其合并到种子迁移脚本中。
然而,经过一些研究,我发现了另一种有趣的方法:将 after_create
事件绑定到 table,并在监听器中绑定 initialize the data。
我还没有实现这个,所以我不确定它是否可以直接用 TypeORM 完成。
不幸的是,TypeORM(在发布此答案时)没有正式发布的解决方案。
但是我们可以使用一个很好的解决方法:
- 在
ormconfig.js
文件中创建另一个连接并指定另一个 "migrations" 的文件夹 - 实际上是我们的 seeds - 使用
-c <connection name>
生成并 运行 你的种子。就是这样!
样本ormconfig.js:
module.exports = [
{
...,
migrations: [
'src/migrations/*.ts'
],
cli: {
migrationsDir: 'src/migrations',
}
},
{
name: 'seed',
...,
migrations: [
'src/seeds/*.ts'
],
cli: {
migrationsDir: 'src/seeds',
}
}
]
样本package.json:
{
...
scripts: {
"seed:generate": "ts-node typeorm migration:generate -c seed -n ",
"seed:run": "ts-node typeorm migration:run -c seed",
"seed:revert": "ts-node typeorm migration:revert -c seed",
},
...
}
对于那些将 TypeORM 与 Nest.js 一起使用的人,这里有一个解决方案可以从您的代码中以编程方式执行播种。
大致思路:
- 我们创建了一个专门的 "seeding module",其中包含一个 "seeding middleware",负责执行播种并确保在回答任何请求之前完成所有播种。
- 对于任何到达的请求,播种中间件都会拦截它并推迟它直到确认播种完成。
- 如果数据库已经播种,"seeding middleware" 将请求传递给下一个中间件。
- 为了加快速度,"seeding middleware" 在内存中保留一个 "seeding complete" 标志作为状态,以避免在播种发生后进行任何进一步的数据库检查。
实施:
为此,首先创建一个模块来注册一个侦听所有传入请求的中间件:
// file: src/seeding/SeedingModule.ts
@Module({})
export class SeedingModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(SeedingMiddleware)
.forRoutes('*')
}
}
现在创建中间件:
// file: src/seeding/SeedingMiddleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
import { EntityManager } from 'typeorm';
import { SeedingLogEntry } from './entities/SeedingLogEntry.entity';
@Injectable()
export class SeedingMiddleware implements NestMiddleware {
// to avoid roundtrips to db we store the info about whether
// the seeding has been completed as boolean flag in the middleware
// we use a promise to avoid concurrency cases. Concurrency cases may
// occur if other requests also trigger a seeding while it has already
// been started by the first request. The promise can be used by other
// requests to wait for the seeding to finish.
private isSeedingComplete: Promise<boolean>;
constructor(
private readonly entityManager: EntityManager,
) {}
async use(req: Request, res: Response, next: Function) {
if (await this.isSeedingComplete) {
// seeding has already taken place,
// we can short-circuit to the next middleware
return next();
}
this.isSeedingComplete = (async () => {
// for example you start with an initial seeding entry called 'initial-seeding'
// on 2019-06-27. if 'initial-seeding' already exists in db, then this
// part is skipped
if (!await this.entityManager.findOne(SeedingLogEntry, { id: 'initial-seeding' })) {
await this.entityManager.transaction(async transactionalEntityManager => {
await transactionalEntityManager.save(User, initialUsers);
await transactionalEntityManager.save(Role, initialRoles);
// persist in db that 'initial-seeding' is complete
await transactionalEntityManager.save(new SeedingLogEntry('initial-seeding'));
});
}
// now a month later on 2019-07-25 you add another seeding
// entry called 'another-seeding-round' since you want to initialize
// entities that you just created a month later
// since 'initial-seeding' already exists it is skipped but 'another-seeding-round'
// will be executed now.
if (!await this.entityManager.findOne(SeedingLogEntry, { id: 'another-seeding-round' })) {
await this.entityManager.transaction(async transactionalEntityManager => {
await transactionalEntityManager.save(MyNewEntity, initalSeedingForNewEntity);
// persist in db that 'another-seeding-round' is complete
await transactionalEntityManager.save(new SeedingLogEntry('another-seeding-round'));
});
}
return true;
})();
await this.isSeedingComplete;
next();
}
}
最后是我们用来在我们的数据库中记录发生了某种类型的播种的实体。确保在您的 TypeOrmModule.forRoot
调用中将其注册为实体。
// file: src/seeding/entities/Seeding.entity.ts
import { Entity, PrimaryColumn, CreateDateColumn } from 'typeorm';
@Entity()
export class Seeding {
@PrimaryColumn()
public id: string;
@CreateDateColumn()
creationDate: Date;
constructor(id?: string) {
this.id = id;
}
}
另一种使用生命周期事件的播种解决方案:
使用 Nest.js,您还可以实现 OnApplicationBootstrap
接口(参见 lifecycle events),而不是寻求基于中间件的解决方案来处理您的播种。 onApplicationBootstrap
方法将 "called once the application has fully started and is bootstrapped"。然而,与中间件解决方案相比,这种方法不允许您在多租户环境中播种数据库,在多租户环境中,将在运行时为不同租户创建数据库模式,并且需要在运行时多次进行播种创建后的不同租户。
看起来正在为此构建一个模块,typeorm-seeding。
虽然使用初始迁移进行播种也有效,但它对于测试可能需要新播种的数据库才能通过测试的地方不是很有用。一旦您开始创建更多迁移,您就无法无误地删除、同步和 运行 迁移。这可以通过对单个迁移文件 运行 migration:run
来解决,但目前不能使用 CLI。我的解决方案是一个轻量级脚本,它通过 typeorm 连接访问 QueryRunner 对象:
// testSeed.ts
import { ConnectionOptions, createConnection, QueryRunner } from "typeorm";
import { config } from "../config";
import { DevSeed } from "./DevSeed";
createConnection(config.typeOrmConfig as ConnectionOptions).then(async connection => {
let queryRunner = connection.createQueryRunner("master");
// runs all seed SQL commands in this function.
await DevSeed(queryRunner);
await queryRunner.release();
return connection.close();
});
然后运行node ./dist/path/to/testSeed.js
所以这就是我如何使用插入语句从 sql 文件播种数据。 这是添加种子后我的整个迁移文件
import { MigrationInterface, QueryRunner } from 'typeorm';
import * as path from 'path';
import * as fs from 'fs';
let insertPermissionQueries = fs
.readFileSync(path.resolve(__dirname, '../../scripts/sql/insert.sql'))
.toString()
.replace(/(\r\n|\n|\r)/gm, ' ') // remove newlines
.replace(/\s+/g, ' '); // excess white space
export class init1591103087130 implements MigrationInterface {
name = 'init1591103087130';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "public"."RoleTemp" ("idx" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" text, "created_on" TIMESTAMP DEFAULT now(), "is_active" boolean DEFAULT true, "role_type" text, "created_by" uuid NOT NULL, "status" text, "alias" text, "operation" text, "rejection_reason" text, "id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "role_id" integer, CONSTRAINT "UQ_835baad60041a3413f9ef95bc07" UNIQUE ("idx"), CONSTRAINT "PK_a76dd0012be252eefbdd4a2a589" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "RoleTemp_idx_key" ON "public"."RoleTemp" ("idx") `,
);
await queryRunner.query(
`CREATE TABLE "public"."PermissionRoleTemp" ("idx" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "permission_base_name" text, "id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "role_id" integer NOT NULL, "permission_id" integer NOT NULL, CONSTRAINT "PK_c1f2648a18ac911e096f08c187d" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "public"."Permission" ("idx" uuid NOT NULL DEFAULT uuid_generate_v4(), "base_name" text NOT NULL, "url" text NOT NULL, "method" text NOT NULL, "created_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "is_active" boolean NOT NULL DEFAULT true, "permission_type" text, "alias" text NOT NULL, "id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "PK_28657fa560adca66b359c18b952" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "public"."PermissionRole" ("idx" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "is_active" boolean NOT NULL DEFAULT true, "permission_base_name" text NOT NULL, "id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "role_id" integer NOT NULL, "permission_id" integer NOT NULL, CONSTRAINT "PK_b5e2271c229f65f17ee93677a0f" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "public"."UserRole" ("idx" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "is_active" boolean NOT NULL DEFAULT true, "id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "role_id" integer NOT NULL, "company_user_id" integer NOT NULL, CONSTRAINT "PK_431fc1ec3d46ac513ef3701604e" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "public"."UsersTemp" ("idx" uuid DEFAULT uuid_generate_v1(), "username" text, "first_name" text, "middle_name" text, "last_name" text, "password" text, "email" text, "address" text, "phone_number" text, "phone_ext" text, "company_idx" uuid, "is_superadmin" boolean NOT NULL DEFAULT false, "operation" text, "created_by" text, "status" text, "rejection_reason" text, "created_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "is_active" boolean NOT NULL DEFAULT true, "id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "role_id" integer, "user_id" integer, CONSTRAINT "PK_9d3fbcec3cc0b054324f93da038" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "public"."Role" ("idx" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" text NOT NULL, "alias" text NOT NULL, "created_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "is_active" boolean NOT NULL DEFAULT true, "role_type" text, "created_by" uuid NOT NULL, "id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "UQ_c9a53325a7642edb5f9bd44f5aa" UNIQUE ("idx"), CONSTRAINT "PK_422113329ddec949e76c7943c56" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "Role_idx_key" ON "public"."Role" ("idx") `,
);
await queryRunner.query(
`CREATE TABLE "public"."Users" ("idx" uuid NOT NULL DEFAULT uuid_generate_v4(), "username" text NOT NULL, "first_name" text NOT NULL, "middle_name" text, "last_name" text NOT NULL, "password" text NOT NULL, "email" text, "address" text, "phone_number" text, "phone_ext" text, "company_idx" uuid, "created_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "is_active" boolean NOT NULL DEFAULT true, "is_superadmin" boolean NOT NULL DEFAULT false, "id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "role_id" integer NOT NULL, CONSTRAINT "PK_ac3c96e3c912cbda773b7c7edc9" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "public"."CompanyUser" ("id" SERIAL NOT NULL, "is_obsolete" boolean NOT NULL DEFAULT false, "modified_on" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "idx" uuid NOT NULL DEFAULT uuid_generate_v4(), "company_idx" uuid, "created_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "is_active" boolean NOT NULL DEFAULT true, "user_id" integer, CONSTRAINT "PK_4a915d69bf079a8e5dd10784cc3" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "public"."RoleTemp" ADD CONSTRAINT "FK_d304588d17c9349ca6e7ebee5d3" FOREIGN KEY ("role_id") REFERENCES "public"."Role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."PermissionRoleTemp" ADD CONSTRAINT "FK_7e7cdde853500f56b3db43fc258" FOREIGN KEY ("role_id") REFERENCES "public"."RoleTemp"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."PermissionRoleTemp" ADD CONSTRAINT "FK_0068d3de1c59050561d35f17544" FOREIGN KEY ("permission_id") REFERENCES "public"."Permission"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."PermissionRole" ADD CONSTRAINT "FK_5b57492441a568bc7562fbbaa5b" FOREIGN KEY ("role_id") REFERENCES "public"."Role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."PermissionRole" ADD CONSTRAINT "FK_1951a810af06342fcd4530ec61c" FOREIGN KEY ("permission_id") REFERENCES "public"."Permission"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."UserRole" ADD CONSTRAINT "FK_fb09d73b0dd011be81a272e1efa" FOREIGN KEY ("role_id") REFERENCES "public"."Role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."UserRole" ADD CONSTRAINT "FK_b221977a41587e58d7c58e16db0" FOREIGN KEY ("company_user_id") REFERENCES "public"."CompanyUser"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."UsersTemp" ADD CONSTRAINT "FK_6d74dfaddaa94e1bba0c8c12a2f" FOREIGN KEY ("role_id") REFERENCES "public"."Role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."UsersTemp" ADD CONSTRAINT "FK_e5b2930fe35042dab17945bb131" FOREIGN KEY ("user_id") REFERENCES "public"."Users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."Users" ADD CONSTRAINT "FK_34be125e29cee0e71d58456aed7" FOREIGN KEY ("role_id") REFERENCES "public"."Role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."CompanyUser" ADD CONSTRAINT "FK_1354e3e408b5ffdebe476a6fbd2" FOREIGN KEY ("user_id") REFERENCES "public"."Users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(insertPermissionQueries);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "public"."CompanyUser" DROP CONSTRAINT "FK_1354e3e408b5ffdebe476a6fbd2"`,
);
await queryRunner.query(
`ALTER TABLE "public"."Users" DROP CONSTRAINT "FK_34be125e29cee0e71d58456aed7"`,
);
await queryRunner.query(
`ALTER TABLE "public"."UsersTemp" DROP CONSTRAINT "FK_e5b2930fe35042dab17945bb131"`,
);
await queryRunner.query(
`ALTER TABLE "public"."UsersTemp" DROP CONSTRAINT "FK_6d74dfaddaa94e1bba0c8c12a2f"`,
);
await queryRunner.query(
`ALTER TABLE "public"."UserRole" DROP CONSTRAINT "FK_b221977a41587e58d7c58e16db0"`,
);
await queryRunner.query(
`ALTER TABLE "public"."UserRole" DROP CONSTRAINT "FK_fb09d73b0dd011be81a272e1efa"`,
);
await queryRunner.query(
`ALTER TABLE "public"."PermissionRole" DROP CONSTRAINT "FK_1951a810af06342fcd4530ec61c"`,
);
await queryRunner.query(
`ALTER TABLE "public"."PermissionRole" DROP CONSTRAINT "FK_5b57492441a568bc7562fbbaa5b"`,
);
await queryRunner.query(
`ALTER TABLE "public"."PermissionRoleTemp" DROP CONSTRAINT "FK_0068d3de1c59050561d35f17544"`,
);
await queryRunner.query(
`ALTER TABLE "public"."PermissionRoleTemp" DROP CONSTRAINT "FK_7e7cdde853500f56b3db43fc258"`,
);
await queryRunner.query(
`ALTER TABLE "public"."RoleTemp" DROP CONSTRAINT "FK_d304588d17c9349ca6e7ebee5d3"`,
);
await queryRunner.query(`DROP TABLE "public"."CompanyUser"`);
await queryRunner.query(`DROP TABLE "public"."Users"`);
await queryRunner.query(`DROP INDEX "public"."Role_idx_key"`);
await queryRunner.query(`DROP TABLE "public"."Role"`);
await queryRunner.query(`DROP TABLE "public"."UsersTemp"`);
await queryRunner.query(`DROP TABLE "public"."UserRole"`);
await queryRunner.query(`DROP TABLE "public"."PermissionRole"`);
await queryRunner.query(`DROP TABLE "public"."Permission"`);
await queryRunner.query(`DROP TABLE "public"."PermissionRoleTemp"`);
await queryRunner.query(`DROP INDEX "public"."RoleTemp_idx_key"`);
await queryRunner.query(`DROP TABLE "public"."RoleTemp"`);
}
}
对于 NestJS,您还可以使用 nestjs-console
包来执行任务。这样您就可以访问实体、服务、存储库等。与@B12Toaster 提出的中间件解决方案相比,我更喜欢这种解决方案,因为您不需要将其作为生产代码进行维护。
如下图创建一个seed
命令,然后简单的:yarn console seed
.
这里有一个工作示例(在 CI 中运行):https://github.com/thisismydesign/nestjs-starter/tree/ee7abf6d481b1420708e87dea3cb99ca110cc168
按照这些思路:
src/console.ts
import { BootstrapConsole } from 'nestjs-console';
import { AppModule } from 'src/server/app/app.module';
const bootstrap = new BootstrapConsole({
module: AppModule,
useDecorators: true,
});
bootstrap.init().then(async (app) => {
try {
await app.init();
await bootstrap.boot();
app.close();
process.exit(0);
} catch (e) {
app.close();
process.exit(1);
}
});
src/console/seed.service.ts
import { Inject } from '@nestjs/common';
import { Console, Command } from 'nestjs-console';
import { UsersService } from 'src/users/users.service';
@Console()
export class SeedService {
constructor(
@Inject(UsersService) private usersService: UsersService,
) {}
@Command({
command: 'seed',
description: 'Seed DB',
})
async seed(): Promise<void> {
await this.seedUsers();
}
async seedUsers() {
await this.usersService.create({ name: 'Joe' });
}
}
package.json
{
"scripts": {
"console": "ts-node -r tsconfig-paths/register src/console.ts",
在 Nest.js 中,这就是 B12Toaster 使用 OnApplicationBootstrap
的替代解决方案。
src/seeding.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { UserEntity} from 'src/entities/user.entity';
import { RoleEntity } from 'src/entities/role.entity';
import { userSeeds } from 'src/seeds/user.seeds';
import { roleSeeds } from 'src/seeds/role.seeds';
@Injectable()
export class SeedingService {
constructor(
private readonly entityManager: EntityManager,
) {}
async seed(): Promise<void> {
// Replace with your own seeds
await Promise.all([
this.entityManager.save(UserEntity, userSeeds),
this.entityManager.save(RoleEntity, roleSeeds),
]);
}
}
src/app.module.ts
import { Module, OnApplicationBootstrap } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm';
import { getConnectionOptions } from 'typeorm';
@Module({
imports: [
TypeOrmModule.forRootAsync({
useFactory: async () =>
Object.assign(await getConnectionOptions(), {
autoLoadEntities: true,
}),
}),
TypeOrmModule.forFeature([
CompanyOrmEntity,
ProductOrmEntity,
]),
],
providers: [
SeedingService,
...
],
...
})
export class AppModule implements OnApplicationBootstrap {
constructor(
private readonly seedingService: SeedingService,
) {}
async onApplicationBootstrap(): Promise<void> {
await this.seedingService.seed();
}
}
我已经修改了@B12Toaster 的答案(用于在 NestJs 中播种到数据库)以便能够获取一组对象来播种。他的回答有很大帮助,我也在寻找一种方法让它一次获取多个数据库对象。这是对 seedingMiddleware.ts
所做的小修改// file: src/seeding/SeedingMiddleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
import { TxnCategory } from 'src/txn-categories/entities/txn-category.entity';
import { init_categories } from 'src/txn-categories/entities/txn_cat-seed-data';
import { init_services } from 'src/txn-services/entities/txn-serv-seed-data';
import { TxnService } from 'src/txn-services/entities/txn-service.entity';
import { EntityManager } from 'typeorm';
import { Seeding } from './entities/seeding.entity';
@Injectable()
export class SeedingMiddleware implements NestMiddleware {
// to avoid roundtrips to db we store the info about whether
// the seeding has been completed as boolean flag in the middleware
// we use a promise to avoid concurrency cases. Concurrency cases may
// occur if other requests also trigger a seeding while it has already
// been started by the first request. The promise can be used by other
// requests to wait for the seeding to finish.
private isSeedingComplete: Promise<boolean>;
constructor(private readonly entityManager: EntityManager) {}
async use(req: Request, res: Response, next: any) {
if (await this.isSeedingComplete) {
// seeding has already taken place,
// we can short-circuit to the next middleware
return next();
}
this.isSeedingComplete = (async () => {
// for example you start with an initial seeding entry called 'initial-seeding'
// if 'init-txn-cats' and 'init-txn-serv' already exists in db, then this
// part is skipped
// MODIFIED
if (
!(await this.entityManager.findOne(Seeding, {
id: 'init-txn-cats',
}))
) {
await this.entityManager.transaction(
async (transactionalEntityManager) => {
for (let i = 0; i < init_categories.length; i++) {
await transactionalEntityManager.save(
TxnCategory,
init_categories[i],
);
}
await transactionalEntityManager.save(new Seeding('init-txn-cats'));
},
);
}
// MODIFIED
if (
!(await this.entityManager.findOne(Seeding, {
id: 'init-txn-serv',
}))
) {
await this.entityManager.transaction(
async (transactionalEntityManager) => {
for (let i = 0; i < init_services.length; i++) {
await transactionalEntityManager.save(
TxnService,
init_services[i],
);
}
await transactionalEntityManager.save(new Seeding('init-txn-serv'));
},
);
}
return true;
})();
await this.isSeedingComplete;
next();
}
}
那么引用的DB对象数组就是这样的:
// file: src/txn-categories/entities/txn_cat-seed-data.ts
export const init_categories = [
{
id: 1,
category_name: 'name 1',
category_code: 'cat_code_1',
enabled: true,
},
{
id: 2,
category_name: 'name 2',
category_code: 'cat_code_2',
enabled: true,
},
{
id: 3,
category_name: 'name 3',
category_code: 'cat_code_3',
enabled: true,
},
// etc
];
src/txn-services/entities/txn-serv-seed-data.ts 文件的格式相同。
B12Toaster 的答案中的所有其他内容都保持不变,因此您仍将拥有如下的模块和实体文件:
播种模块:
// file: src/seeding/SeedingModule.ts
@Module({})
export class SeedingModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(SeedingMiddleware)
.forRoutes('*')
}
}
播种实体:
// file: src/seeding/entities/Seeding.entity.ts
import { Entity, PrimaryColumn, CreateDateColumn } from 'typeorm';
@Entity()
export class Seeding {
@PrimaryColumn()
public id: string;
@CreateDateColumn()
creationDate: Date;
constructor(id?: string) {
this.id = id;
}
}
干杯!
我使用了一种更简单的迁移方法,这是我的代码,我相信它应该更简单,所以 运行 在你的迁移中使用这个
import { MigrationInterface, QueryRunner } from 'typeorm';
const tableName = 'foo';
const columnName = 'foo_column';
const features = ['foo_content_1', 'foo_content_2'];
export class seedIntoPermissionsTable1638518166717 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await Promise.all(features.map((feature) => queryRunner.query(`INSERT INTO ${tableName} (${columnName}) VALUES ('${feature}')`)));
}
public async down(queryRunner: QueryRunner): Promise<void> {
await Promise.all(features.map((feature) => queryRunner.query(`DELETE FROM ${tableName} WHERE ${columnName}='${feature}';`)));
}
}
这就是我倾向于使用的播种机。