nestjs实战(六):诺依Nest.js + MySQL 项目改造为兼容达梦8数据库详细教程

nestjs实战(六):诺依Nest.js + MySQL 项目改造为兼容达梦8数据库详细教程 一、前言1.1 本教程目标本教程旨在帮助开发者将基于Nest.js TypeORM MySQL构建的后台管理系统诺依框架 Nest.js 版平滑迁移至国产达梦8数据库。内容涵盖驱动安装、动态数据源配置、Service 层改造构造函数注入、Repository 获取、查询语法调整、实体兼容性说明及数据迁移等核心环节确保项目在达梦8上稳定运行。1.2 基础知识要求Nest.js 基础了解模块、服务、依赖注入、TypeORM 集成TypeORM 基础掌握实体定义、Repository 模式、查询构建器数据库基础熟悉 MySQL 基本语法了解达梦8基本特性Node.js 环境Node.js 14npm/yarn 包管理工具界面截图二、项目基础准备2.1 原始项目结构MySQL版textsrc/ ├── config/ # 配置文件 │ ├── index.ts # 统一配置导出 │ └── dev.yml # 开发环境配置含数据库 ├── database/ │ └── database.module.ts # 自定义动态数据库模块 ├── module/ # 业务模块 │ ├── system/ # 系统管理模块 │ │ ├── role/ # 角色管理 │ │ │ ├── entities/ # 实体 │ │ │ ├── dto/ # 数据传输对象 │ │ │ └── role.service.ts │ │ └── ... │ └── ... ├── common/ # 公共模块 ├── app.module.ts # 根模块 └── main.ts2.2 安装达梦8驱动重要不要安装nestjs/typeorm请按照以下依赖安装bashnpm install nestjs/config typeorm mysql2 dmdb typeorm-dm reflect-metadatadmdb达梦8官方 Node.js 驱动替代dm8-nodejstypeorm-dmTypeORM 对达梦数据库的适配器reflect-metadataTypeORM 运行时的依赖三、数据库配置改造3.1 原 MySQL 配置dev.ymlyamldb: mysql: host: localhost username: userA #修改为自己的账号 password: xxxxx #修改为自己的密码 database: dmtest port: 33061 charset: utf8mb4 synchronize: true logging: false3.2 新增达梦8配置dev.yml在原有配置基础上添加dm节点# 数据库配置 DB_TYPE: dm db: dm: # 新增达梦配置 host: localhost port: 5236 username: userA #修改为自己的账号 password: XXXXXX #修改为自己的密码 schema: DMTEST # 必须指定 schema synchronize: true logging: false3.3 动态数据源模块database.module.ts创建自定义DatabaseModule根据DB_TYPE动态选择驱动。核心代码已提供typescriptimport { Module, Global, DynamicModule, Logger } from nestjs/common; import { ConfigModule, ConfigService } from nestjs/config; import { DataSource } from typeorm; import { DmdbDataSource } from typeorm-dm; import { join } from path; export interface DatabaseOptions { entities?: (Function | string)[]; } export interface DatabaseAsyncOptions { useFactory: (...args: any[]) DatabaseOptions | PromiseDatabaseOptions; inject?: any[]; imports?: any[]; } Global() Module({}) export class DatabaseModule { static forRoot(entitiesOrOptions?: (Function | string)[] | DatabaseOptions): DynamicModule { let options: DatabaseOptions; if (Array.isArray(entitiesOrOptions)) { options { entities: entitiesOrOptions }; } else { options entitiesOrOptions || {}; } return { module: DatabaseModule, global: true, imports: [ConfigModule], providers: this.createDataSourceProvider(options), exports: [DATA_SOURCE], }; } static forRootAsync(options: DatabaseAsyncOptions): DynamicModule { const imports options.imports || []; const entitiesProvider { provide: DATABASE_OPTIONS, useFactory: options.useFactory, inject: options.inject || [], }; return { module: DatabaseModule, global: true, imports: [...imports, ConfigModule], providers: [entitiesProvider, ...this.createDataSourceProviderAsync()], exports: [DATA_SOURCE], }; } /** * 实体扫描路径 * - 开发环境ts-node扫描 src - 生产环境编译后扫描 */ private static getDefaultEntityPatterns(): string[] { // 检测是否使用 ts-node 运行 const usingTsNode process.execArgv.some(arg arg.includes(ts-node)); const isDev process.env.NODE_ENV ! production || !process.env.NODE_ENV; if (usingTsNode || isDev) { // 开发环境扫描 TypeScript 源文件 // __dirname 在开发时指向 src/database因此需要回到 src 目录 const srcPath join(__dirname, ..); // 返回 src 目录 return [join(srcPath, **, *.entity.ts)]; } else { // 生产环境扫描编译后的 JavaScript 文件 return [join(__dirname, **, *.entity.js)]; } } private static createDataSourceProvider(options: DatabaseOptions) { return [ { provide: DATA_SOURCE, useFactory: async (configService: ConfigService) { const logger new Logger(DatabaseModule); const dbType configService.getstring(DB_TYPE); logger.log(数据库类型: ${dbType}); // 获取嵌套配置db.mysql 或 db.dm const dbConfig configService.get(db.${dbType}); if (!dbConfig) { throw new Error(未找到数据库配置: db.${dbType}); } const entities options.entities || this.getDefaultEntityPatterns(); logger.log(实体扫描路径: ${JSON.stringify(entities)}); let dataSource: DataSource; if (dbType mysql) { const { host, port, username, password, database, synchronize, logging } dbConfig; logger.log(MySQL 连接参数: ${host}:${port}, user${username}, db${database}); dataSource new DataSource({ type: mysql, host, port, username, password, database, entities, synchronize: synchronize ?? true, logging: logging ?? true, }); } else if (dbType dm) { const { host, port, username, password, schema, synchronize, logging } dbConfig; logger.log(达梦连接参数: ${host}:${port}, user${username}, schema${schema}); dataSource new DmdbDataSource({ type: oracle, innerType: dmdb, host, port, username, password, schema, entities, synchronize: synchronize ?? true, logging: logging ?? true, extra: { connectTimeout: 30000, loginEncrypt: false, // 避免加密错误 }, }); } else { throw new Error(不支持的数据库类型: ${dbType}); } await dataSource.initialize(); logger.log(${dbType} 数据库连接成功); return dataSource; }, inject: [ConfigService], }, ]; } private static createDataSourceProviderAsync() { return [ { provide: DATA_SOURCE, useFactory: async ( configService: ConfigService, options: DatabaseOptions, ) { const logger new Logger(DatabaseModule); const dbType configService.getstring(DB_TYPE); logger.log(数据库类型: ${dbType}); const dbConfig configService.get(db.${dbType}); if (!dbConfig) { throw new Error(未找到数据库配置: db.${dbType}); } const entities options.entities || this.getDefaultEntityPatterns(); logger.log(实体扫描路径: ${JSON.stringify(entities)}); let dataSource: DataSource; if (dbType mysql) { const { host, port, username, password, database, synchronize, logging } dbConfig; logger.log(MySQL 连接参数: ${host}:${port}, user${username}, db${database}); dataSource new DataSource({ type: mysql, host, port, username, password, database, entities, synchronize: synchronize ?? true, logging: logging ?? true, }); } else if (dbType dm) { const { host, port, username, password, schema, synchronize, logging } dbConfig; logger.log(达梦连接参数: ${host}:${port}, user${username}, schema${schema}); dataSource new DmdbDataSource({ type: oracle, innerType: dmdb, host, port, username, password, schema, entities, synchronize: synchronize ?? true, logging: logging ?? true, extra: { connectTimeout: 30000, loginEncrypt: false, }, }); } else { throw new Error(不支持的数据库类型: ${dbType}); } await dataSource.initialize(); logger.log(${dbType} 数据库连接成功); return dataSource; }, inject: [ConfigService, DATABASE_OPTIONS], }, ]; } }3.4 根模块导入app.module.tstypescriptimport { Module } from nestjs/common; import { ConfigModule } from nestjs/config; import { DatabaseModule } from ./database/database.module; import configuration from ./config/index; // ... 其他导入 Module({ imports: [ ConfigModule.forRoot({ load: [configuration], isGlobal: true, }), DatabaseModule.forRootAsync({ useFactory: () ({ entities: [${__dirname}/**/*.entity{.ts,.js}], }), inject: [], }), // ... 其他业务模块 ], // ... }) export class AppModule {}四、Service 层改造核心4.1 原有 Service 连接方式回顾在传统 Nest.js TypeORM 项目中Service 通常通过InjectRepository()装饰器注入 Repositorytypescript// 原有方式仅适用于固定数据库 Injectable() export class RoleService { constructor( InjectRepository(SysRoleEntity) private roleRepository: RepositorySysRoleEntity, // ... ) {} }这种写法将 Repository 与具体数据库绑定无法动态切换数据源。为了实现多数据库兼容我们改为自定义数据源注入方式。4.2 改造后的 Service 构造函数关键改造点通过Inject(DATA_SOURCE)获取动态数据源实例然后使用dataSource.getRepository()获取各个实体的 Repository。typescriptimport { Injectable, Inject } from nestjs/common; import { Repository, In, FindManyOptions } from typeorm; import { DmdbDataSource } from typeorm-dm; // 注意这里导入的是自定义数据源类型 import { SysRoleEntity } from ./entities/role.entity; import { SysRoleWithMenuEntity } from ./entities/role-width-menu.entity; import { SysRoleWithDeptEntity } from ./entities/role-width-dept.entity; import { SysDeptEntity } from ../dept/entities/dept.entity; import { MenuService } from ../menu/menu.service; // ... 其他导入 Injectable() export class RoleService { private sysRoleEntityRep: RepositorySysRoleEntity; private sysRoleWithMenuEntityRep: RepositorySysRoleWithMenuEntity; private sysRoleWithDeptEntityRep: RepositorySysRoleWithDeptEntity; private sysDeptEntityRep: RepositorySysDeptEntity; constructor( Inject(DATA_SOURCE) private dataSource: DmdbDataSource, // 注入数据源 private readonly menuService: MenuService, ) { // 通过数据源获取每个实体的 Repository this.sysRoleEntityRep this.dataSource.getRepository(SysRoleEntity); this.sysRoleWithMenuEntityRep this.dataSource.getRepository(SysRoleWithMenuEntity); this.sysRoleWithDeptEntityRep this.dataSource.getRepository(SysRoleWithDeptEntity); this.sysDeptEntityRep this.dataSource.getRepository(SysDeptEntity); } // 业务方法... }4.3 改造说明解耦数据源DatabaseModule内部根据DB_TYPE配置MySQL 或 dm动态创建对应的DataSource实例Service 只需注入统一的DATA_SOURCE无需关心底层数据库类型。保持 TypeORM API 一致无论是 MySQL 还是达梦dataSource.getRepository()返回的都是标准 TypeORMRepository对象后续的save、find、update、createQueryBuilder等方法完全通用。实体无需修改实体中定义的字段名、类型装饰器如Column保持不变TypeORM 会根据驱动自动处理字段映射。仅当字段类型在达梦中不兼容时如tinyint才需调整实体详见第五章节。五、查询语法调整重点LIKE 参数化5.1 TypeORM 查询构建器兼容性TypeORM 的查询构建器QueryBuilder在达梦驱动下大部分语法自动转换但LIKE 语句必须使用参数化查询不能直接拼接字符串。原始问题代码在findAll方法中typescript// ❌ 错误写法直接拼接字符串达梦可能报错或存在 SQL 注入风险 if (query.roleName) { entity.andWhere(entity.roleName LIKE %${query.roleName}%); }✅ 改造后参数化查询typescript// ✅ 正确写法使用 :parameter 占位符 if (query.roleName) { entity.andWhere(entity.roleName LIKE :roleName, { roleName: %${query.roleName}% }); }5.2 完整的findAll方法改造示例typescriptasync findAll(query: ListRoleDto) { const entity this.sysRoleEntityRep.createQueryBuilder(entity); entity.where(entity.delFlag :delFlag, { delFlag: 0 }); if (query.roleName) { // 参数化 LIKE 查询 entity.andWhere(entity.roleName LIKE :roleName, { roleName: %${query.roleName}% }); } if (query.roleKey) { entity.andWhere(entity.roleKey LIKE :roleKey, { roleKey: %${query.roleKey}% }); } if (query.roleId) { entity.andWhere(entity.roleId :roleId, { roleId: query.roleId }); } if (query.status) { entity.andWhere(entity.status :status, { status: query.status }); } if (query.params?.beginTime query.params?.endTime) { // BETWEEN 也使用参数化TypeORM 自动处理 entity.andWhere(entity.createTime BETWEEN :start AND :end, { start: query.params.beginTime, end: query.params.endTime, }); } if (query.pageSize query.pageNum) { entity.skip(query.pageSize * (query.pageNum - 1)).take(query.pageSize); } const [list, total] await entity.getManyAndCount(); return ResultData.ok({ list, total }); }5.3 其他常见 SQL 差异如需原生查询若项目中有直接执行 SQL 的场景如使用query()方法需注意以下差异特性MySQL达梦8字符串拼接CONCAT(a, b)a || b或CONCAT(a, b)当前时间NOW()SYSDATE或CURRENT_TIMESTAMP随机排序RAND()DBMS_RANDOM.VALUE正则表达式REGEXPREGEXP_LIKE限制条数LIMIT nLIMIT n或ROWNUM n自动递增AUTO_INCREMENTIDENTITY(1,1)或序列改造示例随机排序typescript// MySQL 风格 await this.repository.query(SELECT * FROM sys_role ORDER BY RAND() LIMIT 1); // 达梦8兼容写法 await this.repository.query(SELECT * FROM sys_role ORDER BY DBMS_RANDOM.VALUE LIMIT 1);5.4 事务处理通用无需修改typescriptasync createWithTransaction(createRoleDto: CreateRoleDto) { const queryRunner this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const role await queryRunner.manager.save(SysRoleEntity, createRoleDto); // 其他关联操作... await queryRunner.commitTransaction(); return role; } catch (err) { await queryRunner.rollbackTransaction(); throw err; } finally { await queryRunner.release(); } }六、实体Entity兼容性说明6.1 原则尽量不修改实体TypeORM 在底层会根据数据库驱动自动映射字段类型。大多数情况下实体无需改动。但少数类型在达梦中不兼容需微调MySQL类型达梦8推荐类型实体调整方式tinyintSMALLINT将Column({ type: tinyint })改为Column({ type: smallint })datetimeTIMESTAMP保持Column({ type: timestamp })即可达梦支持jsonVARCHAR(4000)或CLOB改为Column({ type: varchar, length: 4000 })并在应用层处理 JSON 序列化textCLOB改为Column({ type: clob })默认值函数CURRENT_TIMESTAMP达梦中用SYSDATE需修改default: () SYSDATE6.2 示例将tinyint改为smallinttypescript// 修改前 Column({ type: tinyint, default: 0 }) roleSort: number; // 修改后达梦兼容 Column({ type: smallint, default: 0 }) roleSort: number;若不想修改实体也可在达梦建表时使用SMALLINT类型TypeORM 会忽略type定义中的提示仅用于生成 SQL但建议保持一致。七、数据初始化与迁移7.1 手动建表脚本示例sql-- 达梦8建表语句注意表名、字段名大写 CREATE TABLE SYS_ROLE ( ROLE_ID INT IDENTITY(1,1) PRIMARY KEY, ROLE_NAME VARCHAR(30) NOT NULL UNIQUE, ROLE_KEY VARCHAR(100) NOT NULL, ROLE_SORT SMALLINT DEFAULT 0, STATUS SMALLINT DEFAULT 1, DEL_FLAG CHAR(1) DEFAULT 0, CREATE_TIME TIMESTAMP DEFAULT SYSDATE ); COMMENT ON TABLE SYS_ROLE IS 角色信息表; COMMENT ON COLUMN SYS_ROLE.ROLE_ID IS 角色ID; COMMENT ON COLUMN SYS_ROLE.ROLE_NAME IS 角色名称; COMMENT ON COLUMN SYS_ROLE.ROLE_KEY IS 角色权限字符串; COMMENT ON COLUMN SYS_ROLE.ROLE_SORT IS 显示顺序; COMMENT ON COLUMN SYS_ROLE.STATUS IS 状态1正常 0停用; COMMENT ON COLUMN SYS_ROLE.DEL_FLAG IS 删除标志; COMMENT ON COLUMN SYS_ROLE.CREATE_TIME IS 创建时间;7.2 数据迁移工具若已有 MySQL 数据需迁移至达梦8可使用达梦官方DTS数据迁移工具支持从 MySQL 直接迁移。八、常见问题与解决方案问题现象可能原因解决方案连接超时达梦服务未启动或端口错误检查达梦服务状态确认端口5236开放表名/字段名大小写错误达梦默认转大写实体中小驼峰未加引号在实体中使用双引号保留大小写如Column({ name: userId })自增主键插入失败达梦IDENTITY列需显式指定确保PrimaryGeneratedColumn配置正确或关闭synchronize手动建表中文乱码字符集不匹配达梦建库时使用UTF-8连接字符串指定编码LIKE查询无结果直接拼接字符串导致语法错误改用参数化查询如LIKE :keywordBETWEEN日期查询报错日期格式不兼容确保传入的日期字符串格式为YYYY-MM-DD HH:MM:SS或使用TO_DATE转换九、总结通过本教程你将掌握在 Nest.js 项目中安装正确的达梦8驱动dmdb和typeorm-dm。改造dev.yml配置文件支持 MySQL 和达梦8动态切换。编写DatabaseModule根据配置动态创建数据源。Service 层改造通过Inject(DATA_SOURCE)注入数据源使用getRepository()获取 Repository替代原有的InjectRepository()。查询语句参数化改造特别是LIKE语句。实体兼容性处理按需修改不兼容类型。使用迁移脚本管理表结构避免synchronize风险。完成以上步骤后你的 Nest.js 项目即可无缝切换至达梦8数据库同时保留对 MySQL 的兼容性实现国产化数据库平滑迁移。点赞关注留言有完整代码