likes
comments
collection
share

Nest.js + Typeorm 尝试动态建表

作者站长头像
站长
· 阅读数 34

需求:希望通过 form 表单操作,进行建表和对数据的增删改查 那么本文将用 TypeScript + Nestjs + Typeorm 满足 “ 动态建表 ” 这一操作

为什么是 Nest

TypeScript 愈来愈受到前端开发者的青睐

Nest 官网简介

Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用 JavaScript 的渐进增强的能力,使用并完全支持 TypeScript (仍然允许开发者使用纯 JavaScript 进行开发),并结合了 OOP (面向对象编程)、FP (函数式编程)和 FRP (函数响应式编程)

在底层,Nest 构建在强大的 HTTP 服务器框架上,例如 Express (默认),并且还可以通过配置从而使用 Fastify !

Nest 在这些常见的 Node.js 框架 (Express/Fastify) 之上提高了一个抽象级别,但仍然向开发者直接暴露了底层框架的 API。这使得开发者可以自由地使用适用于底层平台的无数的第三方模块

提取出来的信息

  • 完全支持 TypeScript
  • Nest 是基于 Express 的。那么 Nest 能够利用 Express 的中间件,使其生态完善
  • OOP、FP、FRP 多种开发模式任君挑选

对比一下 Github 的 Star 数

Nest.js + Typeorm 尝试动态建表

Nest.js + Typeorm 尝试动态建表

Nest.js + Typeorm 尝试动态建表

Nest.js + Typeorm 尝试动态建表

Nest.js + Typeorm 尝试动态建表

NestJS 的 Star 数比 Koa 还高。可见热度之高

  • Koa 和 Express 是较为基础的 HTTP 请求库。可自行安装插件,自由度高。但是也是因为自由度高,如果开发者水平跟我一样菜。那可能会出现一个项目会有多种风格,而且前期搭插件也相对耗时
  • Egg 挺好的。有自己的一套规范,避免了开发风格不统一,而且好上手,文档也好,但是相较于 Nest 来说 TypeScript 的支持度不高
  • Midway 挺看好的。Nest 基于 Express,Midway 基于 Koa 。但从 Star 数来看, Midway 现在的热度确实也是比较低的。可能项目比较新吧,所以暂时先观望观望吧

技术选型还是要根据实际项目需求进行选择哈。基于以上的调研,所以采用了 Nest

为什么是 Typeorm

咋们先来说说什么是 orm ORM(Object Relational Mapping),是在关系型数据库和对象之间作一个映射,在具体的操作数据库的时候,就不需要再去和复杂的SQL语句打交道,可以像平时操作对象一样来对数据进行操作。( 后端初学者狂喜 😄 )


Nest.js + Typeorm 尝试动态建表

Nest.js + Typeorm 尝试动态建表

Nest.js + Typeorm 尝试动态建表

  • 单从 Star 数来看,Typeorm 这轮胜出
  • 从文档上来看。都有自己的中文文档。
  • 三个框架都支持 TypeScript
  • Nest 官方文档有着 Typeorm 和 Sequelize 的基础使用

综合上述几点, 选择了 Typeorm

基础使用

这里根据 Nest 官网文档的 CLI 初始化项目,根据提示创建项目

npm i -g @nestjs/cli
nest new project-name

执行完之后得到以下结构

Nest.js + Typeorm 尝试动态建表

根据 Nest 官网提示安装 Typeorm

npm install --save @nestjs/typeorm typeorm mysql2

利用 Nest Cli 工具创建 User 模块

nest g mo /modules/user
nest g co /modules/user
nest g s  /modules/user

Cli 工具会自动帮我们把模块注入到 user.modules.ts

PS : 要先创建 modules,随后 Service 和 Controller 才会注入到 user.modules.ts , 不然就会注入到 app.module.ts

根据官网提示将服务搭起来

// app.module.ts
import { Module } from '@nestjs/common';
import { UserModule } from './modules/user/user.module';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    UserModule,
    TypeOrmModule.forRootAsync({
      useFactory: () => ({
        type: 'mysql',
        host: 'localhost',
        port: 3306,
        username: 'root',
        password: 'root',
        database: 'test_db',
        autoLoadEntities: true, // 自动链接被 forFeature 注册的实体
        synchronize: true, // 实体与表同步 调试模式下开始。不然会有强替换导致数据丢是
      }),
      inject: [],
    }),
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

