Nest.js + Typeorm 尝试动态建表
需求:希望通过 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 数
NestJS 的 Star 数比 Koa 还高。可见热度之高
- Koa 和 Express 是较为基础的 HTTP 请求库。可自行安装插件,自由度高。但是也是因为自由度高,如果开发者水平跟我一样菜。那可能会出现一个项目会有多种风格,而且前期搭插件也相对耗时
- Egg 挺好的。有自己的一套规范,避免了开发风格不统一,而且好上手,文档也好,但是相较于 Nest 来说 TypeScript 的支持度不高
- Midway 挺看好的。Nest 基于 Express,Midway 基于 Koa 。但从 Star 数来看, Midway 现在的热度确实也是比较低的。可能项目比较新吧,所以暂时先观望观望吧
技术选型还是要根据实际项目需求进行选择哈。基于以上的调研,所以采用了 Nest
为什么是 Typeorm
咋们先来说说什么是 orm
ORM(Object Relational Mapping)
,是在关系型数据库和对象之间作一个映射,在具体的操作数据库的时候,就不需要再去和复杂的SQL语句打交道,可以像平时操作对象一样来对数据进行操作。( 后端初学者狂喜 😄 )
- 单从 Star 数来看,Typeorm 这轮胜出
- 从文档上来看。都有自己的中文文档。
- 三个框架都支持 TypeScript
- Nest 官方文档有着 Typeorm 和 Sequelize 的基础使用
综合上述几点, 选择了 Typeorm
基础使用
这里根据 Nest 官网文档的 CLI 初始化项目,根据提示创建项目
npm i -g @nestjs/cli
nest new project-name
执行完之后得到以下结构
根据 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 时在项目启动的时候。就会进行表同步。但是正式环境如果将表同步交给这个属性可会出大事。如果修改了实体导致实体与表结构不同。会进行强替换。导致数据丢失。在开发环境下图个方便可以开启。在正式环境切记关了。
这里采用 autoLoadEntities
的方式自动载入实体,每个通过forFeature()
注册的实体都会自动添加到配置对象的entities
数组中。
注意,那些没有通过
forFeature()
方法注册,而仅仅是在实体中被引用(通过关系)的实体不能通过autoLoadEntities
配置被包含。
根据官网的示例,是需要用实例进行表同步的。现在的需求是,根据前端传过来的 JSON 生成数据库表,达到动态建表的需求。
尝试动态建表
TypeORM 中文文档 | 迁移 里有一个 queryRunner
这个 API。可以调用 createTable
进行建表操作,参数也符合我们预期的 JSON 格式。
根据上面 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 请求一下
成功创建了 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"
}
先尝试运行一下命令
得到相关的实体 , 稍微再修改下 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 方法得出结果
最后再调整一下 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);
总结
以上是围绕着动态建表 这个需求展开的一些调研以及想法,如果有更好的方案,或者觉得文章中有错误的地方,对于初学者的我来说,欢迎同学们一起探讨以及指正 ❤️❤️❤️
转载自:https://juejin.cn/post/7086326273491861535