Vue3+NestJS实现权限管理系统(二):Docker及数据库的使用

1,265 阅读9分钟

NestJS 作为一个后端框架与数据库打交道是必不可少的,它是如何来连接数据库以及如何操作数据库的呢?本篇文章将以 MySql和Redis 为例来教大家如何使用 NestJS 操作数据库

Docker

什么是 docker

Docker 是一种开源的容器化平台,可以让开发者打包他们的应用程序及其所有依赖项,并将其作为一个独立的容器运行。这样可以确保应用程序在任何环境中都能以相同的方式运行,提高了开发、部署和运维的效率。

Docker 翻译成中文就是船坞的意思

image.png

而船坞上的一个个集装箱代表的就是容器,我们可以将它们运送到任何环境中使用

接下来我们看一下 Docker 如何使用

docker 的使用

首先我们来到 docker 的官网,下载一个桌面化工具docker-desktop,下载完成后根据它的提示一步步安装,安装完成后我们就能使用 docker 啦

启动 docker-desktop 我们会看到如下界面

image.png

其中 images 就是镜像,containers 则是我们上面提到过的容器。那么它们到底是个什么东西呢?如果你学过面向对象编程,那么我这么解释你一定会明白它们是什么

你可以把images看作是一个类,而containers则是这个类实例化出来的对象,也就是说一个镜像images可以创建出多个容器containers,同时这些容器之间互不干扰。

在 docker 中,镜像(Image)是 Docker 容器的只读模板,包含了运行容器所需的文件系统、库和设置,镜像可以从 Docker Hub 等仓库中获取,也可以通过 Dockerfile 自定义构建。

比如在docker-desktop中我们可以之间搜索下载镜像,当然这需要魔法

image.png

如果你没有使用魔法的化,可以之间打开电脑命令行,运行docker pull xxx就可以拉取你想要的镜像,比如我们拉取一个 mysql 镜像: image.png

然后你就可以回到 desktop 中查看到你拉取的 mysql 镜像了

image.png

或者使用命令: docker images也可以查看镜像

image.png

有了镜像之后我们就可以使用镜像创建容器了,直接点击镜像旁边的小三角run

image.png

其中Host port是容器映射到宿主机也就是我们本机的端口(实际连接数据库使用的端口,我这里使用的是 3306)。

volumes 则是数据卷,它是防止数据库中数据丢失的,比如数据库中的一些表什么的都会存放在/var/lib/mysql中,我们将本地目录F:\mysql\db作为数据卷挂载这个目录,这样当我们换一个 Mysql 容器时再挂载一次数据就不会丢失

Environment variables 是环境变量的设置,这里我们设置了数据库的密码MYSQL_ROOT_PASSWORD

创建完成之后就可以看到我们的容器了,点击运行容器就启动了,这样就可以连接这个数据库啦。为例方便查看数据库,我们可以使用 Mysql 可视化工具连接,我选择的是免费的 workbench

到这里我们就了解了 docker 的基本操作,当然 docker 中还有很多内容需要学习,我会在项目部署那一章节进行详细的介绍,譬如 dockerfile,docker-compose 等等,接下来让我们学习 NestJS 中如何连接 Mysql 吧

NestJS 连接 mysql

NestJS 链接 mysql 需要先安装对应的包@nestjs/typeormmysql2,typeorm 可以让对数据库的 sql 操作映射成对象的操作

npm install @nestjs/typeorm typeorm mysql2 -S

安装完之后我们来到 app.module.ts 中进行数据库的配置,引入 TypeOrmModule 调用 forRoot 方法配置 MySql