在 /modules/user/ 里面创建 user.entity.ts

// user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;
}

// user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  async remove(id: string): Promise<void> {
    await this.usersRepository.delete(id);
  }
}

// user.controller.ts
import { Controller, Get } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  async index() {
    this.userService.findAll();
  }
}

// user.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UserService],
  controllers: [UserController],
})
export class UserModule {}

npm run start:dev 跑一下,user 表就创建好了,synchronize 为 true 时在项目启动的时候。就会进行表同步。但是正式环境如果将表同步交给这个属性可会出大事。如果修改了实体导致实体与表结构不同。会进行强替换。导致数据丢失。在开发环境下图个方便可以开启。在正式环境切记关了。

Nest.js + Typeorm 尝试动态建表

这里采用 autoLoadEntities 的方式自动载入实体,每个通过forFeature()注册的实体都会自动添加到配置对象的entities数组中。

注意,那些没有通过forFeature()方法注册,而仅仅是在实体中被引用(通过关系)的实体不能通过autoLoadEntities配置被包含。

根据官网的示例,是需要用实例进行表同步的。现在的需求是,根据前端传过来的 JSON 生成数据库表,达到动态建表的需求。

尝试动态建表

TypeORM 中文文档 | 迁移 里有一个 queryRunner这个 API。可以调用 createTable 进行建表操作,参数也符合我们预期的 JSON 格式。

Nest.js + Typeorm 尝试动态建表

根据上面 Cli 的方式新建一个 Serivce 、 Controller、Module 尝试一下。

// common_table.service.ts
import { Injectable } from "@nestjs/common";
import { getConnection, Table, TableOptions } from "typeorm";
@Injectable()
export class CommonTableService {
  async create(schema: TableOptions) {
    const queryRunner = await getConnection().createQueryRunner();
    await queryRunner.createTable(new Table(schema), true);
    await queryRunner.release();
    return { msg: 'success' };
  }

  async select(tableName: string) {
    const selectData = await getConnection()
      .createQueryRunner()
      .query(`SELECT * FROM ${tableName}`);
    console.log(selectData);
    return selectData;
  }
}

// common_table.controller.ts
import { Body, Controller, Get, Post, Param, Query } from '@nestjs/common';
import { CommonTableService } from './common_table.service';

@Controller('common_table')
export class CommonTableController {
  constructor(private readonly commonTableService: CommonTableService) {}

  @Get('select')
  select(@Query('table') table) {
    return this.commonTableService.select(table);
  }

  @Post('add')
  async add(@Body() schema) {
    const { msg } = await this.commonTableService.create(schema);
    if (msg === 'success') return { code: 200 };
    return { code: -1, msg: 'CreateTable Error' };
  }
}


用 Postman 请求一下

Nest.js + Typeorm 尝试动态建表

Nest.js + Typeorm 尝试动态建表

成功创建了 question 表, queryRunner 还有很多 API 供我们使用。 Schema 的字段也是比较丰富的。可以查看 TableOptions 里面的参数。

这种方式创建的表是没办法用到 Query Builder ,但是可以用 SQL 语句可以查询 都用上 ORM 了,还用 SQL 语句 ? 这不能忍!

QueryBuilder 是 TypeORM 最强大的功能之一 ,它允许你使用优雅便捷的语法构建 SQL 查询


先梳理一下现在的情况。现在是先有了表,但是没有实体。那么问题一下就明确起来 Typeorm 有一个扩展 , ( 从现有数据库生成模型 - typeorm-model-generator ) 恰巧可以解决这个问题

我们需要指定一个存放实例的文件夹,在 src 目录下 创建一个文件夹 entities

