15、服务端实战:数据库工具封装

news2024/12/23 16:14:16

在了解完 NestJS 的基础配置之后,服务端的内容将引来一个比较重要的环节:数据库

因为数据库的内容比较多,所以相关内容将分为两个章节来展开讨论:

  • 数据库工具封装 - 将封装统一的数据库操作工具类,方便后期开发于集成
  • 数据库实操 - 结合实际项目讲述 TypeORM 的使用以及 MySQL 与 Mongoose 的示例

TypeORM


日常对数据库的操作需要借助于 SQL,至少需要掌握基础的 SQL 语法就有建表、增删改查等。但如果想要在代码中直接实现对数据库的操作,就需要去写大量 SQL ,这在可读性、维护性及开发体验上都非常糟糕

于是 ORM 框架应运而生,这类的框架是为了解决面向对象与关系数据库存在的互不匹配的现象,把面向 SQL 开发转变为面向对象开发,开发不需要关注底层实现细节,而是以操作对象的模式使用数据库。

虽然市面上也有其他不错的 ORM 框架,比如 Sequelize、Prisma 等,但 TypeORM 使用 TypeScript 编写,在 NestJS 框架下运行得非常好,也是 NestJS 首推的 ORM 框架,有开箱即用的 @nestjs/typeorm 软件包支持。

这一章对于很多偏前端的同学来说,会稍微有点复杂,但借助于 ORM 框架来说,并非是无从下手。

封装

NestJS 使用 TypeORM 的方式有两种。一种是 NestJS 提供的 @nestjs/typeorm 集成包,可以导出 TypeOrmModule.forRoot 方法来连接数据库,同时可以使用 ormconfig.json 将数据库链接配置项剥离。另外一种是直接使用 typeorm,自由封装 Providers 导入使用。

两种方案各有优缺点,使用 @nestjs/typeorm 集成的方案较为简便,但自建的业务脚手架需要两种数据库保证在开发中体验一致性,此外之前已经自定义了全局环境变量的配置,没有必要再多一个 ormconfig.json 的配置来增加额外理解成本,所以接下来我们将使用第二种方案来连接数据库。

由于我们已经采用了 Monorepo 的开发模式且已经使用了 Lib,所以将封装工具类全部收敛进 libs/comm,所以这一步开始都是基于 libs/comm 工具库。

第一步:跟之前一样,为了使用 TypeORM,先安装以下依赖:

$ pnpm add typeorm mysql2 mongoose -w

第二步:在 dev.yaml 中添加数据库配置参数。

MONGODB_CONFIG:
  name: "ignition_test"              # 自定义次数据库链接名称
  type: mongodb                      # 数据库链接类型
  url: "mongodb://localhost:27017"   # 数据库链接地址
  username: "root"                   # 数据库链接用户名
  password: "123456"                 # 数据库链接密码
  database: "fast_gateway_test"      # 数据库名
  entities: "mongo"                  # 自定义加载类型
  logging: false                     # 数据库打印日志
  synchronize: true                  # 是否开启同步数据表功能
MYSQL_CONFIG:
  name: "material_test"
  type: "mysql"
  host: "121.36.198.10"
  port: 3306
  username: "root"
  password: "123456"
  database: "material_test"
  entities: "mysql"
  logging: true
  synchronize: true

以上是数据库连接的必要参数,其他的参数可以参考文档根据需求添加,例如 retryAttempts(重试连接数据库的次数)、keepConnectionAlive(应用程序关闭后连接是否关闭) 等配置项。

MongoDB 是无模式的,所以即使在配置参数开启了 synchronize,启动项目的时候也不会去数据库创建对应的表,所以不用奇怪,并没有出错,但 Mysql 在每次应用程序启动时自动同步表结构。为了避免意外 synchronize 这个配置参数一定不要在生产环境开启,每次服务启动的时候都会同步数据库表结构,如果出现主键不同、表结构不等的情况下,会直接进行数据库删表操作,生产环境一定要关闭!

第三步:新建 lib/comm/src/database/database.providers.ts

import { DataSource } from 'typeorm';

import { getConfig } from '../utils/index';
import { NamingStrategy } from './naming.strategies';

const { MONGODB_CONFIG, MYSQL_CONFIG } = getConfig();