同时我们需要一个 MySql 可视化工具,这里我使用了 workbench,并且新建了一个名为 fs_admin 的数据库,然后回到我们的项目中就可以连接了

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { UserModule } from "./user/user.module";
import { TypeOrmModule } from "@nestjs/typeorm";
@Module({
  imports: [
    UserModule,
    TypeOrmModule.forRoot({
      type: "mysql",
      host: "localhost",
      port: 3306, // 端口号
      username: "root", // 用户名
      password: "root", // 密码
      database: "fs_admin", //数据库名
      entities: [], //数据库对应的Entity
      synchronize: true, //是否自动同步实体文件,生产环境建议关闭
      connectorPackage: "",
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

启动项目,如果没有报错说明 MySql 连接成功了。数据库创建表在entities中操作就行了,它会将一个类映射为数据库中的表。比如我们在 user 下entities/user.entity.ts导出一个 User 类,然后在app.module.ts中导入

image.png

启动项目,这时候你就可以在workbench中会看到数据库中多了一张User

image.png

数据库操作

接下来我们看一下如何对数据库进行操作,还是以 User 表为例,首先需要在user.module.ts中导入 User 实体

import { Module } from "@nestjs/common";
import { UserService } from "./user.service";
import { UserController } from "./user.controller";
import { User } from "./entities/user.entity";
import { TypeOrmModule } from "@nestjs/typeorm";
@Module({
  controllers: [UserController],
  providers: [UserService],
  imports: [TypeOrmModule.forFeature([User])],
})
export class UserModule {}

然后就可以在user.service.ts进行注入使用了,比如在 create 函数中创建一条数据

import { HttpException, Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { Repository } from 'typeorm';
@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  async create(createUserDto: CreateUserDto) {
    return await this.userRepository.save(createUserDto);
  }
}

user.controller.ts中调用 create 方法并返回

image.png

最后使用 apifox 进行接口调用,传入 username 和 password image.png

这时就会发现数据库中多了一条数据 image.png

同样的,如果你想查询数据库可以调用find方法

//查询所有
this.userRepository.find();
//查询指定
this.userRepository.findOne({ where: { id: id } });

删除调用delete

this.userRepository.delete({ username: "xxx" });

更新调用update

this.userRepository.update({ id: id }, updateUserDto);

配置文件

一般来说数据库以及其它一些包含了一些敏感信息的配置不宜写在代码中提交到远程仓库中,并且这些配置写在业务代码中也不好维护。

所以我们可以将配置写在配置文件中,然后提交代码的时候将生产环境的配置文件忽略就行了,这里我们新建.env 和.env.prod 两个文件分别存放开发与生产环境配置,以数据库为例

# 数据库地址
DB_HOST=localhost
# 数据库端口
DB_PORT=3306
# 数据库登录名
DB_USER=root
# 数据库登录密码
DB_PASSWD=root
# 数据库名字
DB_DATABASE=easyestadmin

然后我们需要安装加载配置文件的包@nestjs/config

npm i @nestjs/config

我们还需要安装 cross-env来判断我们是处于什么环境从而加载不同环境配置文件

npm i cross-env -D

修改 package.json 中的 script,将生产环境的 NODE_ENV 设置为 production

"start:prod": "cross-env NODE_ENV=production node dist/main"

然后就可以在 app.module.ts 判断是否为生产环境从而加载不同配置文件了

image.png

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { UserModule } from "./user/user.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { User } from "./user/entities/user.entity";
import { ConfigModule, ConfigService } from "@nestjs/config";
import * as path from "path";
const isProd = process.env.NODE_ENV == "production";
@Module({
  imports: [
    UserModule,
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: [isProd ? path.resolve(".env.prod") : path.resolve(".env")],
    }),
    TypeOrmModule.forRootAsync({
      useFactory(configService: ConfigService) {
        return {
          type: "mysql",
          host: configService.get("DB_HOST"),
          connectorPackage: "mysql2", //驱动包
          port: configService.get("DB_PORT"), // 端口号
          username: configService.get("DB_USER"), // 用户名
          password: configService.get("DB_PASSWD"), // 密码
          database: configService.get("DB_DATABASE"), //数据库名
          entities: [User], //数据库对应的Entity
          synchronize: !isProd, //是否自动同步实体文件,生产环境建议关闭
        };
      },
      inject: [ConfigService],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

redis

当我们使用 MySQL 这样的关系型数据库时,数据是存储在硬盘中的,而计算机访问硬盘的速度通常相对较慢,这可能导致数据库查询数据时出现性能问题。为了解决这个问题,我们可以使用缓存技术,而其中最常用也是备受推崇的就是 Redis 了。

docker 使用 redis

首先在 docker 中拉取 Redis 镜像:docker pull redis,完成之后就可以使用这个镜像创建一个 redis 容器了 image.png

它的配置参数和 mysql 类似,运行之后 redis 就启动了,然后我们就可以在项目中连接 redis 了

NestJS 使用 redis

npm i redis -S

一般我们会给对 redis 的操作单独建一个模块,后面有哪些模块需要使用 redis 直接引入这个模块即可,这里我们将模块命名为 cache,使用 nestcli 命令创建

nest g res cache

然后我们将 redis 相关配置放到配置文件.env

# redis配置
RD_HOST=localhost
RD_PORT=6379

创建完成之后在 cache.module.ts 中引入 redis,且自定义一个 token 为'REDIS_CLIENT'的 provider,同时将 CacheService 导出,因为要给别的模块使用,和数据库连接一样这里用到了ConfigService来获取配置文件中的参数

import { Module } from "@nestjs/common";
import { CacheService } from "./cache.service";
import { createClient } from "redis";
import { ConfigService } from "@nestjs/config";
@Module({
  providers: [
    CacheService,
    {
      provide: "REDIS_CLIENT",
      async useFactory(configService: ConfigService) {
        const client = createClient({
          socket: {
            host: configService.get("RD_HOST"),
            port: configService.get("RD_PORT"),
          },
        });
        await client.connect();
        return client;
      },
      inject: [ConfigService],
    },
  ],
})
export class CacheModule {}

最后在cache.service.ts通过 Inject 注入REDIS_CLIENT,然后写一下操作 redis 的方法

import { Inject, Injectable } from '@nestjs/common';
import { RedisClientType } from 'redis';
@Injectable()
export class CacheService {
  constructor(@Inject('REDIS_CLIENT') private redisClient: RedisClientType) {}
  //获取值
  async get(key) {
    let value = await this.redisClient.get(key);
    try {
      value = JSON.parse(value);
    } catch (error) {}
    return value;
  }
  /**
   * 设置值
   * @param key {string} key
   * @param value 值
   * @param second 过期时间 秒
   * @returns Promise<any>
   */
  async set(key: string, value: any, second?: number) {
    value = JSON.stringify(value);
    return await this.redisClient.set(key, value, { EX: second });
  }
  //删除值
  async del(key: string) {
    return await this.redisClient.del(key);
  }
  //清除缓存
  async flushall() {
    return await this.redisClient.flushAll();
  }
}

由于 Redis 不能存储 JS 中的对象,所以需要转成字符串的形式,获取的时候再用 JSON.parse 转成对象(转不了的话就原样返回)

接下来我们在其它模块中引入试一下,以 User 模块为例,在user.module.ts导入CacheModule,然后在user.controller.ts导入使用(一般这种操作是在 service 中操作的,这里只是做演示方便防止 controller 中操作)。

import { CacheService } from 'src/cache/cache.service';
@Controller('user')
export class UserController {
  constructor(
    private readonly userService: UserService,
    private cacheService: CacheService,
  ) {}
  @Post('/set')
  async setVal(@Body() val) {
    return await this.cacheService.set('name', 'yueyue');
  }
  @Post('/get')
  async getVal(@Body() key) {
    return await this.cacheService.get('name');
  }
}

这里简单写了两个接口/user/set/user/get分别用来设置和获取 key 为name的值,然后我们用 apifox 调用一下

image.png

image.png

可以发现我们 redis 的存取都已经成功了,后面有用到 redis 的操作我们都可以引用这个模块操作了