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 的兼容性,实现国产化数据库平滑迁移。

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

相关推荐
qq_416018722 小时前
使用Python处理计算机图形学(PIL/Pillow)
jvm·数据库·python
Mr数据杨2 小时前
【通用Vue】学生管理模块通用功能
javascript·vue.js·ecmascript
前端小菜鸟也有人起2 小时前
vue中is的作用和用法
前端·javascript·vue.js
m0_502724952 小时前
vue3在线预览excel表格
javascript·vue.js·excel
酉鬼女又兒2 小时前
零基础入门前端JavaScript 基础语法详解(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·javascript·chrome·蓝桥杯
该怎么办呢2 小时前
packages\engine\Source\Core\Cartesian3.js
前端·javascript·cesium
颜酱2 小时前
吃透回溯算法:从框架到实战
javascript·后端·算法
看我干嘛!2 小时前
在Windows上安装MySQL的两种方法
数据库·mysql
专注API从业者2 小时前
淘宝商品详情 API 的 Webhook 回调机制设计与实现:实现数据主动推送
大数据·前端·数据结构·数据库