likes
comments
collection
share

从0到1实现NestJS的服务端应用——数据库篇

作者站长头像
站长
· 阅读数 74

起步

从这里开始,我们将对nest.js的对数据操作的基本应用做一个简单的讲解,主要是明白如何通过访问接口来进行增删改查的基本操作。

介绍

这里我们使用的数据库是Mysql数据库,可以进入Mysql官方下载地址按照自己的系统选择下载,目前我这里使用的版本图如下:

从0到1实现NestJS的服务端应用——数据库篇

工欲善其事必先利其器,接着就需要安装一个数据库的可视化应用,我这里下载的是Navicat软件。

一切准备就绪后,就可以进行下一个步骤了。

TypeORM

TypeORM 是一个ORM框架,它可以运行在 NodeJS、Browser、Cordova、PhoneGap、Ionic、React Native、Expo 和 Electron 平台上,可以与 TypeScript 和 JavaScript (ES5,ES6,ES7,ES8)一起使用。 它的目标是始终支持最新的 JavaScript 特性并提供额外的特性以帮助你开发任何使用数据库的(不管是只有几张表的小型应用还是拥有多数据库的大型企业应用)应用程序。 -- 摘自官网

nest.js中,内置并推荐我们使用TypeORM来操作数据库,至于什么是ORM?简单来说就是把JavaScript的对象映射为数据的一张表,通过操作该对象来使得影响数据库中表的结果。

连接数据库

安装完数据库后,首先我使用Navicat来连接Mysql数据库,看看是否能成功连接,执行步骤如下:

  1. 进入Navicat软件,点击连接,会弹出一个提示框让我们输入凭证;
  2. 输入对应的地址和用户名、密码,点击测试连接,看到下图即表达连接可行:

从0到1实现NestJS的服务端应用——数据库篇

  1. 点击OK进行连接,就完成了。

在Navicat中测试连接成功后,我们就需要在nest.js来进行数据库的间接了,连接的代码如下所示:

// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'password',
      database: 'demo',
      autoLoadEntities: true,
      synchronize: true,
    })
  ]
})
export class AppModule {}
  • type: 表示要连接的数据库类型,因为TypeORM支持多种数据库;
  • host:连接的目标主机号,我们是本地的服务,所以是localhost;
  • port:数据库的端口号,mysql默认为3306;
  • username:登入数据库的用户名;
  • password:登入数据库的密码;
  • database:要使用的mysql数据库;
  • autoLoadEntities:自动加载实体类去映射到数据库;
  • synchronize:是否同步本地的修改到数据库。

警告:synchronize严禁在生产环境下使用,因为有可能会导致数据的丢失。

完成以上配置后,执行npm run start:dev启动服务,没有报错的话,就表示数据库已经连接成功了。

操作数据库

还记得之前建立的商品实体对象嘛?当时,这个对象就仅仅当做类型来使用了一下,并没有其他用处。但是,在我们是使用TypeORM后,我们就可以让实体类和数据库进行一个映射关系。

关联实体

现在,先来改造一下实体类,代码如下:

import {
  Column,
  Entity,
  JoinTable,
  ManyToMany,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { Keywords } from './keywords.entity';

@Entity()
export class Goods {
  @PrimaryGeneratedColumn()
  id: number;
  
  @Column()
  name: string;

  @Column({
    type: Number,
  })
  price: number;
  
  @Column()
  brand: string;
}

在上面的实体类代码中,我们通过@Entity()装饰器修饰了该实体,并且通过@Column()装饰器装饰每个字段,其中比较特殊的是@PrimaryGeneratedColumn(),这个装饰器表示修饰的该字段是一个自增主键。

当实体类改造完毕以后,我们就需要在goods.service中注入存储库对象,这个对象就是用来操作数据库的。在注入存储库之前,我们得先做一个操作,就是需要在goods.module中进行注册,代码如下:

import { TypeOrmModule } from '@nestjs/typeorm';
import { Module } from '@nestjs/common';
import { GoodsController } from './goods.controller';
import { GoodsService } from './goods.service';
import { Goods } from './entities/goods.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Goods])],
  controllers: [GoodsController],
  providers: [GoodsService],
})
export class GoodsModule {}

如果操作一切正确,打开Navicat我们就能发现多了一张goods表了!

注入存储库

通过TypeOrmModule.forFeature([Goods])该方法后,就实现了存储库的注册,现在我们在goods.service进行注入,代码如下:

import { UpdateGoodsDto } from './dto/update-goods.dto';
import { CreateGoodsDto } from './dto/create-goods.dto';
import { Goods } from './entities/goods.entity';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

@Injectable()
export class GoodsService {
  constructor(
    @InjectRepository(Goods)
    private readonly goodsRepositroy: Repository<Goods>,
  ) {}

  async findGoodsById(id: number) {
    return await this.goodsRepositroy.findOneBy({ id });
  }
}

可以看到,在构造函数中我们定义了一个通过@InjectRepository(Goods)装饰器修饰的goodsRepository对象,通过该对象我们就可以操作数据库了。