const MONGODB_DATABASE_CONFIG = {
  ...MONGODB_CONFIG,
  entities: [`dist/**/*.${MONGODB_CONFIG.entities}.entity.js`]
};

const MYSQL_DATABASE_CONFIG = {
  ...MYSQL_CONFIG,
  namingStrategy: new NamingStrategy(),
  entities: [`dist/**/*.${MYSQL_CONFIG.entities}.entity.js`]
};

const MONGODB_DATA_SOURCE = new DataSource(MONGODB_DATABASE_CONFIG);
const MYSQL_DATA_SOURCE = new DataSource(MYSQL_DATABASE_CONFIG);

// 数据库注入
export const DatabaseProviders = [
  {
    provide: 'MONGODB_DATA_SOURCE',
    useFactory: async () => {
      if (!MONGODB_DATA_SOURCE.isInitialized) await MONGODB_DATA_SOURCE.initialize();
      return MONGODB_DATA_SOURCE;
    },
  },
  {
    provide: 'MYSQL_DATA_SOURCE',
    useFactory: async () => {
      if (!MYSQL_DATA_SOURCE.isInitialized) await MYSQL_DATA_SOURCE.initialize();
      return MYSQL_DATA_SOURCE;
    },
  },
];

注意:创建的实体类文件命名后缀统一为 entity.ts,但为了区分不同的数据库扫描,加了 MYSQL_CONFIG.entities 来区分不同的数据库类型,同样当我们需要使用多数据库的时候,可以依照这种模式来新增不同的数据库。

其中针对于 MySQL 理论上都需要遵守驼峰命名规范,需要对一些不太规范的实体类名进行转换,所以会比 Mongoose 对一个配置 naming.strategies.ts

/*
 * @Author: Cookie
 * @Description: 添加数据库表与字段驼峰转下划线功能
 */

import { DefaultNamingStrategy, NamingStrategyInterface } from 'typeorm';
import { snakeCase } from 'typeorm/util/StringUtils';

export class NamingStrategy
  extends DefaultNamingStrategy
  implements NamingStrategyInterface {

  tableName(className: string, customName: string): string {
    return customName ? customName : snakeCase(className);
  }

  columnName(
    propertyName: string,
    customName: string,
    embeddedPrefixes: string[],
  ): string {
    return (
      snakeCase(embeddedPrefixes.concat('').join('_')) +
      (customName ? customName : snakeCase(propertyName))
    );
  }

  relationName(propertyName: string): string {
    return snakeCase(propertyName);
  }

  joinColumnName(relationName: string, referencedColumnName: string): string {
    return snakeCase(relationName + '_' + referencedColumnName);
  }

  joinTableName(
    firstTableName: string,
    secondTableName: string,
    firstPropertyName: string,
    secondPropertyName: string,
  ): string {
    return snakeCase(
      `${firstTableName}_${firstPropertyName.replace(
        /\./gi,
        '_',
      )}_${secondTableName}`,
    );
  }

  joinTableColumnName(
    tableName: string,
    propertyName: string,
    columnName?: string,
  ): string {
    return snakeCase(
      tableName + '_' + (columnName ? columnName : propertyName),
    );
  }

  classTableInheritanceParentColumnName(
    parentTableName: any,
    parentTableIdPropertyName: any,
  ): string {
    return snakeCase(parentTableName + '_' + parentTableIdPropertyName);
  }

  eagerJoinRelationAlias(alias: string, propertyPath: string): string {
    return alias + '__' + propertyPath.replace('.', '_');
  }

}

第四步:新建 database.module.ts

import { Global, Module } from '@nestjs/common';
import { DatabaseProviders } from './database.providers';

@Global()
@Module({
  providers: [...DatabaseProviders],
  exports: [...DatabaseProviders],
})

export class DatabaseModule { }

至此我们已经封装了 MongoDB 与 MySQL 的 Provider,作为统一的数据库操作类提供给其他的服务调用,但这其中也有一些缺陷,例如实体类的注册是依赖于静态路径收集注册,也就是采用此方式的话,不太适用于 Webpack 热更新与 Monorep 的方案,所以想使用其他方案的话,就不要采取这个模式。

使用

从这一步开始都是基于 app/low-code-test 实际服务端项目

第一步:注册实体,创建 src/user/user.mongo.entity.ts

import { Entity, Column,  ObjectIdColumn } from 'typeorm';

