TypeORM如何填充数据库

69

我正在使用typeorm ORM运行我的Node JS后端。

来自Entity Framework,仅需几行代码即可轻松填充数据库:

Database.SetInitializer(new DbInitializer()); 

DbInitializer类将包含所有种子信息。

TypeOrm中是否有类似的方法来填充数据库? 如果没有,那么推荐的做法是什么?

1)使用数据插入语句创建一个新的迁移? 2)创建一个任务,实例化并保存实体?


种子数据库?您可以通过一些GUI或命令行SQL代码直接输入数据,或者只需构建您的CRUD模块并输入数据。 - Preston
使用 TypeORM CLI,您可以创建迁移,如 https://github.com/typeorm/typeorm/blob/master/docs/migrations.md 中所述。 - Jesus Gilberto Valenzuela
我最近发表了一篇关于如何种子化数据库的文章。请查看:https://medium.com/@bansalsushil_34403/how-to-seed-typeorm-d9637a5948cc - sushil bansal
12个回答

67

很遗憾,在发布本回答时,TypeORM 没有正式发布的解决方案。

但是,我们可以使用一个不错的解决方法:

  1. ormconfig.js 文件中创建另一个连接,并为“迁移”指定另一个文件夹 - 实际上是我们的种子数据
  2. 使用 -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",
  },
  ...
}

20

如果您正在使用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 方法将“在应用程序完全启动并引导后仅被调用一次”。然而,与中间件解决方案相比,这种方法将不允许您在多租户环境中为不同租户创建的数据库架构进行运行时种植,并需要在运行时为不同租户多次进行种植。


18
我也很希望能看到这样的功能(我们不是唯一的),但目前还没有官方的种子功能。
由于缺乏内置功能,我认为最好的办法是创建一个名为0-Seed的迁移脚本(这样它就会在你可能拥有的任何其他迁移脚本之前运行),并在那里填充种子数据。 @bitwit已经创建了一个代码片段,可能对你有用;它是一个从yaml文件读取数据的函数,你可以将其合并到种子迁移脚本中。

经过一些研究,我发现另一种有趣的方法:将after_create事件绑定到表中,并在监听器中初始化数据
我还没有实现这个方法,所以不确定它是否可以直接在TypeORM中完成。


2
关于:https://github.com/w3tecch/typeorm-seeding#-introduction - Memke

6
在Nest.js中,这是使用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();
      }
    }

3
看起来已经有一个模块正在为此构建,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


2
此外,对于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",

2

最简单、最高效的方法是按照以下方式创建一个新的迁移文件:

    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();
      }
  }

运行以下代码,它会像魔法一样正常工作!

1

我修改了@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();
  }
}

那么引用的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 的回答中的其他内容保持不变,因此您仍将拥有以下模块和实体文件:

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;
  }
}

干杯!


0

我在这里使用了一种更简单的迁移方法,以下是我的代码。我相信它应该更简单,所以在你的迁移中运行它。

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}';`)));
  }
}

这是我倾向于用于我的种子文件的内容。


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接