NestJS + TypeORM 一对多 / 多对一 / 一对一 增删改查
核心:NestJS 一般搭配 TypeORM 做关系映射,下面先讲三者在实现上的区别,再给最简可运行示例(一对多 + 多对一,因为一对多和多对一本质是同一条关系的两端)
一、三种关系实现核心区别
1. 一对多 & 多对一(最常用,二者成对出现)
举例:用户(User) 一对多 订单(Order);订单(Order) 多对一 用户(User)
-
多的一方(Order):持有外键 ,数据库表会生成
userId字段 -
@ManyToOne():放在多的那端(Order),自动维护外键 -
@OneToMany():放在一的那端 (User),不会新增数据库字段 ,仅用于关联查询,@OneToMany必须搭配@ManyToOne -
特点:
- 新增:保存订单时传入用户 ID 即可;也可以传入实体对象
- 查询:可
leftJoinAndSelect联查用户 / 订单列表 - 删除:有级联选项
cascade,谨慎使用;默认不会级联删除
-
一句话:外键在多的一方
2. 一对一(OneToOne)
举例:用户(User) 一对一 用户资料(UserProfile),一个用户只有一份资料
-
外键只在其中一张表 ,需要手动指定
@JoinColumn(),谁写 @JoinColumn,谁的表就产生外键 -
@OneToOne两边都要声明 -
特点:
- 业务上一一对应,不能重复关联
- 查询时联查;新增时需要保证唯一
- 很少用,很多场景可以直接合并成一张表
-
一句话:外键在写了 @JoinColumn 的那张表
快速对比表
表格
| 关系 | 外键位置 | 装饰器 | 典型场景 |
|---|---|---|---|
| 一对多 | 多的那张表 | @OneToMany(一端)@ManyToOne(多端) |
用户 - 订单,分类 - 商品 |
| 多对一 | 多的那张表 | @ManyToOne(多端) |
订单所属用户 |
| 一对一 | 写@JoinColumn的表 |
@OneToOne + @JoinColumn |
用户 - 个人档案 |
注意:没有
@ManyToOne单独存在的情况,@OneToMany必须配合@ManyToOne;@OneToMany本身不在数据库创建字段。
二、完整示例:一对多 + 多对一(User <-> Order)
1. 实体定义
user.entity.ts(一的一方:一个用户多个订单)
typescript
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { Order } from './order.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ nullable: true })
email: string;
// 一对多:一个用户拥有多个订单
// 第一个参数:关联实体,第二个:反向关联字段(Order里的user)
@OneToMany(() => Order, (order) => order.user, {
cascade: true, // 级联保存,可选,生产慎用
})
orders: Order[];
}
order.entity.ts(多的一方:多个订单属于一个用户)
less
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';
@Entity()
export class Order {
@PrimaryGeneratedColumn()
id: number;
@Column()
orderNo: string;
@Column('decimal')
amount: number;
// 多对一:多个订单归属一个用户,外键 userId 生成在 order 表
@ManyToOne(() => User, (user) => user.orders)
@JoinColumn({ name: 'userId' }) // 指定外键列名,不写默认 user.id
user: User;
@Column()
userId: number; // 显式定义外键字段,方便直接传id操作
}
2. Service 层 CRUD
typescript
// user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { Order } from './order.entity';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepo: Repository<User>,
@InjectRepository(Order)
private readonly orderRepo: Repository<Order>,
) {}
// 创建用户
async createUser(name: string, email: string) {
const user = this.userRepo.create({ name, email });
return this.userRepo.save(user);
}
// 创建订单(关联用户)
async createOrder(userId: number, orderNo: string, amount: number) {
const order = this.orderRepo.create({
userId,
orderNo,
amount,
});
return this.orderRepo.save(order);
}
// 查询用户,连带订单列表
async findUserWithOrders(id: number) {
return this.userRepo.findOne({
where: { id },
relations: ['orders'], // 加载关联
});
}
// 查询订单,连带所属用户信息
async findOrderWithUser(id: number) {
return this.orderRepo.findOne({
where: { id },
relations: ['user'],
});
}
// 更新订单
async updateOrder(id: number, data: Partial<Order>) {
await this.orderRepo.update(id, data);
return this.orderRepo.findOneBy({ id });
}
// 删除订单
async deleteOrder(id: number) {
return this.orderRepo.delete(id);
}
}
3. Controller
less
import { Controller, Post, Get, Put, Delete, Body, Param } from '@nestjs/common';
import { UserService } from './user.service';
@Controller()
export class AppController {
constructor(private readonly userService: UserService) {}
@Post('user')
createUser(@Body() dto: { name: string; email: string }) {
return this.userService.createUser(dto.name, dto.email);
}
@Post('order')
createOrder(@Body() dto: { userId: number; orderNo: string; amount: number }) {
return this.userService.createOrder(dto.userId, dto.orderNo, dto.amount);
}
@Get('user/:id')
getUser(@Param('id') id: number) {
return this.userService.findUserWithOrders(id);
}
@Get('order/:id')
getOrder(@Param('id') id: number) {
return this.userService.findOrderWithUser(id);
}
@Put('order/:id')
updateOrder(@Param('id') id: number, @Body() body) {
return this.userService.updateOrder(id, body);
}
@Delete('order/:id')
delOrder(@Param('id') id: number) {
return this.userService.deleteOrder(id);
}
}
三、一对一简单示例(对比参考)
less
// user.entity.ts
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToOne(() => UserProfile, profile => profile.user)
profile: UserProfile;
}
// user-profile.entity.ts
@Entity()
export class UserProfile {
@PrimaryGeneratedColumn()
id: number;
@Column()
address: string;
@OneToOne(() => User, user => user.profile)
@JoinColumn() // 这里写JoinColumn,外键 userId 在 user_profile 表
user: User;
}
四、开发踩坑要点
relations只适合简单查询;复杂联查推荐createQueryBuilder,性能更好cascade级联保存 / 删除,生产环境尽量不要全局开启,容易误删数据- 外键字段可以显式声明(如
userId),接口传 ID 时更方便,不用传完整实体对象 @OneToMany只是虚拟关联,数据库不会生成字段;只用于查询时加载关联数据- 删除主表数据时,数据库外键约束默认会报错,需要手动处理子表数据或者设置外键
ON DELETE
五、三种关系增删改查逻辑小结
-
一对多(用户 - 订单)
- 新增:新增用户;新增订单填 userId
- 查询:
relations: ['orders']获取用户下所有订单 - 修改:直接修改订单,修改用户订单列表需要操作数组
- 删除:删订单无影响;删用户要先处理订单,否则外键报错
-
多对一(订单 - 用户)
- 本质就是一对多的反向视角,代码写在多端,持有外键
-
一对一(用户 - 档案)
- 新增:创建用户,创建档案绑定 userId
- 查询:
relations: ['profile'] - 约束:要保证关联唯一,不能多个档案绑定同一个用户
六、核心区别对比表
| 维度 | 一对一 (1:1) | 一对多 (1:N) | 多对一 (N:1) |
|---|---|---|---|
| 装饰器 | @OneToOne |
@OneToMany |
@ManyToOne |
| 外键位置 | 拥有方(@JoinColumn) |
无外键(在多方) | 多方表 |
| 属性类型 | 单个对象 Profile |
数组 Post[] |
单个对象 User |
@JoinColumn |
拥有方需要 | 不需要 | 需要(默认也会建) |
| 是否拥有外键 | 拥有方有 | 无 | 有 |
| 级联方向 | 拥有方 → 被拥有方 | 一方 → 多方 | 多方 → 一方 |
七、易踩的坑
-
一对多的
@JoinColumn不要加 :@OneToMany不拥有外键,加@JoinColumn会报错或行为异常。 -
循环依赖 :双向关系用箭头函数
() => Post延迟解析,不要用Post直接引用。 -
eager vs relations :
eager: true会全局自动加载,可能造成性能问题;推荐显式relations。 -
cascade 与 onDelete 的区别:
cascade是 TypeORM 层面的(保存/删除时级联操作)onDelete是数据库外键层面的(DB 约束)
-
userId与user同时存在 :可以既暴露userId(方便赋值)又有user关系(方便加载),但要注意同步