@Entity()
export class User {
  @ObjectIdColumn()
  id?: number;

  @Column({ default: null })
  name: string;
}

在 MongoDB 里面使用的是 ObjectIdColumn 作为类似 MySQL 的自增主键,来保证数据唯一性,只是类似,并不是跟普通自增主键一样会递增,把它看成 uuid 类似即可。

第二步:创建 user.providers.ts

import { User } from './user.mongo.entity';

export const UserProviders = [
  {
    provide: 'USER_REPOSITORY',
    useFactory: async (AppDataSource) => await AppDataSource.getRepository(User),
    inject: ['MONGODB_DATA_SOURCE'],
  },
];

第三步:创建 user.service.ts,新增添加用户 service

import { In, Like, Raw, MongoRepository } from 'typeorm';
import { Injectable, Inject } from '@nestjs/common';
import { User } from './user.mongo.entity';

@Injectable()
export class UserService {
  constructor(
    @Inject('USER_REPOSITORY')
    private userRepository: MongoRepository<User>
  ) { }

  createOrSave(user) {
   return this.userRepository.save(user)
  }
}

第四步:创建 user.dto.ts,插件

import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
export class AddUserDto {
  @ApiProperty({ example: 123, })
  id?: string;

  @ApiProperty({ example: 'cookie' })
  @IsNotEmpty()
  name: string;

  @ApiProperty({ example: 'cookieboty@qq.com' })
  @IsNotEmpty()
  email: string;

  @ApiProperty({ example: 'cookieboty' })
  @IsNotEmpty()
  username: string;
}

有的同学可能会问,DTO(Data Transfer Object) 与 Entities 的区别,毕竟两个文件都很类似。

首先它们都用于表示数据,但在设计和用途方面有所不同:

  • DTO 是一种数据传输对象,用于在不同的层之间传输数据。它通常用于将数据从数据库层传输到应用程序层,或将数据从应用程序层传输到前端层。DTO 的设计目的是为了最大程度地减少数据传输的开销,通常只包含必要的数据字段,而不包含任何业务逻辑或操作方法。

  • Entities 是一种实体对象,用于表示应用程序中的业务对象或领域对象。它通常用于表示数据库中的表或文档,或者表示应用程序中的业务对象。Entities 的设计目的是为了封装业务逻辑和操作方法,以便在应用程序中进行操作和处理。

第五步:创建 user.controller.ts,添加新增用户的 http 请求方法:

import { Controller, Post, Body, Query, Get } from '@nestjs/common';
import { UserService } from './user.service';
import { AddUserDto } from './user.dto';
import { ApiOperation, ApiTags } from '@nestjs/swagger';

@ApiTags('用户')
@Controller('user')
export class UserController {
  constructor(
    private readonly userService: UserService,
  ) { }

  @ApiOperation({
    summary: '新增用户',
  })
  @Post('/add')
  create(@Body() user: AddUserDto) {
    return this.userService.createOrSave(user);
  }
}

第六步:创建 user.module.ts,将 controllerprovidersservice 等都引入后,切记将 user.module.ts 导入 app.module.ts 后才会生效,这一步别忘记了 :

import { Module } from '@nestjs/common';
import { DatabaseModule } from '@/common/database/database.module';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { UserProviders } from './user.providers';
import { FeishuController } from './feishu/feishu.controller';
import { FeishuService } from './feishu/feishu.service';

@Module({
  imports: [
    DatabaseModule
  ],
  controllers: [
    FeishuController,
    UserController
  ],
  providers: [...UserProviders, UserService, FeishuService],
  exports: [UserService],
})
export class UserModule { }

完成上述所有步骤之后,此时打开 Swagger 文档可以看到,已经创建好了 /api/user/add 新增用户的 http 接口:

点击测试能正常得到如下返回值的话,则代表数据插入成功,功能正常:

查询数据库所得如下所示: 

写在最后


本章主要介绍了如何封装一个数据库操作类,与直接使用 NestJS 自带的 TypeORM 工具库不同的是,我们是自己封装了一套,这样的好处是自定义程度会更高,但与之而来就是很多特性我们也就无法再使用,如果有需求的话就需要自己重新开发。

如果不喜欢自己折腾的话,可以看下 @nestjs/typeorm 的使用,结合 NetsJS 的官方文档上手也不慢。