在下面的findGoodsById方法中,就通过了findOneBy({ id })方法对数据库进行了一个条件的查询,现在我通过Navigate工具手动添加一条数据:

从0到1实现NestJS的服务端应用——数据库篇

数据添加完成后,尝试进行对商品查询的接口调用,结果如下图:

从0到1实现NestJS的服务端应用——数据库篇

一切都很好,说明我们的查询功能十分顺利。

存储库管理

可以看到,上面我们通过定义的goodsRepositroy对象就可以查询数据库了,那么这个goodsRepositroy是什么呢? 其实goodsRepositroy就是存储库管理Repository实例的对象,通过该对象我们可以访问指定实体对应的数据库关系,并且该对象提供了一系列方法供我们来使用,比如下面常用的4个Api方法:

  • find:查询出满足条件的所有数据;
  • findOneBy:通过指定条件查询一条数据出来
  • save:即可用作保存,也可用作修改操作;
  • delete:根据条件删除数据;

表之间关联

之前我们定义Goods实体的时候使用keywords属性来存放关键字,但实际上关键字作为一个实体更合理。现在我们以Keywords来新建另一个实体,并且让GoodsKeywords来进行一个关联。

新建Keywords实体类文件,代码如下:

import { Goods } from './goods.entity';
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Keywords {
  @PrimaryGeneratedColumn()
  id: number;
  
  @Column()
  name: string;
}

当程序编译完成,数据库中多了一张keywords表,接下来我们就需要把KeywordsGoods实体关联起来。 不难知道,商品和关键字的关系是多对多的关系,因为一个关键字可以对应多个商品,一个商品也可以拥有多个关键字,所以这里我们需要TypeORM提供的@ManyToMany()装饰器来修饰,代码如下:

// goods.entity.ts
@JoinTable()
@ManyToMany((type) => Keywords, (keywords) => keywords.goods, {
  cascade: true,
})
keywords: Keywords[];

// keywords.entity.ts
@ManyToMany((type) => Goods, (goods) => goods.keywords, {
  cascade: true,
})
goods: Goods[];

现在来说明一下上面代码的意思:

  • @JoinTable():表示关系的拥有者,只需要在其中一个定义即可;
  • @ManyToMany():表示两个实体间的关系为多对多
    • arg1:表示需要建立关系的实体类对象;
    • arg2:建立关系的实体类对象引用的当前实体类属性;
    • options:cascade表示是否级联,级联之后添加和修改时,主表都会影响从表的记录。

注意:多对多关系会生成三张表,多出来的那一张是中间表,记录两个表数据的对应关系。

完成以上步骤后,我们现在尝试进行数据的保存,代码如下:

// goods.service.ts
async addGoods(createGoodsDto: CreateGoodsDto) {
    const keywords = await Promise.all(
      createGoodsDto.keywords.map((item) => this.preloadKeywordsByName(item)),
    );
    const goods = this.goodsRepositroy.create({
      ...createGoodsDto,
      keywords,
    });
    return await this.goodsRepositroy.save(goods);
}

private async preloadKeywordsByName(name: string) {
    const keywords = await this.keywordsRepository.findOneBy({ name });
    if (keywords) {
      return keywords;
    }
    return this.keywordsRepository.create({ name });
}

上面我们通过顶一个preloadKeywordsByName方法来根据关键字名称来加载实体:

  1. 表中存在关键字,存在直接返回该实体;
  2. 表中没有关键字,则创建一个实体对象; 然后通过传递进来的keywords参数遍历生成对应的实体对象,进行save()操作后nest.js就会将我们的数据分别保存到三个表中。

通过接口调用,结果如下图:

从0到1实现NestJS的服务端应用——数据库篇

从0到1实现NestJS的服务端应用——数据库篇

从0到1实现NestJS的服务端应用——数据库篇

可以看到,我们虽然只操作了Goods实体的保存,但是也自动的在keywords表中进行了数据的添加,这是因为cascade级联起的作用。

好的,我们再进行查询,结果如图:

从0到1实现NestJS的服务端应用——数据库篇

发现,没有keywords关键字,这是因为当我们查询时,如果需要查询出关联的表的话,需要加上relations参数来指定关联,如下所示:

async findGoodsById(id: number) {
  return await this.goodsRepositroy.findOne({
    relations: ['keywords'],
    where: { id },
  });
}

通过添加relations关联后,我们再进行查询,可以发现keywords对应的信息也出现了,如下图:

从0到1实现NestJS的服务端应用——数据库篇

分页查询

查询数据的时候,一般都会进行分页的一个筛选查询,否则当数据量大的时候,全部查询出来的效率是比较低的,也是不好处理的。

进行分页查询需要用到两个参数,一个是skip表示跳过的记录数,另一个是take表示需要查询的记录数量,代码如下:

async findAll(paginationQueryDto: PaginationQueryDto) {
    const { page, size } = paginationQueryDto;
    return this.goodsRepositroy.find({
      skip: (page - 1) * size,
      take: size,
    });
}

