连接mysql数据库
安装插件:npm i -S @nestjs/typeorm typeorm mysql2
在根模块app.module.ts
中连接mysql
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
/** 连接mysql数据库 */
TypeOrmModule.forRoot({
type: 'mysql',
host: '11.111.111.111',
port: 3306,
username: 'root',
password: 'you-database-password',
database: 'you-database-name',
// 扫描本项目中.entity.ts或者.entity.js的文件: [__dirname + '/**/*.entity{.ts,.js}']
entities: [],
// 定义数据库表结构与实体类字段同步(这里一旦数据库少了字段就会自动加入,根据需要来使用),一般用于数据库初始化
synchronize: true,
// 开发环境打印所有日志
logging: process.env.NODE_ENV === 'development' ? true : ['error'],
}),
],
})
export class AppModule {}
现在我们需要读取环境变量设置数据库, 修改
app.module.ts
数据库连接的写法
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ConfigEnum } from './enum/config.enum';
@Module({
imports: [
/** 配置环境变量 */
ConfigModule.forRoot({
isGlobal: true,
envFilePath: `./.env.${process.env.NODE_ENV}`,
load: [() => dotenv.config({ path: '.env' })],
validationSchema: Joi.object({
DB_TYPE: Joi.string().valid('mysql'),
DB_DATABASE: Joi.string(),
DB_PORT: Joi.number().default(3306),
DB_HOST: Joi.alternatives().try(
Joi.string().ip(), // ip
Joi.string().domain(), // 域名
),
DB_USERNAME: Joi.string(),
DB_PASSWORD: Joi.string(),
DB_SYNC: Joi.boolean().default(false),
}),
}),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
return {
type: configService.get(ConfigEnum.DB_TYPE),
host: configService.get(ConfigEnum.DB_HOST),
port: configService.get(ConfigEnum.DB_PORT) || 3306,
username: configService.get(ConfigEnum.DB_USERNAME),
password: configService.get(ConfigEnum.DB_PASSWORD),
database: configService.get(ConfigEnum.DB_DATABASE),
synchronize: configService.get(ConfigEnum.DB_SYNC) || false,
entities: [],
logging: process.env.NODE_ENV === 'development' ? true : ['error'],
} as TypeOrmModuleOptions;
},
}),
],
controllers: [],
providers: [],
})
export class AppModule {}
创建配置文件:./enum/config.enum.ts
/** mysql数据库的配置枚举 */
export enum ConfigEnum {
DB_TYPE = 'DB_TYPE',
DB_DATABASE = 'DB_DATABASE',
DB_HOST = 'DB_HOST',
DB_PORT = 'DB_PORT',
DB_USERNAME = 'DB_USERNAME',
DB_PASSWORD = 'DB_PASSWORD',
DB_SYNC = 'DB_SYNC',
}
typeorm创建表结构
接下来需要创建4个实体类:user.entity.ts
,profile.entity.ts
,logs.entity.ts
,roles.entity.ts
,typeorm会每个将每个实体类创建一张对应的表,同时会根据我们稍后的语法生成一张额外的users_roles
表
下图是用vscode插件
Draw.io Integration
,用法参考前面的文章
创建src/user/user.entity.ts
实体类
import { Logs } from 'src/logs/logs.entity';
import { Roles } from 'src/roles/roles.entity';
import { Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryGeneratedColumn,} from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true }) // options设置用户名唯一
username: string;
@Column()
password: string;
// user表和logs表是一对多的关系,
// 在user表中用logs字段保存了该用户所有的日志(用数组保存),
// 在logs表中用user字段保存了对应的用户信息,并且会在logs中自动生成一个userId字段做关联
@OneToMany(() => Logs, (logs) => logs.user)
logs: Logs[];
// user表和roles表是多对多的关系
@ManyToMany(() => Roles, (roles) => roles.users)
@JoinTable({ name: 'users_roles' }) // 自动生成一张users_roles表,name参数是给该表命名。这这张表会记录user表和roles表的关联字段
roles: Roles[];
// profile表和user表是一对一的关系
@OneToOne(
() => Profile,
(profile) => profile.user // 连表查询的时候对应profile表中的user字段
)
profile: Profile
}
创建src/user/profile.entity.ts
实体类
import { User } from './user.entity';
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column()
gender: number;
@Column()
photo: string;
// profile表和user表是一对一的关系
@OneToOne(() => User)
@JoinColumn() // 会在这张表中自动生成关联userId字段
user: User;
}
创建src/logs/logs.entity.ts
实体类
@Entity()
export class Logs {
@PrimaryGeneratedColumn()
id: number;
@Column()
path: string;
@Column()
method: string;
@Column()
data: string;
@Column()
result: number;
// logs表和user表是多对一的关系,
@ManyToOne(() => User, (user) => user.logs)
@JoinColumn() // 会在这张表中自动生成关联userId字段(这个装饰器可以省略)
user: User;
}
创建src/roles/roles.entity.ts
实体类
import { User } from 'src/user/user.entity';
@Entity()
export class Roles {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
// user表和logs表是一对多的关系,
@ManyToMany(() => User, (user) => user.roles)
users: User[];
}
在入口模块app.module.ts
文件中引入上面创建的4个实体类,修改TypeOrmModule
的entities
配置。
/** 连接mysql数据库 */
TypeOrmModule.forRootAsync({
...
- entities: [],
+ entities: [User, Profile, Logs, Roles],
...
}),
查询数据库会发现创建了5张表
用已有数据库生成typeorm实体类
安装插件link:npm i typeorm-model-generator -D
在package.json
中添加一条命令
"generate:models": "typeorm-model-generator -h 11.111.111.111 -p 3306 -d testdb -u username -x !password -e mysql -o ./src/entites"
运行命令:
npm run generate:models
,会在目录中输出实体类代码文件
typeorm进行数据库的增删改查
基本的增删改查
在src/user/user.module.ts
中引入typeorm模块
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserController } from './user.controller';
import { User } from './user.entity';
import { UserService } from './user.service';
@Module({
+ imports: [TypeOrmModule.forFeature([User])],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
编辑src/user/user.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User) private readonly userRepo: Repository<User>,
) {}
// 查询所有数据
findAll() {
return this.userRepo.find();
}
// 查询某一条数据
findOne(id: number) {
if (!id) return;
return this.userRepo.findOne({ where: { id } });
}
// 创建数据
async create(username: string, password: string) {
const user = await this.userRepo.create({ username, password });
return this.userRepo.save(user);
}
// 更新数据
async update(id: number, attrs: Partial<User>) {
const user = await this.findOne(id);
if (!user) throw new NotFoundException('user not found');
Object.assign(user, attrs);
await this.userRepo.update(id, user);
return this.findOne(id);
}
// 查询所有数据
async remove(id: number) {
return this.userRepo.delete(id);
}
}
在src/user/user.controller.ts
中调用userService
import { Controller, Get, Post } from '@nestjs/common';
import { UserService } from './user.service';
import { ConfigService } from '@nestjs/config';
@Controller('user')
export class UserController {
constructor(
private userService: UserService,
private configService: ConfigService,
) {}
@Get()
getUsers() {
return this.userService.findAll();
}
@Post()
addUser() {
return this.userService.create('kgm', 'password');
}
}
一对一关联查询
在src/user/user.service.ts
中增加方法
// 关联查询用户信息
findProfile(id: number) {
return this.userRepo.findOne({
where: { id },
relations: { profile: true, }, // 连表查询出profile的信息,如果不连表查询就没有profile字段信息
})
}
一对多关联查询
在src/user/user.service.ts
中增加方法
// 关联查询用户信息
return this.userRepo.find({
where: { id },
relations: {
logs: true, // 连表查询出所有的logs的信息,如果不连表查询就没有logs字
},
})
多对一关联查询
在src/user/user.service.ts
中增加方法(需要在user.module.ts中引入Logs实体类,此处省略代码)
@Injectable()
export class UserService {
constructor(
@InjectRepository(User) private readonly userRepo: Repository<User>,
@InjectRepository(Logs) private readonly logsRepo: Repository<Logs>,
) {}
// 查询当前用户日志
async findUserLogs(id: number) {
const user = await this.findOne(id)
return this.logsRepo.find({
where: { user },
relations: { user: true, }, // 连表查询出user的信息,如果不连表查询就没有user字段信息
})
}
}
使用QueryBuilder进行联合查询
在src/user/user.service.ts
添加方法
// 获取日志并分组
findLogsByGroup(id: number) {
return this.logsRepo
.createQueryBuilder('logs')
.select('logs.result', 'result') // 查找出result字段
.addSelect('COUNT("logs.result")', 'count') // 统计数量
.leftJoinAndSelect('logs.user', 'user')
.where('user.id = :id', { id }) // 通过id查找用户信息
.groupBy('logs.result') // 分组
.orderBy('count', 'DESC') // 倒序排列
.addOrderBy('result', 'DESC') // 添加倒序排列
.offset(1) // 跳过几条
.limit(2) // 只查2条
.getRawMany()
}
上面的查询最后生成的sql语句如下
SELECT `logs`.`result` AS `result`, `user`.`id` AS `user_id`, `user`.`username` AS `user_username`, `user`.`password` AS `user_password`, COUNT("logs.result") AS `count` FROM `logs` `logs` LEFT JOIN `user` `user` ON `user`.`id`=`logs`.`userId` WHERE `user`.`id` = ? GROUP BY `logs`.`result` ORDER BY count DESC, result DESC LIMIT 2 OFFSET 1
使用原生sql语法,在src/user/user.service.ts
修改方法
// 获取日志并分组
findLogsByGroup(id: number) {
return this.logsRepo.query('SELECT * FROM logs WHERE userId=2')
// 或者下面的查询
return this.logsRepo.query(
'SELECT logs.result as result, COUNT(logs.result) as count from logs, user WHERE user.id = logs.userId AND user.id = 2 GROUP BY logs.result',
)
}
实践案例
列表分页查询:GET /api/user?current=1&pageSize=4&role=1&gender=1&username=kjw
存在user|profile|roles
表,查询满足条件的用户列表
- current: 查询第几页
- pageSize:每一页查询数量
- role: 要查询的用户角色id
- gender:用户的性别
- username: 用户名
查询方案1:
// 查询所有用户数据
async findAll(query: getUserDto) {
const { current = 1, pageSize = 10, username, role, gender } = query
const skip = (current - 1) * pageSize
// 主表是user表
return this.userRepo.find({
relations: { profile: true, roles: true}, // 要关联查询的表
// 配置输出的字段
select: {
id: true, username: true, // 输出: user.id, user.username
profile: { gender: true }, // 输出: profile.gender
roles: { name: true }, // 输出: roles.name
},
// 查询筛选条件,满足以下条件才会筛选出来
where: { username, profile: { gender }, roles: { id: role } },
take: pageSize, // 每页的查询条数
skip, // 跳过多少条查询
})
}
查询方案2:
...
const queryBuilder = this.userRepo
.createQueryBuilder('user')
.leftJoinAndSelect('user.profile', 'profile')
.leftJoinAndSelect('user.roles', 'roles')
// 三元表达式,处理查询条件不存在产生查询有误的问题
.where(username ? 'user.username = :username' : '1=1', { username })
.andWhere(gender ? 'profile.gender = :gender' : '1=1', { gender })
.andWhere(role ? 'roles.id = :role' : '1=1', { role })
return queryBuilder.take(pageSize).skip(skip).getMany()
...
查询方案3:
// 创建工具函数
import { SelectQueryBuilder } from 'typeorm'
export const conditionUtils = <T>(queryBuilder: SelectQueryBuilder<T>, obj: Record<string, unknown>): SelectQueryBuilder<T> => {
Object.keys(obj).forEach((key) => {
if (obj[key]) queryBuilder.andWhere(`${key} = :${key}`, { [key]: obj[key] })
})
return queryBuilder
}
// 下面是查询的真正实现
...
const obj = { 'user.username': username, 'profile.gender': gender, 'roles.id': role }
const queryBuilder = this.userRepo
.createQueryBuilder('user')
.leftJoinAndSelect('user.profile', 'profile')
.leftJoinAndSelect('user.roles', 'roles')
const newQuery = conditionUtils<User>(queryBuilder, obj)
return newQuery.take(pageSize).skip(skip).getMany()
...
添加用户数据:POST /api/user
请求体body:{"username":"kgm@qq.com", "password":"123456"}
在添加用户过程中,需要验证
username
字段的唯一性,可以通过修改user.entity.ts
中user实例的username
字段定义简化验证操作
@Entity()
export class User {
...
@Column({unique: true}) // 配置{unique: true},告诉数据库username唯一
username: string
...
}
异常处理方式1: 直接捕获错误
// 创建数据
async create(dto: User) {
const user = await this.userRepo.create(dto)
try {
return await this.userRepo.save(user)
} catch (error) {
// 错误处理(用户名重复错误)
if (error.errno && error.errno === 1062) throw new HttpException(error?.sqlMessage, 500)
}
}
异常处理方式2:全局捕获错误
// 创建数据
async create(dto: User) {
const user = await this.userRepo.create(dto)
return this.userRepo.save(user)
}
修改全局异常捕获文件./src/filters/all-exception.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, LoggerService } from '@nestjs/common'
import { HttpAdapterHost } from '@nestjs/core'
import { Request, Response } from 'express'
import * as requestIp from 'request-ip'
import { QueryFailedError } from 'typeorm'
@Catch()
export class AllExceptionFilter implements ExceptionFilter {
constructor(
private logger: LoggerService,
private httpAdapterHost: HttpAdapterHost,
) {}
catch(exception: unknown, host: ArgumentsHost) {
const { httpAdapter } = this.httpAdapterHost
const ctx = host.switchToHttp()
const req: Request = ctx.getRequest() // 请求对象
const res: Response = ctx.getResponse() // 响应对象
// http状态码
const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR
// 加入更多错误逻辑-------------------------------------开始
let errorMsg = exception['response'] || 'Internal Server Error'
if (exception instanceof QueryFailedError) {
errorMsg = exception.message
// if (exception.driverError.errno === 1062) errorMsg = '唯一索引冲突' // 更详细
}
// 加入更多错误逻辑-------------------------------------结束
const resBody = {
Headers: req.headers,
query: req.query,
body: req.body,
params: req.params,
timestamp: new Date().toISOString(),
ip: requestIp.getClientIp(req),
exception: exception['name'],
error: errorMsg,
}
// 打印日志
this.logger.error('[toimic]', resBody)
// 返回接口
const resData = {
code: status, msg: resBody.error,
// timestamp: new Date().toDateString(),
}
httpAdapter.reply(response, resData, status)
}
}
在main.ts
中配置全局异常处理器
import { HttpAdapterHost, NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'
import { AllExceptionFilter } from './filters/all-exception.filter'
async function bootstrap() {
const app = await NestFactory.create(AppModule, {})
const logger = app.get(WINSTON_MODULE_NEST_PROVIDER)
app.useLogger(logger)
app.setGlobalPrefix('api') // 给每一个接口添加前缀'/api'
// 捕获所有异常
+ app.useGlobalFilters(new AllExceptionFilter(logger, app.get(HttpAdapterHost)))
await app.listen(9010)
}
bootstrap()
异常处理方式3:局部捕获错误
创建一个异常处理过滤器nest g f filters/typeorm --flat --no-spec
,会生成一个src/filters/typeorm.filter.ts
文件
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'
import { QueryFailedError, TypeORMError } from 'typeorm'
import { Response } from 'express'
@Catch(TypeORMError)
export class TypeormFilter implements ExceptionFilter {
catch(exception: TypeORMError, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response: Response = ctx.getResponse() // 响应对象
// 打印日志
let code = 500
if (exception instanceof QueryFailedError) code = exception.driverError.errno
response.status(500).json({
code: code,
// timestamp: new Date().toDateString(),
// path: request.url, method: request.method,
msg: exception.message,
})
}
}
在user.controller.ts
中使用
@Controller('user')
+ @UseFilters(new TypeormFilter())
export class UserController {
constructor(
private userService: UserService,
) {
}
}
更新有关联关系的用户
请求信息如下,会同时修改user
表中的用户和该用户对应的profile
表中的信息
PATCH {{host}}/api/user/2
content-type: application/json
Authorization: 1
{
"id": 2,
"username": "lyl@qq.com",
"password": "1234567",
"profile": {"id": 2, "gender": 2, "photo": "lyl photo 111", "address": "lyl address 111"}
}
改造user.entity.ts
@Entity()
export class User {
...
@OneToMany(() => Logs, (logs) => logs.user, {
// 更新的时候会连同logs信息一起修改
cascade: true,
})
logs: Logs[]
@ManyToMany(() => Roles, (roles) => roles.users, {
// 更新的时候会连同roles信息一起修改,只有在'insert','update'的情况下才会同步更新
cascade: ['insert', 'update'],
})
@JoinTable({ name: 'users_roles' }) // 生成中间表,name(给中间表命名)
roles: Roles[]
@OneToOne(() => Profile, (profile) => profile.user, {
+ cascade: true, // 更新的时候会连同profile信息一起修改
})
profile: Profile
}
改造profile.entity.ts
@Entity()
export class Profile {
...
@OneToOne(() => User, {
// 当实例user被删除时删除,有权删除对应的profile数据(呼应上面user表中关于profile的cascade配置)
// 这里为什么不能设置cascade: true呢,应为可能导致profile和user循环删除而报错
+ onDelete: 'CASCADE',
})
...
}
修改user.service.ts
...
// 查询所有数据
async update(id: number, attrs: Partial<User>) {
const userTemp = await this.findProfile(id)
const newUser = this.userRepo.merge(userTemp, attrs)
// 联合模型更新,需要使用save方法或者queryBuilder
return this.userRepo.save(newUser)
}
...
修改user.controller.ts
/**
* 更新用户信息
* @param dto
* @param id
* @returns
*/
@Patch(':id')
updateUser(@Body() dto: any, @Param('id') id: number, @Headers('Authorization') Authorization: any) {
// 权限1,判断用户是否是自己
if (id === Authorization) {
// 权限2,判断用户是否有更新的权限
// 返回数据不能包含password等信息
const user = dto as Partial<User>
return this.userService.update(id, user)
} else {
// 不是同一个用户
throw new UnauthorizedException()
}
}