ORM {#ORM}
对象关系映射器ORM(Object/Relational Mapping) 通过实例对象的语法,完成关系型数据库的操作。
ORM 把数据库映射成对象 ORM 实例教程-阮一峰
- 数据库的表(table) ---> 类(class)
- 记录(record,行数据)---> 对象(object)
- 字段(field)---> 对象的属性(attribute)
ORM 使用对象,封装了数据库操作,不使用 SQL 语言,只面向对象编程,与数据对象直接交互,在更高的抽象层次上操作数据库。因此可以通过一个 ORM 框架,操作多种数据库。让开发者更专注业务逻辑的处理。
TypeORM {#TypeORM}
TypeORM 是TS生态中最成熟的ORM,TypeORM中文文档
Nest 提供了与它的紧密集成库 @nestjs/typeorm,文档
安装: npm i -S @nestjs/typeorm typeorm
还需要安装node侧的数据库驱动程序,如 mysql2、pg、sqlite3 等
安装: npm i -S mysql2
连接数据库 {#连接数据库}
TypeOrmModule
是全局动态模块,用于注册数据库连接,并进行一些配置。
forRoot()
方法支持 TypeORM 中 DataSource 构造函数暴露的所有配置属性,并且还有一些额外的配置属性。
基本配置
|---------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Module({ imports: [ TypeOrmModule.forRoot({ type: "mysql", //数据库类型 username: "root", //账号 password: "123456", //密码 host: "localhost", //host port: 3306, // database: "test", //库名 entities: [__dirname + '/**/*.entity{.ts,.js}'], //实体文件 synchronize:true, //synchronize字段代表是否自动将实体类同步到数据库 // 下面三个是Nest提供的额外配置 retryDelay:500, //重试连接数据库间隔 retryAttempts:10,//重试连接数据库的次数 autoLoadEntities:true, //如果为true,将自动加载实体 forFeature()方法注册的每个实体都将自动添加到配置对象的实体数组中 cache: true, //启用查询缓存 // cache: { // duration: 30000, //默认缓存时间为 1000 毫秒,可以传入数字指定缓存时间 // } }), ], }) export class AppModule {}
|
使用 forRootAsync()
异步注册方式,注入 ConfigService
进行配置。
|---------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| TypeOrmModule.forRootAsync({ useFactory: (configService: ConfigService) => { const config = configService.get<Configuration>(configToken).database; return { type: config.type, //数据库类型 username: config.username, //账号 password: config.password, //密码 host: config.host, //host port: config.port, // database: config.name, //库名 // ......其他配置 } as TypeOrmModuleOptions; }, inject: [ConfigService], }),
|
配置完成后,TypeORM 的 Repository
对象将可在整个项目中 通过 @InjectRepository()
注入(无需导入任何模块)
注册实体 {#注册实体}
在 forRoot()
的 entities
选项中统一注册实体文件,支持静态 glob 路径和 entities 数组
|-------------|-----------------------------------------------------------------------------------------------------|
| 1 2
| entities: [__dirname + '/**/*.entity{.ts,.js}'], // 静态 glob 路径数组 entities: [User], // 实体类数组
|
也可以开启 autoLoadEntities
自动加载实体,并使用 forFeature()
方法在不同模块中注册实体,这些局部注册的实体都将自动添加到配置对象的实体数组中。
|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4
| //自动加载实体,forFeature()方法注册的每个实体都将自动添加到配置对象的实体数组中 autoLoadEntities: true, // 在其它模块中使用 forFeature() 注册实体 TypeOrmModule.forFeature([entity])
|
实体 {#实体}
TypeORM中,实体是由 @Entity()
注释类,用于映射数据库表。文档
提供了 @Column()
等实体属性装饰器,用于定义列等数据库表的结构和信息。
|------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10
| import {Entity,Column,PrimaryGeneratedColumn} from 'typeorm' @Entity() export class Guard { // 自增主键列 @PrimaryGeneratedColumn() id:number // 普通列 @Column() name:string }
|
在开发环境 开启 synchronize
配置项,TypeORM 会根据实体 的定义自动创建 数据库表,且在每次应用启动时 都会检查实体类和表的同步状态,如果不同步则会自动更新表结构。
|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3
| synchronize: true, //synchronize字段代表是否自动将实体类同步到数据库 // 设置为true,表示每次应用启动时都会检查实体类和数据库表的同步状态,如果不同步则会自动更新数据库表结构 // 生产环境应设置为false,避免自动更新数据库表结构,否则可能会丢失生产数据。
|
实体属性装饰器 {#实体属性装饰器}
常用实体属性装饰器:
@Entity()
声明实体@PrimaryColumn()
主键列@PrimaryGeneratedColumn()
自增主键列@Column()
普通列@Generated()
生成列,能自动生成值,如UUID等。@CreateDateColumn()
*特殊列,自动为实体插入创建时间。@UpdateDateColumn()
*特殊列,每次save时自动更新时间。@DeleteDateColumn
*特殊列,软删除标记列,初始值为null,软删除时记录删除时间。@VersionColumn()
*特殊列,每次save时自动增长实体版本(增量编号)
*特殊列的值将根据内置规则自动设置,无需手动赋值。 user实体 src\db\entities\user.entity.ts
|------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { Column, CreateDateColumn, DeleteDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() desc: string; @CreateDateColumn({ type: 'timestamp' }) create_time: Date; @UpdateDateColumn({ type: 'timestamp' }) update_time: Date; @DeleteDateColumn({ type: 'timestamp' }) delete_time: Date; }
|
Column选项 {#Column选项}
@Column()
装饰器可以接受ColumnOptions选项,用于定义列的属性。
|------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| type: ColumnType - 列类型。https://typeorm.bootcss.com/entities#列类型 name: string - 数据库表中的列名。默认情况下,列名称是从属性的名称生成的。 你也可以通过指定自己的名称来更改它。 length: number - 列类型的长度。 例如,如果要创建varchar(150)类型,请指定列类型和长度选项。 width: number - 列类型的显示范围。 仅用于MySQL integer types onUpdate: string - ON UPDATE触发器。 仅用于 MySQL. nullable: boolean - 在数据库中使列NULL或NOT NULL。 默认情况下,列是nullable:false。 update: boolean - 指示"save"操作是否更新列值。如果为false,则只能在第一次插入对象时编写该值。 默认值为"true"。 select: boolean - 定义在进行查询时是否默认隐藏此列。 设置为false时,列数据不会显示标准查询。 默认情况下,列是select:true default: string - 添加数据库级列的DEFAULT值。 primary: boolean - 将列标记为主要列。 使用方式和@ PrimaryColumn相同。 unique: boolean - 将列标记为唯一列(创建唯一约束)。 comment: string - 数据库列备注,并非所有数据库类型都支持。 precision: number - 十进制(精确数字)列的精度(仅适用于十进制列),这是为值存储的最大位数。仅用于某些列类型。 scale: number - 十进制(精确数字)列的比例(仅适用于十进制列),表示小数点右侧的位数,且不得大于精度。 仅用于某些列类型。 zerofill: boolean - 将ZEROFILL属性设置为数字列。 仅在 MySQL 中使用。 如果是true,MySQL 会自动将UNSIGNED属性添加到此列。 unsigned: boolean - 将UNSIGNED属性设置为数字列。 仅在 MySQL 中使用。 charset: string - 定义列字符集。 并非所有数据库类型都支持。 collation: string - 定义列排序规则。 enum: string[]|AnyEnum - 在enum列类型中使用,以指定允许的枚举值列表。 你也可以指定数组或指定枚举类。 asExpression: string - 生成的列表达式。 仅在MySQL中使用。 generatedType: "VIRTUAL"|"STORED" - 生成的列类型。 仅在MySQL中使用。 hstoreType: "object"|"string" -返回HSTORE列类型。 以字符串或对象的形式返回值。 仅在Postgres>)中使用。 array: boolean - 用于可以是数组的 postgres 列类型(例如 int []) transformer: { from(value: DatabaseType): EntityType, to(value: EntityType): DatabaseType } - 用于将任意类型EntityType的属性编组为数据库支持的类型DatabaseType。
|
注意:大多数列选项都是特定于 RDBMS 的,并且在MongoDB中不可用。
操作实体-Repositories {#操作实体-Repositories}
配置好 TypeOrmModule
并注册了实体后,使用 @InjectRepository()
注入 Repository
存储库类。
每个实体都有自己的 Repository
存储库,可以处理其实体的所有操作。通过调用 Repository
的方法,实现对数据库的增删改查。
src\db\db.service.ts
|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9
| import { InjectRepository } from '@nestjs/typeorm'; import { User } from './entities/user.entity'; import { Repository } from 'typeorm'; @Injectable() export class DbService { constructor( @InjectRepository(User) private userRepository: Repository<User>, ) {} }
|
CURD {#CURD}
通过实体存储库 Repository
实现CURD,Repository API
|------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 1. `manager` 存储库使用的 `EntityManager` 2. `metadata` 存储库管理的实体的 `EntityMetadata` 3. `queryRunner` EntityManager 使用的查询器。仅在 EntityManager 的事务实例中使用。 4. `target` 此存储库管理的目标实体类。仅在 EntityManager 的事务实例中使用。 5. `createQueryBuilder()` 创建用于构建 SQL 查询的查询构建器。详见[QueryBuilder](https://typeorm.bootcss.com/select-query-builder) 6. `hasId()` 检查是否定义了给定实体的主列属性。 7. `getId()` 获取给定实体的主列属性值。复合主键返回的值将是一个具有主列名称和值的对象。 8. `create()` 创建一个新的实体实例。可选接收具有实体属性的对象,将写入新创建的实体实例。相当于 `new User()` 后再往实例上添加属性。 9. `merge()` 将多个实体合并为一个实体。 10. `preload()` 将给定的实体与数据库中的实体进行比较,并返回一个补全了缺失的属性新的实体。 11. `save()` 保存(插入)给定实体或实体数组,返回保存后的实体值。若主键已存在,则会更新该实体,否则插入一个新实体。 12. `insert()` 插入新实体或实体数组。 13. `update()` 通过给定的更新选项或实体 ID 部分更新实体。 14. `upsert()` 与 `save()` 类似,但不包含级联、关系和其他操作。 15. `remove()` 删除给定的实体或实体数组。 16. `delete()` 根据实体id、id数组或给定的条件删除实体。 17. `softDelete()` 软删除,参数同 `delete()`。 18. `softRemove()` 软删除,参数同 `remove()`。 19. `recover()` 传入实体或实体数组,恢复软删除的实体。 20. `restore()` 根据实体id、id数组或给定的 `FindOptionsWhere` 条件,恢复软删除的实体。 21. `increment()` 增加符合条件的实体某些列值。 22. `decrement()` 减少符合条件的实体某些列值。 23. `find()` 查询匹配Find选项的实体,适用于大多数查询场景。 24. `findAndCount()` 查询匹配Find选项实体。还会计算与给定条件匹配的所有实体数量,但是忽略分页设置 (skip 和 take 选项)。 25. `findOneById()` 按 ID 查询实体。 26. `findByIds()` 按 ID 查询多个实体。 27. `findOne()` 查询匹配Find选项的第一个实体。 28. `findOneOrFail()` 查询匹配Find选项的第一个实体。如果没有匹配,则 Rejects 一个 promise。 29. `exist()` 检查是否存在匹配Find选项的实体。 30. `count()` 符合指定条件的实体数量。对分页很有用。 31. `query()` 执行原始 SQL 语句。 32. `clear()` 清除给定表中的所有数据。
|
创建/插入 {#创建-插入}
create()
创建一个新的实体实例。可选接收具有实体属性的对象,将写入新创建的实体实例。相当于实例化User类后再添加属性。save()
保存(插入)给定实体或实体数组,返回保存后的实体值。若主键已存在,则会更新该实体,否则插入一个新实体。insert()
插入新实体或实体数组。返回InsertResult
对象,包含插入的实体的标识符和生成的映射。upsert()
与save()
类似,但不包含级联、关系和其他操作。
|------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| async create(createDbDto: CreateDbDto) { const user = this.userRepository.create({ name: createDbDto.name, desc: createDbDto.desc, }); // 创建一个实体 const result = await this.userRepository.save(user); // 保存到数据库 // const result = await this.userRepository.insert(user); // 插入到数据库 return result; } // save返回保存后的实体值 // { // "name": "user2", // "desc": "这是一个用户2", // "id": 15, // "create_time": "2024-02-20T05:10:52.740Z", // "update_time": "2024-02-20T05:10:52.740Z" // } // insert返回InsertResult // { // "identifiers": [ // { // "id": 14 // } // ], // "generatedMaps": [ // { // "id": 14, // "create_time": "2024-02-20T05:09:07.306Z", // "update_time": "2024-02-20T05:09:07.306Z" // } // ], // "raw": { // "fieldCount": 0, // "affectedRows": 1, // "insertId": 14, // "info": "", // "serverStatus": 2, // "warningStatus": 0, // "changedRows": 0 // } // }
|
查询 {#查询}
Repository 提供了多种查询方法,通常返回实体或实体数组。
find()
查询匹配Find选项的实体,适用于大多数查询场景。findAndCount()
查询匹配Find选项实体。还会计算与给定条件匹配的所有实体数量,但是忽略分页设置 (skip 和 take 选项)。findOneById()
按 ID 查询实体。findByIds()
按 ID 查询多个实体。findOne()
查询匹配Find选项的第一个实体。findOneOrFail()
查询匹配Find选项的第一个实体。如果没有匹配,则 Rejects 一个 promise。exist()
检查是否存在匹配Find选项的实体。count()
符合指定条件的实体数量。对分页很有用。query()
执行原始 SQL 语句。
大部分查询方法能接收 Find 选项,用于指定查询条件、排序、分页等。
一些查询方法还有 find*By()
版本,传入 FindOptionsWhere
直接根据查询条件查询实体。如 findBy()
、findOneBy()
等。
常用 Find 选项
|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| userRepository.find({ select: ["firstName", "lastName"], // 指定要选择的列 relations: ["profile", "photos", "videos"], // 指定要加载的关系 where: { // 指定查询条件 firstName: "Timber", lastName: "Saw" }, order: { // 指定排序 name: "ASC", // 升序 id: "DESC" // 降序 }, // 分页 skip: 5, // 跳过前5个 take: 10, // 取10个 cache: true // 缓存查询结果,需要在TypeOrmModule配置中启用查询缓存 // cache: 60000 // 默认缓存时间为 1000 毫秒,可以传入数字指定缓存时间 });
|
TypeORM 还提供了许多内置运算符,可用于创建更复杂的查询。部分运算符与 SQL 同名的关键字差不多。
Not()
不等于LessThan()
小于LessThanOrEqual()
小于等于MoreThan()
大于MoreThanOrEqual()
大于等于Equal()
等于Like()
模糊查询,支持 SQL 通配符ILIKE()
不区分大小写的模糊查询,也支持通配符Between()
在两个值之间In()
在给定的值数组中Any()
在给定的值数组中的任意一个,通常配合其它运算符使用IsNull()
为空Raw()
原始 SQL 语句
FindAll查询案例 {#FindAll查询案例}
支持搜索、分页、排序 DTO src\db\dto\find-all.dto.ts
|---------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import { Transform } from 'class-transformer'; import { IsInt, IsOptional, IsString, Min } from 'class-validator'; export class FindAllDto { @IsString() @IsOptional() keyword?: string; @IsInt() @IsOptional() @Min(1) @Transform((p) => parseInt(p.value, 10)) @RequireOtherFields('size') // 当传入了page时,必须同时传入size page?: number; @IsInt() @IsOptional() @Min(0) @Transform((p) => parseInt(p.value, 10)) size?: number; }
|
控制器 src\db\db.controller.ts
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5
| import { FindAllDto } from './dto/find-all.dto'; @Get() findAll(@Query() query: FindAllDto) { return this.dbService.findAll(query); }
|
服务 src\db\db.service.ts
|------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| async findAll(options: FindAllDto) { const result = await this.userRepository.find({ where: { name: options.keyword ? Like(`%${options.keyword}%`) : undefined, }, order: { id: 'ASC', }, skip: options.page && options.size && (options.page - 1) * options.size, take: options.size, }); const total = await this.userRepository.count(); return { users: result, total, }; } // { // "users": [ // { // "id": 1, // "name": "fsfdf", // "desc": "这是46456户", // "create_time": "2024-02-19T13:35:28.264Z", // "update_time": "2024-02-20T06:45:50.000Z", // "delete_time": null // }, // { // "id": 3, // "name": "user1", // "desc": "这是一个用户", // "create_time": "2024-02-19T13:35:38.280Z", // "update_time": "2024-02-19T13:36:19.896Z", // "delete_time": null // } // ], // "total": 16 // }
|
更新/修改 {#更新-修改}
update()
通过给定的更新选项或实体 ID 部分更新实体。save()
保存(插入)给定实体或实体数组,返回保存后的实体值。若主键已存在,则会更新该实体,否则插入一个新实体。upsert()
与save()
类似,但不包含级联、关系和其他操作。
|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9
| async update(id: number, updateDbDto: UpdateDbDto) { const result = await this.userRepository.update(id, updateDbDto); return result; } // { // "generatedMaps": [], // "raw": [], // "affected": 1 // }
|
若需要条件更新,需要与查询方法配合使用,获取需要更新的实体,使用 merge()
合并实体,再调用 save()
或 upsert()
。
|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7
| async update(id: number, updateDbDto: UpdateDbDto) { // const result = await this.userRepository.update(id, updateDbDto); const oldUser = await this.userRepository.findOneBy({ id }); const newUser = this.userRepository.merge(oldUser, updateDbDto); const result = await this.userRepository.save(newUser); return result; }
|
删除 {#删除}
delete()
根据实体id、id数组或给定的FindOptionsWhere
条件删除实体。remove()
删除给定的实体或实体数组。softDelete()
软删除,参数同delete()
。softRemove()
软删除,参数同remove()
。
|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8
| async remove(id: number) { const result = await this.userRepository.delete(id); return result; } // { // "raw": [], // "affected": 1 // }
|
在实际业务中,可能需要软删除 ,即不真正删除数据,而是标记为已删除。
使用 @DeleteDateColumn()
声明标记删除时间的列。就可以通过 soft*()
进行软删除。
|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9
| async remove(id: number) { const result = await this.userRepository.softDelete(id); return result; } // { // "generatedMaps": [], // "raw": [], // "affected": 1 // }
|
恢复软删除 {#恢复软删除}
recover()
传入实体或实体数组,恢复软删除的实体。restore()
根据实体id、id数组或给定的FindOptionsWhere
条件,恢复软删除的实体。
恢复软删除的实体,即将标记删除时间的列置为null。
|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9
| async restore(id: number) { const result = await this.userRepository.restore(id); return result; } // { // "generatedMaps": [], // "raw": [], // "affected": 1 // }
|
完整案例 {#完整案例}
控制器 src\db\db.controller.ts
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| import { Controller, Get, Post, Body, Patch, Param, Delete, Query, ParseIntPipe } from '@nestjs/common'; import { DbService } from './db.service'; import { CreateDbDto } from './dto/create-db.dto'; import { UpdateDbDto } from './dto/update-db.dto'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { FindAllDto } from './dto/find-all.dto'; @Controller('db') @ApiTags('db') export class DbController { constructor(private readonly dbService: DbService) {} @Post() @ApiOperation({ summary: '创建', description: '创建一个用户', }) create(@Body() createDbDto: CreateDbDto) { return this.dbService.create(createDbDto); } @Get() @ApiOperation({ summary: '查询所有', description: '查询所有用户', }) findAll(@Query() query: FindAllDto) { return this.dbService.findAll(query); } @Get(':id') @ApiOperation({ summary: '查询单个', description: '根据id查询单个用户', }) findOne(@Param('id', ParseIntPipe) id: number) { return this.dbService.findOne(id); } @Patch(':id') @ApiOperation({ summary: '更新', description: '根据id更新用户', }) update( @Param('id', ParseIntPipe) id: number, @Body() updateDbDto: UpdateDbDto, ) { return this.dbService.update(id, updateDbDto); } @Delete(':id') @ApiOperation({ summary: '删除', description: '根据id删除用户', }) remove(@Param('id', ParseIntPipe) id: number) { return this.dbService.remove(id); } @Patch('restore/:id') @ApiOperation({ summary: '恢复', description: '根据id恢复用户', }) restore(@Param('id', ParseIntPipe) id: number) { return this.dbService.restore(id); } }
|
服务 src\db\db.service.ts
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| import { Injectable } from '@nestjs/common'; import { CreateDbDto } from './dto/create-db.dto'; import { UpdateDbDto } from './dto/update-db.dto'; import { InjectRepository } from '@nestjs/typeorm'; import { User } from './entities/user.entity'; import { ILike, Like, Repository } from 'typeorm'; import { FindAllDto } from './dto/find-all.dto'; @Injectable() export class DbService { constructor( @InjectRepository(User) private userRepository: Repository<User>, ) {} async create(createDbDto: CreateDbDto) { const user = this.userRepository.create({ name: createDbDto.name, desc: createDbDto.desc, }); // 创建一个实体 // const result = await this.userRepository.save(user); // 保存到数据库 const result = await this.userRepository.insert(user); // 插入到数据库 return result; } async findAll(options: FindAllDto) { const result = await this.userRepository.find({ where: { name: options.keyword ? Like(`%${options.keyword}%`) : undefined, }, order: { id: 'ASC', }, skip: options.page && options.size && (options.page - 1) * options.size, take: options.size, }); return result; } async findOne(id: number) { const result = this.userRepository.findOneBy({ id }); return result; } async update(id: number, updateDbDto: UpdateDbDto) { // const result = await this.userRepository.update(id, updateDbDto); const oldUser = await this.userRepository.findOneBy({ id }); const newUser = this.userRepository.merge(oldUser, updateDbDto); const result = await this.userRepository.save(newUser); return result; } async remove(id: number) { const result = await this.userRepository.softDelete(id); return result; } async restore(id: number) { const result = await this.userRepository.restore(id); return result; } }
|
关系 {#关系}
实际业务中,经常有多个实体之间的关系,如用户和角色、用户和订单等。反映到数据库中,就是表与表之间的关联。在 MySQL 中通过主键和外键来建立表格之间的关系。
TypeORM 支持多种关系,如一对一、一对多、多对多等。文档
关系装饰器:
@OneToOne()
一对一@OneToMany()
一对多@ManyToOne()
多对一@ManyToMany()
多对多
参数:
- 第一个参数是一个函数,返回一个实体类,表示关联的实体。如
@OneToOne(() => Config)
。 - 第二个可选 参数是一个函数,返回一个实体类的属性,表示关联的实体的属性,用于实现双向关系。如
@OneToOne(() => Config, (config) => config.user)
。 - 第三个可选 参数是关系选项。
关系选项: 文档
eager: boolean
如果为 true,则在此实体上使用find*()
,将始终加载此关系。cascade: boolean
如果为 true,则将插入相关对象并在数据库中更新。onDelete: "RESTRICT"|"CASCADE"|"SET NULL"
指定删除引用对象时外键的行为方式primary: boolean
指示此关系的列是否为主列。nullable: boolean
指示此关系的列是否可为空。默认为true。orphanedRowAction: "nullify" | "delete"
将子行从其父行中删除后,确定该子行是孤立的(默认值)还是删除的。
设置外键、中间表:
@JoinColumn()
设置外键 ,用于一对一、多对一关系。@JoinTable()
用于多对多 关系,自动生成一个中间表。
关系可以是单向的和双向的。单向 是仅在一侧有关系装饰器。双向 是两侧都有的关系装饰器。但注意 @JoinColumn()
只在关系的一侧 且拥有外键的表上使用,另一条关系则称为反向关系。
在一对多、多对一关系中,@JoinColumn()
添加至"一"的一侧。
一对一 {#一对一}
以 User
和 Config
实体为例,一个用户只有一个配置,一个配置只属于一个用户。文档
1、 先实现 User
单向关联 Config
。
- 在
User
中新增一个config
属性,通过@JoinColumn()
设置为外键。 - 在使用
@OneToOne()
声明关系,指定关联的实体。
src\db\entities\user.entity.ts
|-------------------|-------------------------------------------------------------------------------------|
| 1 2 3 4 5
| export class User { @OneToOne(() => Config) @JoinColumn() config: Config; }
|
完成单向一对一关系后,User
表中就会新增一个 configId 外键,用于存放 Config
的主键(id)。
可以发现,单向的关系仅需处理主实体,不需要在被关联的实体中做任何处理。
2、 再完成反向关系 ,实现双向的一对一关系。
- 在被关联的
Config
中,新增user
属性(但不使用@Column()
注释为列 ),使用@OneToOne()
声明关系,指定关联的实体,以及第二个参数指定关联的实体的属性。 - 在主实体
User
中,也需要向@OneToOne()
传入第二个参数指定关联的实体的属性。
src\db\entities\config.entity.ts
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8
| export class Config { @PrimaryGeneratedColumn() id: number; @Column() name: string; // 配置名 @OneToOne(() => User, (user) => user.id) user: User; }
|
src\db\entities\user.entity.ts
|-------------------|-------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5
| export class User { @OneToOne(() => Config, (config) => config.id, { cascade: true }) @JoinColumn() config: Config; }
|
接口案例 {#接口案例}
添加用户配置的接口: src\db\db.service.ts
|---------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| async setConfig(params: AddConfigDto) { // 获取需要添加配置的用户 const user = await this.userRepository.findOneBy({ id: params.userId }); // 创建一个配置实体 const config = this.configRepository.create({ name: params.config.name, }); // 若是开启了级联保存,可以省略此步,会自动保存到数据库 await this.configRepository.insert(config); // 插入配置实体 user.config = config; // 关联用户和配置 const result = await this.userRepository.save(user); // 保存到数据库 return result; } // { // "id": 1, // "name": "用户1", // "desc": "这是一个用户", // "create_time": "2024-02-19T13:35:28.264Z", // "update_time": "2024-02-20T14:03:13.000Z", // "delete_time": null, // "config": { // "name": "配置2", // "id": 2 // } // }
|
查询时,需要指定 relations
选项,以加载关联的实体,值为数组,元素是实现关联的属性名。
src\db\db.service.ts
|---------------------|-------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6
| const result = this.userRepository.findOne({ relations: ['config'], where: { id, }, });
|
一对多/多对一 {#一对多-多对一}
以 User
和 Category
实体为例,一个分类下有多个用户,一个用户只属于一个分类。文档
有了一对一关系的基础,下面直接实现双向的一对多/多对一关系。 src\db\entities\user.entity.ts
|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5
| export class User { @ManyToOne(() => Category, (category) => category.users, { cascade: true }) @JoinColumn() category: Category; }
|
src\db\entities\category.entity.ts
|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8
| export class Category { @PrimaryGeneratedColumn() id: number; @Column() name: string; // Category名称 @OneToMany(() => User, (user) => user.category) users: User[]; }
|
完成后,User
表中新增了一个 categoryId 外键,用于存放 Category
的主键(id)。
@JoinColumn()
通常与 @ManyToOne()
在主实体中一起使用,用于指定外键。
实际上 @JoinColumn()
可以完全省略,除非需要自定义关联列(外键)在数据库中的名称,因为在多对一关系中,外键总是在"一"的一侧,TypeORM 会自动添加外键。
多对多 {#多对多}
以 User
和 Tag
实体为例,一个用户有多个标签,一个标签可分配给多个用户。文档
TypeORM 会为多对多关系 自动创建一个中间表 ,用于表示两个实体的关联关系,中间表有两个外键 ,分别指向两个实体的主键。
@JoinTable()
是 @ManyToMany()
关系所必需的,可以放在关系的***任意一侧***。用于生成中间表。
下面直接实现双向的多对多关系。 src\db\entities\user.entity.ts
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5
| export class User { @ManyToMany(() => Tag, (tag) => tag.users, { cascade: true }) // 只能一侧设置级联 @JoinTable() tags: Tag[]; }
|
src\db\entities\tag.entity.ts
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8
| export class Tag { @PrimaryGeneratedColumn() id: number; @Column() name: string; // tag名称 @ManyToMany(() => User, (user) => user.tags) users: User[]; }
|
默认自动生成了名为 user_tags_tag 的中间表,包含两个外键,分别指向 User
和 Tag
的主键。
接口案例 {#接口案例-1}
先实现个创建新标签的接口。 src\db\db.service.ts
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7
| async createTag(createTagDto: CreateTagDto) { const tag = this.tagRepository.create({ ...createTagDto, }); // 创建一个标签实体 const result = await this.tagRepository.save(tag); // 保存到数据库 return result; }
|
再实现给用户添加标签的接口。 src\db\db.service.ts
|---------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| async addTag(addTagDto: AddTagDto) { // 获取用户和标签 const user = await this.userRepository.findOne({ where: { id: addTagDto.userId }, relations: ['tags'], }); const tag = await this.tagRepository.findOne({ where: { id: addTagDto.tagId }, relations: ['users'], }); // 因为启用了级联,所以仅需操作一侧即可 user.tags.push(tag); // 关联用户和标签 const result = await this.userRepository.save(user); // 保存到数据库 return result; }
|
多次添加后,在 user_tags_tag
中间表中,就可以看到用户和标签的,通过各自主键关联的多对多关系。
事务 {#事务}
事务是数据库操作的一种机制,用于保证一组操作的原子性,要么全部成功,要么全部失败。
四大特性:
- 原子性:事务包含的各项操作,是一个不可分割的工作单位,要么全部成功,要么全部失败。任何一项出错都会导致整个事务的失败,同时其它已经执行的操作都将被撤销并回滚,只有所有的操作全部成功,整个事务才算是成功完成。
- 一致性:事务开始前和结束后,数据库的完整性约束没有被破坏。即数据库从一个一致性状态转换到另一个一致性状态。
- 隔离性 :并发的事务是互相隔离的,一个事务的执行不能被其它事务干扰,不同的事务并发操作相同的数据时,每个事务都有各自完整的数据空间。
- 持久性:事务一旦提交后,数据库中的数据必须被永久的保存下来。即使服务器故障。只要数据库重新启动,那么一定能够将其恢复到事务成功结束后的状态。
EntityManager {#EntityManager}
实体管理器 EntityManager 可以操作任何实体,就像是存放实体存储库的集合的地方
EntityManager API 与 Repository
的类似,但 EntityManager
需要指定操作的实体存储库。
通过任意存储库的 manager
属性可以获取 EntityManager
。
在 TypeORM 中,使用 EntityManager.transaction()
来创建并处理事务。
该方法接收一个回调函数,参数为 EntityManager
,所有事务内的CURD等操作,都必须通过 EntityManager
在回调中执行。
|-------------------|---------------------------------------------------------------------------------------------------|
| 1 2 3 4 5
| this.mangerRepository.manager.transaction( async (manager) => { // 通过 manager 事务内的操作 } );
|
转账案例 {#转账案例}
转账分为两步,先从A账户扣款,再给B账户加款。需要事务保证两步操作的原子性,若加款失败,扣款也要回滚。 DTO
|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9
| export class CreateMangerDto { name: string; // 用户 money: number; // 金额 } export class TransferMoneyDto { fromId: number; // 发起人 toId: number; // 接收人 money: number; // 转账金额 }
|
src\manger\manger.service.ts
|---------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| async transferMoney(transferMoneyDto: TransferMoneyDto) { try { await this.mangerRepository.manager.transaction(async (manager) => { const from = await manager.findOneBy(Manger, { id: transferMoneyDto.fromId, }); const to = await manager.findOneBy(Manger, { id: transferMoneyDto.toId, }); if (from.money < transferMoneyDto.money) { throw new HttpException('余额不足', 400); } // 进行转账 from.money -= transferMoneyDto.money; to.money += transferMoneyDto.money; await manager.save(from); await manager.save(to); }); } catch (e) { return { message: e.message, }; } return { message: '转账成功', }; }
|