我们通过上面的方式,来实现根据页数来查询对应数量的记录,尝试调接口看一看返回,如图:

从0到1实现NestJS的服务端应用——数据库篇

从0到1实现NestJS的服务端应用——数据库篇

结果发现,数据是能够分页信息查询出来的!

事务

当我们在做某个功能的时候,可能需要操作多次数据库,有可能在其中的某一次操作中会发生异常导致失败。但此时,成功的操作也无法逆转,导致数据出现问题。 此时,我们就需要通过事务来解决这个问题。简单说,事务就是保证多次操作数据库时的一致性,要么都成功,要么都失败。

使用事务时,我们需要通过注入的方式来引用,代码如下:

@Injectable()
export class GoodsService {
  constructor(
    @InjectRepository(Goods)
    private readonly goodsRepositroy: Repository<Goods>,
    @InjectRepository(Keywords)
    private readonly keywordsRepository: Repository<Keywords>,
    private readonly dataSource: DataSource,
  ) {}
}

其中,private readonly dataSource: DataSource定义了一个数据源对象,这个对象不用通过装饰器来装饰也能被nest.js给我们进行管理。 接着,我们再来看看执行事务的代码,如下:

async recommendGoods(id: number) {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();
    try {
      const goods = await this.goodsRepositroy.findOneBy({ id });
      goods.recommend++;
      const recommentEvent = new Event();
      recommentEvent.name = 'recommend_goods';
      recommentEvent.type = 'goods';
      recommentEvent.payload = { goodsId: goods.id };
      await queryRunner.manager.save(goods);
      await queryRunner.manager.save(recommentEvent);
      await queryRunner.commitTransaction();
    } catch (err) {
      await queryRunner.rollbackTransaction();
    } finally {
      await queryRunner.release();
    }
  }

上面,我们通过dataSource对象创建了一个QueryRunner对象,通过该对象我们就可以建立连接,并手动控制事务的状态。

  • startTransaction(): 开启事务;
  • commitTransaction():提交所有的更改;
  • queryRunner.rollbackTransaction():事务回滚,撤销所有更改;
  • queryRunner.release():释放当前的连接;

数据迁移

数据迁移的作用在于,当我们可能需要修改表结构时:如果直接通过修改实体类的话,会导致表里面的数据造成丢失;所以需要通过数据迁移文件来操作数据库,进而达到修改表结构的目的。

现在,我们通过TypeORM CLI来创建一个数据迁移的文件,输入以下命令:

npx typeorm migration:create src/migrations/KeywordsRefactor

执行该命令后,在我们的src/migrations目录下就会生成一个{timestamp}-KeywordsRefactor.ts的文件(timestamp表示自动生成的时间戳),进入该文件发下以下内容:

import { MigrationInterface, QueryRunner } from 'typeorm';

export class KeywordsRefactor1678969889283 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {}

  public async down(queryRunner: QueryRunner): Promise<void> {}
}

其中,up方法表示迁移所需要执行的sql代码;而down表示迁移失败之后,需要回复up操作的方法。 例如,我们这里将keywords表的name字段改为title字段,代码如下:

import { MigrationInterface, QueryRunner } from 'typeorm';

export class KeywordsRefactor1678969889283 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    queryRunner.query(`
      ALTER TABLE keywords CHANGE name title varchar(255);
    `);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    queryRunner.query(`
      ALTER TABLE keywords CHANGE title name varchar(255);
    `);
  }
}

在执行迁移之前,我们还需要创建一个db.config.ts配置文件,内容如下:

import { DataSource } from 'typeorm';

const AppDataSource = new DataSource({
  type: 'mysql',
  host: 'localhost',
  port: 3306,
  username: 'root',
  password: '',
  database: 'demo',
  entities: ['dist/**/*.entity.js'],
  migrations: ['dist/**/migrations/*.js'],
});

export default AppDataSource;

配置就和建立数据库连接时的配置基本一样,只是多了两个字段:

  • entities:表示实体类的存储位置;
  • migrations:表示迁移文件的存储位置;

因为这里我们迁移过程是根据打包后的dist目录来识别的,所以在迁移之前先进行build构建,执行命令:

npm run build

当生成好dist目录后,我们就可以进行迁移命令了:

npx typeorm-ts-node-esm migration:run -d db.config.ts

看到终端提示如下信息的话,就表明迁移已经成功了:

从0到1实现NestJS的服务端应用——数据库篇

并且数据库里面keywords表的字段也已经被修改了:

从0到1实现NestJS的服务端应用——数据库篇

此时,如果我们执行下面命令:

npx typeorm-ts-node-esm migration:revert -d db.config.ts

再次查看keywords表结构,发现字段名称又被改回去了:

从0到1实现NestJS的服务端应用——数据库篇

注意:执行了run命令迁移之后的文件会被记录到数据库中,不会再次执行;当revert后记录会被删除。

完结

基于nest.js配合TypeORM的数据库操作就简单介绍这里,实际上还有很多的API提供使用,但内容太过繁多,不便细写。