nestjs 实现一对多,多对一,一对一在实现上的区别

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;
}

四、开发踩坑要点

  1. relations 只适合简单查询;复杂联查推荐 createQueryBuilder,性能更好
  2. cascade 级联保存 / 删除,生产环境尽量不要全局开启,容易误删数据
  3. 外键字段可以显式声明(如 userId),接口传 ID 时更方便,不用传完整实体对象
  4. @OneToMany 只是虚拟关联,数据库不会生成字段;只用于查询时加载关联数据
  5. 删除主表数据时,数据库外键约束默认会报错,需要手动处理子表数据或者设置外键 ON DELETE

五、三种关系增删改查逻辑小结

  1. 一对多(用户 - 订单)

    • 新增:新增用户;新增订单填 userId
    • 查询:relations: ['orders'] 获取用户下所有订单
    • 修改:直接修改订单,修改用户订单列表需要操作数组
    • 删除:删订单无影响;删用户要先处理订单,否则外键报错
  2. 多对一(订单 - 用户)

    • 本质就是一对多的反向视角,代码写在多端,持有外键
  3. 一对一(用户 - 档案)

    • 新增:创建用户,创建档案绑定 userId
    • 查询:relations: ['profile']
    • 约束:要保证关联唯一,不能多个档案绑定同一个用户

六、核心区别对比表

维度 一对一 (1:1) 一对多 (1:N) 多对一 (N:1)
装饰器 @OneToOne @OneToMany @ManyToOne
外键位置 拥有方(@JoinColumn 无外键(在多方) 多方表
属性类型 单个对象 Profile 数组 Post[] 单个对象 User
@JoinColumn 拥有方需要 不需要 需要(默认也会建)
是否拥有外键 拥有方有
级联方向 拥有方 → 被拥有方 一方 → 多方 多方 → 一方

七、易踩的坑

  1. 一对多的 @JoinColumn 不要加@OneToMany 不拥有外键,加 @JoinColumn 会报错或行为异常。

  2. 循环依赖 :双向关系用箭头函数 () => Post 延迟解析,不要用 Post 直接引用。

  3. eager vs relationseager: true 会全局自动加载,可能造成性能问题;推荐显式 relations

  4. cascade 与 onDelete 的区别

    • cascade 是 TypeORM 层面的(保存/删除时级联操作)
    • onDelete 是数据库外键层面的(DB 约束)
  5. userIduser 同时存在 :可以既暴露 userId(方便赋值)又有 user 关系(方便加载),但要注意同步

相关推荐
爱丶不疚5 天前
Electron:Module 与 Service 的职责边界与加载时序编排
前端·electron·nestjs
沐沐师14 天前
Nest.js 微服务入门教程
微服务·nestjs
沐沐师14 天前
Redis 入门教程
redis·nestjs
谷无姜14 天前
QuizForge:在不停踩坑后的技术决策复盘
前端·nestjs
Flynt16 天前
NestJS 12升级踩坑:从Webpack到Rspack,我折腾了一整个周末
typescript·node.js·nestjs
张洪权17 天前
nest.js websocket 群聊----私聊功能
前端·nestjs
用户712828968446718 天前
NestJS 发布v12版本,看一下有哪些改动
nestjs
YIAN23 天前
NestJS 入门核心梳理:模块化架构、装饰器与依赖注入
后端·nestjs
半个落月24 天前
NestJS 入门实战:从工厂模式到 Todo CRUD,讲透模块化、依赖注入与测试
后端·nestjs