但对于小册来说,我希望带来的是不一样的视角与实战的经验而不是文档的转述与解读,有一定自学能力的同学其实看文档也就足够了。

无论选择哪一种方案,下一章,我们将学习数据库实操的相关内容。

如果你有什么疑问,欢迎在评论区提出。 👏

15 服务端实战:数据库工具封装

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/723669.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

识别肿瘤内微生物的生物信息学工具—MEGA

谷禾健康 已有研究证明宿主微生物在癌症预防和治疗反应中的关键作用&#xff0c;了解宿主微生物和癌症之间的相互作用&#xff0c;可以推动癌症诊断和微生物治疗(即用微生物作为药物)。 然而肿瘤内微生物组数据通常是复杂的&#xff0c;想要厘清相互关系也是极为困难的&#xf…

低代码平台——少量编码即可快速生成应用程序

低代码平台&#xff0c;即无需编码或通过少量代码就可以快速生成应用程序的开发平台。 低代码平台面向的是IT或者平民程序员&#xff0c;解决传统软件开发模式带来的周期长、成本高等问题&#xff0c;客户群体主要为软件开发公司或者拥有IT的中大型企业。而零代码&#xff08;N…

API接口测试工具的几个特色

API接口测试工具在软件开发过程中起着举足轻重的作用。它们帮助测试人员快速发现和解决API接口的问题&#xff0c;并确保系统的稳定性和性能。本文将介绍API接口测试工具的几个特色&#xff0c;以及为什么它们对测试人员来说非常重要。 首先&#xff0c;API接口测试工具的一个特…

【uniapp】学习之【生命周期】

uniapp生命周期 uni-app框架的生命周期分为两种 &#xff1a; 应用中的生命周期 和 页面内的生命周期 uni-app 应用生命周期 uni-app 页面生命周期

微信公众号本地开发调试 - 无公网IP —— 内网穿透

文章目录 前言1. 配置本地服务器2. 内网穿透2.1 下载安装cpolar内网穿透2.2 创建隧道 3. 测试公网访问4. 固定域名4.1 保留一个二级子域名4.2 配置二级子域名 5. 使用固定二级子域名进行微信开发 前言 在微信公众号开发中&#xff0c;微信要求开发者需要拥有自己的服务器资源来…

软考:中级软件设计师:进程死锁,死锁的预防和避免,银行算法家,

软考&#xff1a;中级软件设计师:进程死锁 提示&#xff1a;系列被面试官问的问题&#xff0c;我自己当时不会&#xff0c;所以下来自己复盘一下&#xff0c;认真学习和总结&#xff0c;以应对未来更多的可能性 关于互联网大厂的笔试面试&#xff0c;都是需要细心准备的 &…

STM32:使用RS485和多摩川编码器通信

本文主要讲使用STM32F767和绝对式多摩川TS5700N8501编码器通信的流程和注意事项。 首先使用STM32CubeMX生成RS485驱动部分功能代码&#xff0c;注意该款编码器的波特率是2.5Mbps。 注意使能的GPIO可以使用其他管脚&#xff0c;我们的主控板使用的是PA8。前期可以这么配置。 配…

zabbix的安装

前提 作为一个运维&#xff0c;需要会使用监控系统查看服务器系统性能、应用服务状态和网站流量指标等&#xff0c;利用监控系统的数据去了解网站上线发布的结果和健康状态。 利用一个优秀的监控软件&#xff0c;我们可以: ●通过一个友好的界面进行浏览整个网站所有的服务器…

Linux—实操篇:用户管理

1、基本介绍 Linux系统是一个多用户多任务的操作系统&#xff0c;任何一个要使用系统资源的用户&#xff0c;都必须首先向系统管理员申请一个 账号&#xff0c;然后以这个账号的身份进入系统。 2、添加用户 基本语法&#xff1a; useradd 用户名 细节说明&#xff1a; 1、…

【Kafka】Kafka基础操作笔记

【Kafka】Kafka基础操作笔记 文章目录 【Kafka】Kafka基础操作笔记1. 两种模式1.1 点对点模式1.2 发布/订阅模式 2. 基础架构3. Topic命令行操作3.1 查看 Topic 操作3.2 创建 Topic3.3 查看所有 Topic3.4 查看 Topic 的详情3.5 修改分区数3.6 删除 Topic 1. 两种模式 Kafka作为…

