下来这篇文章我们来讲解一下nestjs中使用数据库,不了解数据库操作的同学,请查看这里
Nodejs ORM框架
ORM特点
- 方便维护:数据模型定义在同一个提防,利于重构。
- 代码量少,对接多种库。
- 工具多,自动化能力强(数据库删除关联数据,事务操作等)
- 他是基于配置做sql操作的,所以我们不能直接书写sql,从而进行特别大的性能优化。
常见数据库
关系型:MySQL、Oracle、SQL Server、Access、SQLite
非关系型:MonogoDB、Redis、Hbase、Memcache。数据模型变动比较大,数据结构不确定。
关系型数据库特点
优点:易于维护、使用方便、支持复杂查询效率高
缺点:读写性能差,灵活性差
场景:各类业务系统、管理系统、安全性较高的场景
非关系型数据库特点
优点:易于扩展,大文件存储,查询速度快
缺点:复杂计算与联合查询效率低,
场景:多格式&海量数据、分布式消息系统、统计排行
分多张表: 方便索引,结构清晰,防止数据冗余
nestjs结合typeORM
nestjs默认集成了typeORM模块,我们只需要安装对应的库然后进行配置即可。@nestjs/typeorm, typeorm, mysql2
。
js
npm install --save @nestjs/typeorm typeorm mysql2
Nest
使用TypeORM是因为它是 TypeScript
中最成熟的对象关系映射器( ORM
)。因为它是用 TypeScript
编写的,所以可以很好地与 Nest
框架集成。
基础配置
js
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
// 导入模块
imports: [
// 配置数据库连接信息
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'xxx',
database: 'xx',
entities: [],
// 同步本地的schema与数据库 -> 初始化的时候去使用
synchronize: true,
// 日志等级
logging: ['error'],
}),
UserModule,
],
// 注册控制器
controllers: [AppController, UserController],
// 依赖注入,在控制器中自动实例化该服务
providers: [AppService, UserService],
})
为了更易于扩展,我们可以提供环境变量给数据库连接配置提供属性
js
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
const envFilePath = `.env.${process.env.NODE_ENV || 'development'}`;
@Module({
// 导入模块
imports: [
// 工厂模式返回,使用环境变量中的配置
TypeOrmModule.forRootAsync({
// 为了可以使用ConfigService实例对象获取环境变量
imports: [ConfigModule],
inject: [ConfigService],
useFactory(configService: ConfigService) {
return {
type: 'mysql',
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USER_NAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATE_BASE'),
entities: [],
// 同步本地的schema与数据库 -> 初始化的时候去使用
synchronize: true,
// 日志等级
logging: ['error'],
};
},
}),
UserModule,
],
// 注册控制器
controllers: [AppController, UserController],
// 依赖注入,在控制器中自动实例化该服务
providers: [AppService, UserService],
})
数据库设计
- 原子性,要求属性设计具有原子性。不能存在嵌套属性,需要借助外键关联多个表。
- 记录唯一性,表示一个表中所有字段没有依赖关系。(即非主键字段必须依赖主键字段,即主键由单一属性组成)否则可能出现数据冗余,删除异常,插入异常,更新异常等等。
js
表:职工号 姓名 职称 项目号 项目名称
职工号 -> 姓名,职称
项目号 -> 项目名称
正确设计
职工信息表:职工号,姓名,职称
项目信息表:项目号,项目名称
- 第三范式。当属于第二范式后,并且在两个或多个非主键属性之间不存在函数依赖(传递依赖)。它是对字段的冗余性做限制,要求任何字段不能有其他字段派生出来。
js
表:学号 姓名 年龄 性别 所在院校 院校地址 院校电话
上述属于第二范式,因为主键有单一属性组成。但是承载依赖传递 (学号 - 学生 - 所在学院 - 学院电话)
正确设计
学生表:学号 姓名 年龄 所在学院
学院表:学院 学院名称 电话
配置好ORM实体类并注册后,自动在连接的数据库中创建对应的表以及表结构。注意:实体类中只设置应有的属性字段,外键我们是通过注解去配置的,而不需要设置在实体类中。
js
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
@Column()
password: string;
}
js
TypeOrmModule.forRootAsync({
// 为了可以使用ConfigService实例对象获取环境变量
imports: [ConfigModule],
inject: [ConfigService],
useFactory(configService: ConfigService) {
return {
type: 'mysql',
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USER_NAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATE_BASE'),
// 注册实体类
entities: [User],
// 同步本地的schema与数据库 -> 初始化的时候去使用
synchronize: true,
// 日志等级
logging: ['error'],
};
},
}),
这里需要了解一些注解。
普通注解
Column
表示列属性PrimaryGeneratedColumn
表示主键自增
关系注解
对于多对一和一对一,我们只需要在对应的OneToOne,OneToMany中指定第二个参数与主表的映射属性即可。在主表中并不需要指定。设置JoinColumn注解即可。
一对一
OneToOne
一对一关系。我们在映射的时候并不关系主键外键,他们只是一个桥接的作用,我们主要是获取两个表中的其他列属性。所以需要映射一个实体类。JoinColumn
连接外部表在当前表中的烈属性。即外键。
js
// 设置外键映射的实体类
@OneToOne(() => User)
// 默认键名叫做 关联表名+主键名称,小驼峰形式。但是我们也可以手动设置
// 例如外表主键是index,那么这个外键命名就是userIndex
@JoinColumn({ name: 'userId' })
user: User;
js
// 这里需要指定关联属性
@OneToOne(() => Profile, (profile) => profile.user)
profile: Profile;
一对多,多对一
ManyToOne
多对一。OneToMany
一对多。
我们关联多对一关系时,需要在多的那方建立外键,作为主表。@ManyToOne
可以单独使用,但@OneToMany
必须搭配@ManyToOne
使用。
其实这里的JoinColumn
注解是不需要的,因为OneToMany
, ManyToOne
的第二个参数已经可以建立联系了。 或者删除(user) => user.logs
。(user) => user.logs
这种方式是给高级语言看的,而JoinColumn
是数据库进行查询找到属性的。
js
// 第一个参数 指定返回的数据类型是啥样的
// 第二个参数 表示查询时的条件
@ManyToOne(() => User, (user) => user.logs)
// 建立多表连接(外键和主键链接)
@JoinColumn()
user: User;
js
// 一对多
// 一个用户对应多个操作日志。
// 多对一时,主表就是多的那方,在主表中建立一个外键绑定user的主键。
// 第二个参数就是和logs表的连接条件 user.id === logs.userId
@OneToMany(() => Logs, (logs) => logs.user)
logs: Logs[];
多对多
ManyToMany
建立多对多关系。我们知道多对多关系,主要通过第三个表进行建立。所以需要用到JoinTable
注解。
js
// 多对多
@ManyToMany(() => Roles, (roles) => roles.users)
// 建立额外关联表
@JoinTable()
roles: Roles[];
js
// 多对多
@ManyToMany(() => User, (user) => user.roles)
users: User[];
如何将已存在的数据库映射生成ORM实体类文件
通过typeorm-model-generator
去通过数据库表结构生成对应的实体类。 然后配置运行命令
js
typeorm-model-generator -h 域名 -p 端口号 -d 数据库 -u 用户名 -x 密码 -e 数据库类型 -o .
DI容器工作原理
所以我们使用typeOrm提供的数据库操作方法,只需要在对应各模块中导入即可被nestjs收集,并自动实例化。
js
// user.module.ts
@Module({
// 在module中导入的依赖就会被自动实例化。
imports: [TypeOrmModule.forFeature([User])],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
数据库crud测试
一个crud。下面这个是单表的数据库操作。
js
// user.service.ts
import { User } from 'src/user/user.entity';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User) private readonly userRepository: Repository<User>,
) {}
/**
* 查找全部
*/
async findAll() {
return this.userRepository.find();
}
/**
* 查找指定的
*/
async findById(id: number) {
return this.userRepository.find({ where: { id } });
}
/**
* 添加用户
*/
async createUser(user: User) {
// 创建一个数据库实体类
const dbUser = await this.userRepository.create(user);
return this.userRepository.save(dbUser);
}
/**
* 更新用户
*/
async updateUser(id: number, user: Partial<User>) {
return this.userRepository.update(id, user);
}
/**
* 删除用户
*/
async removeUser(id: number) {
return this.userRepository.delete(id);
}
}
js
// user.controller.ts
import { User } from 'src/user/user.entity';
import { ConfigService } from '@nestjs/config';
import {
Controller,
Get,
Param,
Query,
Post,
Body,
Put,
Delete,
} from '@nestjs/common';
import { UserService } from './user.service';
@Controller('user')
export class UserController {
constructor(
private userService: UserService,
private config: ConfigService,
) {}
@Get('getAllUser')
async getAllUser() {
return this.userService.findAll();
}
@Get('getUserById/:id')
async getUserById(@Query() { id }) {
return this.userService.findById(id);
}
@Post('createUser')
async createUser(@Body() user: User) {
return this.userService.createUser(user);
}
@Put('updateUser')
async updateUser(@Body() user: User) {
const id = user.id;
delete user.id;
return this.userService.updateUser(id, user);
}
@Delete('deleteUserById')
async deleteUserById(@Param() { id }) {
return this.userService.removeUser(id);
}
}
多表联查
我们在进行实体类之间关系设计时,记住这句话就可以。对于多对一和一对一,我们只需要在对应的OneToOne,OneToMany中指定第二个参数与主表的映射属性即可。在主表中并不需要指定,设置JoinColumn注解即可。
我们进行条件查询时,where中指定的属性就是当前实体类中定义的属性。
js
// 关联查询 (一对一)
async findProfile(id: number) {
return this.userRepository.findOne({
where: { id },
relations: {
profile: true,
},
});
}
// 多对多(以user为主记录)
async findLogs(id: number) {
return this.userRepository.findOne({
where: { id },
relations: {
logs: true,
},
});
}
// 多对多(以logs为主记录)
// async findLogs(id: number) {
// const user = await this.findById(id);
// return this.logsRepository.find({
// // 这里的条件就是logs实体类的属性
// where: { user },
// relations: {
// user: true,
// },
// });
// }
以上即是简单的介绍,具体请看typerOrm官网
往期年度总结
往期文章
- 因为原生,选择一家公司(前端如何防笔试作弊)
- 结合开发,带你熟悉package.json与tsconfig.json配置
- 如何优雅的在项目中使用echarts
- 如何优雅的做项目国际化
- 近三个月的排错,原来的憧憬消失喽
- 带你从0开始了解vue3核心(运行时)
- 带你从0开始了解vue3核心(computed, watch)
- 带你从0开始了解vue3核心(响应式)
- 3w+字的后台管理通用功能解决方案送给你
- 入职之前,狂补技术,4w字的前端技术解决方案送给你(vue3 + vite )