我正在使用typeorm ORM运行我的Node JS后端。
来自Entity Framework,仅需几行代码即可轻松填充数据库:
Database.SetInitializer(new DbInitializer());
DbInitializer类将包含所有种子信息。
TypeOrm中是否有类似的方法来填充数据库? 如果没有,那么推荐的做法是什么?
1)使用数据插入语句创建一个新的迁移? 2)创建一个任务,实例化并保存实体?
我正在使用typeorm ORM运行我的Node JS后端。
来自Entity Framework,仅需几行代码即可轻松填充数据库:
Database.SetInitializer(new DbInitializer());
DbInitializer类将包含所有种子信息。
TypeOrm中是否有类似的方法来填充数据库? 如果没有,那么推荐的做法是什么?
1)使用数据插入语句创建一个新的迁移? 2)创建一个任务,实例化并保存实体?
很遗憾,在发布本回答时,TypeORM 没有正式发布的解决方案。
但是,我们可以使用一个不错的解决方法:
ormconfig.js
文件中创建另一个连接,并为“迁移”指定另一个文件夹 - 实际上是我们的种子数据-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",
},
...
}
如果您正在使用Nest.js与TypeORM,这里有一个解决方案可以让您在代码内部以编程方式进行种子数据填充。
大致思路:
实现:
为了使其工作,首先创建一个模块,该模块注册一个中间件来监听所有传入的请求:
// 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
接口(请参见生命周期事件)的方法,而不是采用基于中间件的解决方案来处理您的种子。 onApplicationBootstrap
方法将“在应用程序完全启动并引导后仅被调用一次”。然而,与中间件解决方案相比,这种方法将不允许您在多租户环境中为不同租户创建的数据库架构进行运行时种植,并需要在运行时为不同租户多次进行种植。
OnApplicationBootstrap
来实现B12Toaster的替代解决方案的样子。
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();
}
}
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
nestjs-console
seed
命令,然后简单地运行:yarn console seed
。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",
最简单、最高效的方法是按照以下方式创建一个新的迁移文件:
import { MigrationInterface, QueryRunner } from 'typeorm';
export class <Class Name> implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.connection
.createQueryBuilder()
.insert()
.into('table_name', ['columns_1','column_2',...])
.values([
{
columns_1: value,
},
{
column_2: value
}
])
.execute();
}
}
我修改了@B12Toaster的答案(用于在NestJs中向数据库进行种子数据填充),以便能够接受对象数组进行填充。他的答案帮助很大,我也正在寻找一种方法使其能够一次性接受多个DB对象。以下是对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();
}
}
// 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 的回答中的其他内容保持不变,因此您仍将拥有以下模块和实体文件:
SeedingModule:
// file: src/seeding/SeedingModule.ts
@Module({})
export class SeedingModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(SeedingMiddleware)
.forRoutes('*')
}
}
SeedingEntity:
// 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}';`)));
}
}
这是我倾向于用于我的种子文件的内容。