Nest探究(二): Nest的TypeOrm前言 连接数据库 既然是后端的项目,连接数据库是必不可少的,这里我选择更为
前言
最近在探究Nest,发现这东西还不错,分享给大家,上一篇文章Nest探究(一):Nest已经大致说明了一下Nest的结构以及Nest简单的使用,而这篇文章将会介绍Nest数据库的相关操作,如有错误,还请各位大佬指出!
连接数据库
既然是后端的项目,连接数据库是必不可少的,这里我选择更为熟悉的MySQL数据库。
而Nest当中,有很多连接数据库的ORM
(对象关系映射器),如TypeORM
、Sequelize
、Prisma
,在此我选择TypeORM
,Sequelize
实际上与koa
一致,是使用sqly
语句,来对数据库进行操作,而TypeORM
它本身是由TypeScript
写的,对Nest
的支持相对较好,而且Nest
也提供了@nestjs/typeorm
包,对于一些常用的增删改查sql语句也是进行了函数封装,开箱即用,比较方便;Prisma
呼声很高,有兴趣的同学可以自行查看,在这里我使用TypeORM
。
在此之前,我先解释一下,什么是ORM
?
举个例子,我们有如下一张表,
+----+--------+------------+
| id | title | text |
+----+-------------+--------------+
| 1 | 标题 | 内容文本 |
+----+--------+------------+
而这张表的每一行,可以使用一个javaScript
对象来表示
{
id: 1,
title:"标题",
content:"内容文本"
}
ORM
就是这样,把关系数据库的结构映射到对象上。而我们只要传入这些结构的javaScript
对象,数据库就会自行对应存储数据,是不是简单方便?接下来,我们看看如何使用TypeORM
。
- 首先我们需要安装依赖
npm install --save @nestjs/typeorm
- 其次使用
TypeORM
连接mysql
数据库,也需要安装依赖
npm install --save typeorm mysql2
环境配置
- 首先需要在项目根目录下创建两个文件
.env
文件和.env.prod
文件,存储的分别是开发环境和线上环境不同的环境变量。
// 数据库地址
DB_HOST=localhost
// 数据库端口
DB_PORT=3306
// 数据库登录名
DB_USER=root
// 数据库登录密码
DB_PASSWD=root
// 数据库名字
DB_DATABASE=demo
.env.prod
中的是上线要用的数据库信息,如果你的项目要上传到线上管理,为了安全性考虑,建议这个文件添加到.gitignore
中。
- 接着在根目录下创建一个文件夹
config
(与src
同级),然后再创建一个env.ts
用于根据不同环境读取相应的配置文件。
import * as fs from 'fs';
import * as path from 'path';
const isProd = process.env.NODE_ENV === 'production';
function parseEnv() {
const localEnv = path.resolve('.env');
const prodEnv = path.resolve('.env.prod');
if (!fs.existsSync(localEnv) && !fs.existsSync(prodEnv)) {
throw new Error('缺少环境配置文件');
}
const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv;
return { path:filePath };
}
export default parseEnv();
- 而
nest
带有环境配置
yarn add @nestjs/config
@nestjs/config
默认会从项目根目录载入并解析.env
文件,从.env
文件和process.env
合并环境变量键值对,forRoot()
方法注册了ConfigService
提供者,后者提供了一个get()
方法来读取这些解析/合并的配置变量。要注入ConfigService
,需要在需要使用的地方先导入ConfigModule
。- 而在
app.module
中使用了ConfigModule.forRoot()
,将isGlobal
设置为true
,在其他地方使用时不需要做任何事,表示全局使用,此时就可以在全局范围内使用process.env.xxx
读取全局变量 - 在
app.module.ts
文件中使用@nestjs/config
进行全局配置,以及使用@nestjs/typeorm
提供的TypeOrmModule
连接数据库如下 - 在app.module.ts使用
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
// 环境配置相关
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true
}),
TypeOrmModule.forRootAsync({
useFactory: () => ({
type: 'mysql',
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT, // 来自process.env的每个值都是字符串,前面加+转数字
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true, // 自动加载模块 推荐
// entities: [path.join(__dirname, '/../**/*.entity{.ts,.js}')], // 不推荐
synchronize: true // 开启同步,生产中要禁止
})
}),
],
controllers: [],
providers: []
})
export class AppModule {}
- 上文代码中
forRootAsync
使用了TypeORM
的异步工程模式,这样可以解决imports
的顺序问题,也就是说,使用了forRootAsync
,可以不用在意imports
这个数组中使用TypeOrmModule
的顺序. - 因为每个创建的实体必须在连接选项中进行注册,所以
TypeORM
提供了autoLoadEntities
来自动加载创建的数据库实体,使用这种方式也比较推荐,也可以使用entities: [path.join(__dirname, '/../**/*.entity{.ts,.js}')]
的方式,这表示,指定包含所有实体的整个目录,该目录下所有实体都将被加载.
Entity(实体)
使用@Entity()
来标记通过定义一个新类创建的实体,装饰器根据编写的类,通过映射自动生成一个SQL表,以及他们包含的元数据,其中包含
Column(普通列)
-
用
@Colimn()
来标记普通列 -
为列指定类型:
@Column("int")
/@Column({ type: "int" })
:数字类型
-
还需要指定其他参数:
@Column("varchar", { length: 200 })
:字符串类型,长度为200@Column({ type: "int", length: 200 })
:数字类型,长度为200
列选项
-
列选项定义实体列的其他选项。 你可以在
@Column()
上指定列选项:title: string
: 数据库表中的列名。unique: true
:将列标记为唯一列,里面的值不可重复nullable: boolean
: 在数据库中使列NULL
或NOT NULL
。 默认情况下,列是nullable:false
。- 更多的请看TypeORM中文文档
主列
-
每个实体都必须要有一个主列
-
用
@PrimaryColumn()
来标记主列,需要给它手动分配值 -
用
@PrimaryGeneratedColumn()
来标记主列,该值将使用自动增量值自动生成 -
用
@PrimaryGeneratedColumn('uuid')
来标记主列,该值将使用uuid
(通用唯一标识符)自动生成,uuid
可以被认为是唯一的uuid
是让分布式系统中的所有元素都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定。如此一来,每个人都可以创建不与其它人冲突的uuid
。在这样的情况下,就不需考虑数据库创建时的名称重复问题uuid
的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12
的32
个字符,如:550e8400-e29b-41d4-a716-446655440000
。
关系
OneToOne(一对一关系)
- 使用
OneToOne
指明一对一的关系,在一对一关系中,表A
中的一条记录,只能关联表B
中的一条记录。比如:每一个用户都只能有一个用户档案, - 在用户表的实体当中使用
OneToOne
指明一对一的关系,并使用档案表实体作为用户表的某个属性。使用@ JoinColumn
定义外键,并允许自定义连接列名和引用的列名。
@JoinColumn
必须在且只在关系的一侧的外键上, 设置@JoinColumn
在该表的实体中,该表将会包含一个relation id
和目标实体表的外键。记住,不能同时在二者的entity
中使用。
OneToMany(一对多关系)
- 使用
OneToMany
指明一对多的关系,在一对多关系中,表A
中的一条记录,可以关联表B
中的一条或多条记录。比如:每一个文章分类都可以对应多篇文章,反过来一篇文章只能属于一个分类,这种分类表和文章表的关系就是一对多的关系。 - 在
一
,即分类表的实体当中使用OneToMany
指明一对多的关系,并使用多
即文章表的实体作为一
的某个属性。 - 在
多
,即文章表的实体当中使用ManyToOne
指明关系,并使用一
即分类表的实体作为多
的某个属性。使用@ JoinColumn
定义外键,并允许自定义连接列名和引用的列名。 - TypeORM在处理“一对多”的关系时, 将
一
的主键作为多
的外键,即@ManyToOne
装饰的属性;这样建表时用最少的数据表操作代价,避免数据冗余,提高效率。
ManyToMany(多对多关系)
-
@ManyToMany()
指明多对多关系,多对多是表A
的一条记录关联表B
的多个记录,而表B
的一条记录也可以关联表A
的多条记录的关系。比如:一篇文章可以对应多个板块,而一个板块下面也有多个文章。 -
@JoinTable
用于描述“多对多”关系,@JoinTable()
是@ManyToMany()
关系所必需的, 通过配置joinColumns
和inverseJoinColumns
来自定义中间表
的列名称。 -
保存这种关系,需要启动级联
cascade
@ManyToMany((type) => Role, (role) => role.users, { cascade: true })
使用TypeORM
-
进行注册实体,首先我们创建
user
用户实体。Role
是角色实体
// user.entity.ts
import {
Entity,
Column,
JoinTable,
ManyToMany,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn
} from 'typeorm';
import { Role } from './role.entity';
// @Entity()装饰器自动从所有类生成一个SQL表,以及他们包含的元数据
// @Entity('users') // sql表名为users
@Entity() // sql表名为user
export class User {
// 主键装饰器,也会进行自增
@PrimaryGeneratedColumn()
id: number;
// 列装饰器
@Column()
username: string;
// @Column('json', { nullable: true }) json格式且可为空
@Column()
password: string;
// 定义与其他表的关系
// name 用于指定创中间表的表名
@JoinTable({ name: 'user_roles' })
// 指定多对多关系
/**
* 关系类型,返回相关实体引用
* cascade: true,插入和更新启用级联,也可设置为仅插入或仅更新
* ['insert']
*/
@ManyToMany((type) => Role, (role) => role.users, { cascade: true })
roles: Role[];
@CreateDateColumn()
createAt: Date;
@UpdateDateColumn()
updateAt: Date;
}
复制代码
- 创建角色实体
// role.entity.ts
import {
Entity,
Column,
ManyToMany,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn
} from 'typeorm';
import { User } from './user.entity';
@Entity()
export class Role {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany((type) => User, (user) => user.roles)
users: User[];
@CreateDateColumn()
createAt: Date;
@UpdateDateColumn()
updateAt: Date;
}
在查找关联表的信息时,需要注意:
- 使用
relations
表示需要加载主体,如下是一个查询用户列表所有数据的代码 -
-
方式一:
relations: { roles: true }
-
方式二:
relations: ['roles']
-
async getUserList() {
return await this.userRepository.find({
// 1
// relations: {
// roles: true
// },
// 2
relations: ['roles'],
});
}
方法
TypeOrm
其中有封装好的操作数据库的函数,代码奉上。
async create(createArticleDto: CreateArticleDto) {
const article = await this.articleRepository.create({
...createArticleDto
});
return await this.articleRepository.save(article);
}
async getArticleList(paginationsQuery: PaginationQueryDto) {
const { limit, offset } = paginationsQuery;
return await this.articleRepository.find({
skip: offset,
take: limit
});
}
async findOneById(id: number) {
return await this.articleRepository.findOneBy({ id });
}
async update(id: number, updateArticleDto: UpdateArticleDto) {
const article = await this.articleRepository.preload({
id,
...updateArticleDto
});
if (!article) {
throw new NotFoundException(`${id} not found`);
}
return await this.articleRepository.save(article);
}
async remove(id: number) {
const article = await this.articleRepository.findOneBy({ id });
if (!article) {
throw new NotFoundException(`${id} not found`);
}
return await this.articleRepository.remove(article);
}
总结
这里我们只是讲解了TypeOrm
的数据库连接和基本操作,下一篇我们将会讲到一些更加复杂的管道配置和各种装饰器。
我是小白,我们一起学习!
转载自:https://juejin.cn/post/7184350230492479545