每次生成实例之前清空一下实例文件夹,避免生成的实例不准确。这里用 rimraf 删除文件夹 , 解决平台删除命令差异 npx typeorm-model-generator 如果全局安装了就不需要加 npx 没有全局安装就加上去 ( 建议全局安装。npx 考虑到网络不好的情况会很慢 )

  • -h IP 地址
  • -d 数据库名字
  • -p 端口
  • -u 用户名
  • -x 密码
  • -e 数据库类型
  • -o entities表示输出到指定的文件夹
  • --noConfig true表示不生成ormconfig.json和tsconfig.json文件
  • --ce pascal表示将类名转换首字母是大写的驼峰命名
  • --cp camel表示将数据库中的字段比如create_at转换为createAt
  • -a 表示会继承一个BaseEntity的类,根据自己需求加

package.json 增加一个命令

"scripts": {
  "db": "rimraf ./src/entities & npx typeorm-model-generator -h 127.0.0.1 -d test_db -p 3306 -u root -x root -e mysql -o ./src/entities --noConfig true --ce pascal --cp camel"
}

先尝试运行一下命令

Nest.js + Typeorm 尝试动态建表

Nest.js + Typeorm 尝试动态建表

得到相关的实体 , 稍微再修改下 Service 的 select 方法, 在数据库随意添加几条数据

// common_table.service.ts
import { Injectable } from '@nestjs/common';
import {
  getConnection,
  Table,
  TableOptions,
  createQueryBuilder,
} from 'typeorm';
@Injectable()
export class CommonTableService {
  ...
  async select(tableName: string) {
    const modules = await import(`../entities/${tableName}`);
    const selectData = await createQueryBuilder(modules[tableName]).getOne();
    console.log(selectData);
    return selectData;
  }
}

Postman 直接调用 select 方法得出结果

Nest.js + Typeorm 尝试动态建表

最后再调整一下 controller 的 add 方法,让每次创建表后都自动执行脚本

// common_table.controller.ts
import { Body, Controller, Get, Post, Param, Query } from '@nestjs/common';
import { CommonTableService } from 'src/services/common_table/common_table.service';
import { execSync } from 'child_process';

@Controller('common_table')
export class CommonTableController {
  constructor(private readonly commonTableService: CommonTableService) {}
  ...
  @Post('add')
  async add(@Body() schema) {
    const { msg } = await this.commonTableService.create(schema);
    if (msg === 'success') {
      execSync('npm run db');
      return { code: 200 };
    }
    return { code: -1, msg: 'CreateTable Error' };
  }
}

优化方案

发现 typeorm-model-generator 脚本执行的很慢;那么不得不考虑其他方法; Typeorm 上有一种分离式实体;可以在单独的文件中定义一个实体及其列,这些文件在Typeorm中称为"entity schemas"。

import {EntitySchema} from "typeorm";

export const CategoryEntity = new EntitySchema({
    name: "category",
    columns: {
        id: {
            type: Number,
            primary: true,
            generated: true
        },
        name: {
            type: String
        }
    }
});

最终方案

实践后发现上述方法的效率不高,翻阅一系列文章后,发现如下方法

import {Entity, PrimaryColumn, Column} from "typeorm";

export function createEntity(tableName: string) {
    @Entity({name: tableName})
    class EntityClass {
        public static tableName = tableName;

        @PrimaryColumn()
        public name: string = "";

        @Column()
        public value: number = 0;
    }

    return EntityClass;
}
import {Connection, createConnection} from "typeorm";

class ConnectionService {
    public connections: Map<any, Promise<Connection>> = new Map();

   public async getConnection(entityType: object) {
      const key = entityType;
      if (!this.connections.has(key)) {
         const tableName = (entityType as any).tableName;
         const name = `table:${tableName}`;

         const options = {type: "mysql"}; // the rest of the config data 
         const newOptions = {...options, name, entities: [entityType] as any}
         const connection = createConnection(newOptions);
         this.connections.set(key, connection);
      }
      return this.connections.get(key) as Promise<Connection>;
   }
}

export const connectionService = new ConnectionService();
const EntityA = createEntity("any-table-name");
const connectionA = await connectionService.getConnection(EntityA);

let repositoryA = connectionA.getRepository(EntityA);
const testingA = await repositoryA.create();
testingA.name = "hello-world";
await repositoryA.save(testingA);

总结

以上是围绕着动态建表 这个需求展开的一些调研以及想法,如果有更好的方案,或者觉得文章中有错误的地方,对于初学者的我来说,欢迎同学们一起探讨以及指正 ❤️❤️❤️