Linux设备驱动基础04:Linux设备驱动中的互斥与同步(部分)

1 并发与竞态 1.1 竞态概念 1. 并发&#xff08;Concurrency&#xff09;是指多个执行单元在同一时间段内执行&#xff08;但并不一定在同一时刻&#xff09;&#xff0c;而并发的执行单元对共享资源&#xff08;包括硬件资源和软件资源&#xff09;的访问就会导致竞态&#…

期货程序化交易软件文华财经和无限易那个成交速度快?自编软件能不能接CTP通道?

期货程序化交易软件是指可以根据预设的策略和条件&#xff0c;自动执行买卖指令的软件。期货程序化交易软件有很多种&#xff0c;不同的软件有不同的特点和优劣&#xff0c;选择哪一种要根据自己的需求和偏好。 期货程序化交易是指将交易思路用编程语言编写成计算机可以执行的指…

Zabbix 6.0 介绍及部署

目录 一、Zabbix 6.0 介绍1. 简介2. **利用一个优秀的监控软件带来的好处**3. **zabbix 6.0 的功能组件**4.zabbix 监控原理 二、Zabbix 6.0 部署 一、Zabbix 6.0 介绍 1. 简介 Zabbix 是由 Alexei Vladishev 创建&#xff0c;目前是由 Zabbix SIA 在持续开发和提供支持。zab…

2023秋招,软件测试面试八股文(有答案版),金九银十季,涨薪涨薪

最近呢有很多的小伙伴问我有没有什么软件测试的面试题&#xff0c;由于笔者之前一直在忙工作上的事情&#xff0c;没有时间整理面试题&#xff0c;刚好最近休息了一下&#xff0c;顺便整理了一些面试题&#xff0c;现在就把整理的面试题分享给大家&#xff0c;希望可以帮助到各…

途乐证券|指数反弹上行周期仍未结束 建议继续做多

股票的开盘价通常可以表示商场的情绪&#xff0c;一起还能影响投资者进行决策。那么股票低开好仍是高开好&#xff1f;股票低开高走再回落是什么意思&#xff1f;下面为我们预备了相关内容&#xff0c;以供参考。 股票低开好仍是高开好&#xff1f; 股票高开比较好。股票高开阐…

kali安装安全漏洞扫描工具Snyk

进kali&#xff0c;sudo su进入root用户 apt install npm 下载npm sudo npm install -g snyk 安装snyk snyk auth进行授权&#xff0c;会自动跳转到一个网页&#xff0c;我选的是github授权。 如果github上不去的话&#xff0c;物理机上面科学上网开个全局就行了&#xff0c;…

WWW(URL,HTTP,HTML)

1.WWW 万维网(World Wide Web&#xff0c;WWW)&#xff0c;是一个规模巨大、可以互联的资料空间。该资料空间的资源依靠URL进行定位&#xff0c;通过HTTP协议传送给使用者&#xff0c;又由HTML来进行文档的展现。由定义可以知道WWW的核心由三个主要标准构成:URL、HTTP、HTML。(…

软件设计模式与体系结构-设计模式-行为型软件设计模式-访问者模式

目录 二、访问者模式概念代码类图实例一&#xff1a;名牌运动鞋专卖店销售软件实例二&#xff1a;计算机部件销售软优缺点适用场合课程作业 二、访问者模式 概念 对于系统中的某些对象&#xff0c;它们存储在同一个集合中&#xff0c;具有不同的类型对于该集合中的对象&#…

力扣算法练习(三)

目录 1. N 字形变换&#xff08;6&#xff09; 题解一&#xff08;力扣官方解析力扣&#xff09; 题解二&#xff08;官方解法&#xff09; 题解三&#xff08;力扣&#xff09; 2. 整数反转&#xff08;7&#xff09; 题解一 题解二 题解三&#xff08;官方解析&#…

车辆在刹车不及时导致与行人发生碰撞事故,产生出险记录

车辆在刹车不及时导致与行人发生碰撞事故&#xff0c;是一种常见的交通事故。当发生此类事故时&#xff0c;车主需要及时处理保险理赔事宜&#xff0c;同时也需要了解车辆出险、理赔、事故记录情况&#xff0c;以便更好地维护车辆和自身权益。为方便车主查询车辆出险、理赔、事…