从0到1实现NestJS的服务端应用——数据库篇
起步
从这里开始,我们将对nest.js的对数据操作的基本应用做一个简单的讲解,主要是明白如何通过访问接口来进行增删改查的基本操作。
介绍
这里我们使用的数据库是Mysql数据库,可以进入Mysql官方下载地址按照自己的系统选择下载,目前我这里使用的版本图如下:
工欲善其事必先利其器,接着就需要安装一个数据库的可视化应用,我这里下载的是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数据库,看看是否能成功连接,执行步骤如下:
- 进入Navicat软件,点击连接,会弹出一个提示框让我们输入凭证;
- 输入对应的地址和用户名、密码,点击测试连接,看到下图即表达连接可行:
- 点击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
工具手动添加一条数据:
数据添加完成后,尝试进行对商品查询的接口调用,结果如下图:
一切都很好,说明我们的查询功能十分顺利。
存储库管理
可以看到,上面我们通过定义的goodsRepositroy
对象就可以查询数据库了,那么这个goodsRepositroy
是什么呢?
其实goodsRepositroy
就是存储库管理Repository
实例的对象,通过该对象我们可以访问指定实体对应的数据库关系,并且该对象提供了一系列方法供我们来使用,比如下面常用的4个Api
方法:
- find:查询出满足条件的所有数据;
- findOneBy:通过指定条件查询一条数据出来
- save:即可用作保存,也可用作修改操作;
- delete:根据条件删除数据;
表之间关联
之前我们定义Goods
实体的时候使用keywords
属性来存放关键字,但实际上关键字作为一个实体更合理。现在我们以Keywords
来新建另一个实体,并且让Goods
和Keywords
来进行一个关联。
新建Keywords
实体类文件,代码如下:
import { Goods } from './goods.entity';
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Keywords {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
当程序编译完成,数据库中多了一张keywords
表,接下来我们就需要把Keywords
和Goods
实体关联起来。
不难知道,商品和关键字的关系是多对多的关系,因为一个关键字可以对应多个商品,一个商品也可以拥有多个关键字,所以这里我们需要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
方法来根据关键字名称来加载实体:
- 表中存在关键字,存在直接返回该实体;
- 表中没有关键字,则创建一个实体对象;
然后通过传递进来的
keywords
参数遍历生成对应的实体对象,进行save()
操作后nest.js
就会将我们的数据分别保存到三个表中。
通过接口调用,结果如下图:
可以看到,我们虽然只操作了Goods
实体的保存,但是也自动的在keywords
表中进行了数据的添加,这是因为cascade
级联起的作用。
好的,我们再进行查询,结果如图:
发现,没有keywords
关键字,这是因为当我们查询时,如果需要查询出关联的表的话,需要加上relations
参数来指定关联,如下所示:
async findGoodsById(id: number) {
return await this.goodsRepositroy.findOne({
relations: ['keywords'],
where: { id },
});
}
通过添加relations
关联后,我们再进行查询,可以发现keywords
对应的信息也出现了,如下图:
分页查询
查询数据的时候,一般都会进行分页的一个筛选查询,否则当数据量大的时候,全部查询出来的效率是比较低的,也是不好处理的。
进行分页查询需要用到两个参数,一个是skip
表示跳过的记录数,另一个是take
表示需要查询的记录数量,代码如下:
async findAll(paginationQueryDto: PaginationQueryDto) {
const { page, size } = paginationQueryDto;
return this.goodsRepositroy.find({
skip: (page - 1) * size,
take: size,
});
}
我们通过上面的方式,来实现根据页数来查询对应数量的记录,尝试调接口看一看返回,如图:
结果发现,数据是能够分页信息查询出来的!
事务
当我们在做某个功能的时候,可能需要操作多次数据库,有可能在其中的某一次操作中会发生异常导致失败。但此时,成功的操作也无法逆转,导致数据出现问题。 此时,我们就需要通过事务来解决这个问题。简单说,事务就是保证多次操作数据库时的一致性,要么都成功,要么都失败。
使用事务时,我们需要通过注入的方式来引用,代码如下:
@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
看到终端提示如下信息的话,就表明迁移已经成功了:
并且数据库里面keywords
表的字段也已经被修改了:
此时,如果我们执行下面命令:
npx typeorm-ts-node-esm migration:revert -d db.config.ts
再次查看keywords
表结构,发现字段名称又被改回去了:
注意:执行了run命令迁移之后的文件会被记录到数据库中,不会再次执行;当revert后记录会被删除。
完结
基于nest.js
配合TypeORM
的数据库操作就简单介绍这里,实际上还有很多的API
提供使用,但内容太过繁多,不便细写。
转载自:https://juejin.cn/post/7211074223758114872