一、前言
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()返回的都是标准 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 方法改造示例
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() |
SYSDATE 或 CURRENT_TIMESTAMP |
| 随机排序 | RAND() |
DBMS_RANDOM.VALUE |
| 正则表达式 | REGEXP |
REGEXP_LIKE |
| 限制条数 | LIMIT n |
LIMIT n 或 ROWNUM <= 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 转换 |
九、总结
通过本教程,你将掌握:
-
在 Nest.js 项目中安装正确的达梦8驱动(
dmdb和typeorm-dm)。 -
改造
dev.yml配置文件,支持 MySQL 和达梦8动态切换。 -
编写
DatabaseModule,根据配置动态创建数据源。 -
Service 层改造 :通过
@Inject('DATA_SOURCE')注入数据源,使用getRepository()获取 Repository,替代原有的@InjectRepository()。 -
查询语句参数化改造(特别是
LIKE语句)。 -
实体兼容性处理(按需修改不兼容类型)。
-
使用迁移脚本管理表结构,避免
synchronize风险。
完成以上步骤后,你的 Nest.js 项目即可无缝切换至达梦8数据库,同时保留对 MySQL 的兼容性,实现国产化数据库平滑迁移。
点赞,关注,留言,有完整代码!