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 14+,npm/yarn 包管理工具

界面截图


二、项目基础准备

2.1 原始项目结构(MySQL版)

text

复制代码
src/
├── config/                     # 配置文件
│   ├── index.ts               # 统一配置导出
│   └── dev.yml                # 开发环境配置(含数据库)
├── database/
│   └── database.module.ts     # 自定义动态数据库模块
├── module/                    # 业务模块
│   ├── system/                # 系统管理模块
│   │   ├── role/              # 角色管理
│   │   │   ├── entities/      # 实体
│   │   │   ├── dto/           # 数据传输对象
│   │   │   └── role.service.ts
│   │   └── ...
│   └── ...
├── common/                    # 公共模块
├── app.module.ts              # 根模块
└── main.ts

2.2 安装达梦8驱动

重要 :不要安装 @nestjs/typeorm,请按照以下依赖安装:

bash

复制代码
npm install @nestjs/config typeorm mysql2 dmdb typeorm-dm reflect-metadata
  • dmdb:达梦8官方 Node.js 驱动(替代 dm8-nodejs

  • typeorm-dm:TypeORM 对达梦数据库的适配器

  • reflect-metadata:TypeORM 运行时的依赖


三、数据库配置改造

3.1 原 MySQL 配置(dev.yml)

yaml

bash 复制代码
db:
  mysql:
    host: 'localhost'
    username: 'userA' #修改为自己的账号
    password: 'xxxxx' #修改为自己的密码
    database: 'dmtest'
    port: 33061
    charset: 'utf8mb4'
    synchronize: true
    logging: false

3.2 新增达梦8配置(dev.yml)

在原有配置基础上添加 dm 节点:

bash 复制代码
# 数据库配置
DB_TYPE: dm
db:
  dm:                     # 新增达梦配置
    host: 'localhost'
    port: 5236
    username: 'userA'    #修改为自己的账号 
    password: 'XXXXXX'    #修改为自己的密码
    schema: 'DMTEST'      # 必须指定 schema
    synchronize: true
    logging: false
复制代码
 

3.3 动态数据源模块(database.module.ts)

创建自定义 DatabaseModule,根据 DB_TYPE 动态选择驱动。核心代码(已提供):

typescript

bash 复制代码
import { 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 | Promise<DatabaseOptions>;
  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.get<string>('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.get<string>('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.ts)

typescript

bash 复制代码
import { 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() 装饰器注入 Repository:

typescript

bash 复制代码
// 原有方式(仅适用于固定数据库)
@Injectable()
export class RoleService {
  constructor(
    @InjectRepository(SysRoleEntity)
    private roleRepository: Repository<SysRoleEntity>,
    // ...
  ) {}
}

这种写法将 Repository 与具体数据库绑定,无法动态切换数据源。为了实现多数据库兼容,我们改为自定义数据源注入方式。

4.2 改造后的 Service 构造函数

关键改造点 :通过 @Inject('DATA_SOURCE') 获取动态数据源实例,然后使用 dataSource.getRepository() 获取各个实体的 Repository。

typescript

bash 复制代码
import { 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: Repository<SysRoleEntity>;
  private sysRoleWithMenuEntityRep: Repository<SysRoleWithMenuEntity>;
  private sysRoleWithDeptEntityRep: Repository<SysRoleWithDeptEntity>;
  private sysDeptEntityRep: Repository<SysDeptEntity>;

  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() 返回的都是标准 TypeORM Repository 对象,后续的 savefindupdatecreateQueryBuilder 等方法完全通用。

  • 实体无需修改 :实体中定义的字段名、类型装饰器(如 @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 方法改造示例

typescript

bash 复制代码
async 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
当前时间 NOW() SYSDATECURRENT_TIMESTAMP
随机排序 RAND() DBMS_RANDOM.VALUE
正则表达式 REGEXP REGEXP_LIKE
限制条数 LIMIT n LIMIT nROWNUM <= n
自动递增 AUTO_INCREMENT IDENTITY(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 事务处理(通用,无需修改)

typescript

bash 复制代码
async 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推荐类型 实体调整方式
tinyint SMALLINT @Column({ type: 'tinyint' }) 改为 @Column({ type: 'smallint' })
datetime TIMESTAMP 保持 @Column({ type: 'timestamp' }) 即可,达梦支持
json VARCHAR(4000)CLOB 改为 @Column({ type: 'varchar', length: 4000 }),并在应用层处理 JSON 序列化
text CLOB 改为 @Column({ type: 'clob' })
默认值函数 CURRENT_TIMESTAMP 达梦中用 SYSDATE,需修改 default: () => 'SYSDATE'

6.2 示例:将 tinyint 改为 smallint

typescript

复制代码
// 修改前
@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 :keyword
BETWEEN 日期查询报错 日期格式不兼容 确保传入的日期字符串格式为 YYYY-MM-DD HH:MM:SS,或使用 TO_DATE 转换

九、总结

通过本教程,你将掌握:

  1. 在 Nest.js 项目中安装正确的达梦8驱动(dmdbtypeorm-dm)。

  2. 改造 dev.yml 配置文件,支持 MySQL 和达梦8动态切换。

  3. 编写 DatabaseModule,根据配置动态创建数据源。

  4. Service 层改造 :通过 @Inject('DATA_SOURCE') 注入数据源,使用 getRepository() 获取 Repository,替代原有的 @InjectRepository()

  5. 查询语句参数化改造(特别是 LIKE 语句)。

  6. 实体兼容性处理(按需修改不兼容类型)。

  7. 使用迁移脚本管理表结构,避免 synchronize 风险。

完成以上步骤后,你的 Nest.js 项目即可无缝切换至达梦8数据库,同时保留对 MySQL 的兼容性,实现国产化数据库平滑迁移。

点赞,关注,留言,有完整代码!

相关推荐
Lyyaoo.6 小时前
Spring Boot Validation 声明式参数校验
spring boot·后端·mysql
你别说话了7 小时前
Vue项目解决跨域
前端·javascript·vue.js
指尖时光.7 小时前
Three.js 超全入门综合案例
开发语言·javascript·ecmascript
唐青枫7 小时前
http-server:别再双击 index.html,一条命令把目录变成网站
前端·javascript
oradh7 小时前
Oracle闪回技术操作总结
数据库·oracle·oracle闪回技术操作总结·oracle闪回·flashback技术
南城以南溫暖如初1478 小时前
外卖CPS实战指南:从技术选型到运营落地全流程解析
java·spring boot·mysql·架构·vue
ltl8 小时前
RocksDB 并发 Compaction 与 Rate Limiter
数据库
闻道且行之9 小时前
图片处理助手|泊松融合原理 + C++ 工程实现,seamlessClone 三模式一次讲透
数据库·c++·人工智能·opencv
学习星球10 小时前
Solid.js 实战:拆解官方 RealWorld 项目
开发语言·javascript·vue.js
夏炳辉.10 小时前
PostgreSQL 高可用集群核心配置参数全解:从原生流复制到 Patroni 企业级方案
数据库·postgresql