Skip to content

第十一章 关系查询与级联 ​

核心概念 ​

前面的章节解决了"关系怎么建",本章解决两个日常问题:

  • 怎么把关联数据查出来:像点外卖——默认只送来主食(主表),relations / join 是"加购配菜";eager 是"每单固定带配菜"
  • 保存/删除时关联数据怎么办(cascade 级联):像"拎包入住"——保存用户时行李(文章)要不要自动跟着进门、更新、甚至被一起扔掉,都可以逐项配置。

级联选项(用于一对多关系) ​

ts
cascade?: boolean | ("insert" | "update" | "remove" | "soft-remove" | "recover")[];
  • cascade: true 等价于全选:保存/删除父实体时,关系属性里挂着的子实体自动跟着 insert / update / remove / soft-remove / recover。
  • 数组写法逐项开启,例如 cascade: ["insert"]:只在插入时级联,之后改子实体再 save 父实体,子实体的修改不会落库。

注意

  • 以前得是先存没有外键的,再存有外键的.用了级联以后,就可以直接保存了(必须用save)

使用 ​

添加级联选项 ​

  • 在没有外键的那张表上,添加cascade: true

  • Author 有 Book 关联,添加cascade: true后,保存 Author 时,会自动保存关联的 Book 数据,Author没有外键,Books有外键

  • Author.ts

ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
import { Book } from "./Book";

@Entity()
export class Author {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @OneToMany(() => Book, (book) => book.author, { cascade: true })
  books: Book[];
}
  • Book.ts
ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm";
import { Author } from "./Author";

@Entity()
export class Book {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @ManyToOne(() => Author, (author) => author.books)
  author: Author;
}

使用 ​

  • 这样存储的时候就没必要先存Author,再存Book了,直接保存Author,就会自动保存关联的Book

  • 必须用save方法保存,不能用insert方法保存

ts
import { AppDataSource } from "./data-source";
import { Author } from "./entities/Author";
import { Book } from "./entities/Book";
AppDataSource.initialize()
  .then(async () => {
    const manage = AppDataSource.manager;

    const book1 = new Book();
    book1.title = "Book 3";

    const book2 = new Book();
    book2.title = "Book 4";

    const user = new Author();
    user.name = "李四";
    user.books = [book1, book2];
    await manage.save(Author, user); // 不用单独 save 文章
  })
  .catch((error) => console.log(error));

⚠️ cascade: ["remove"] 的危险:userRepo.remove(user) 会把 user.posts 里挂着的文章一起从数据库删掉。本章演示 C 组里删 1 个用户,2 篇文章跟着消失。另一个易错点:remove 时若没把子实体查出来(没带 relations),级联删除不会触发——先查全再删。

区分两个"级联":cascade 是 TypeORM 层的(save/remove 时 ORM 帮你发 SQL);onDelete: "CASCADE" 是数据库层的外键约束(第 12 章)。两者不要混淆。

eager 自动加载关联数据(少用) ​

  • 一句话就是不用写relations,直接用eager加载关联数据
ts
@OneToMany(() => Post, (post) => post.user, { eager: true })
posts: Relation<Post[]>;

用 find* 方法查父实体时关系自动带出,不用写 relations。两个坑:

  • eager 会产生额外的 JOIN 查询,数据量大时是性能炸弹(每个用户都带全部文章);
  • QueryBuilder 的 getMany/getOne 不会自动加载 eager 关系(RelationOptions.d.ts 原文:"Only using QueryBuilder prevents loading eager relations"),需要自己 leftJoinAndSelect;
  • eager 只能设在关系的一边。

自引用树:@Tree + TreeRepository(用的较少) ​

分类、部门、评论楼中楼都是"自己引用自己"的树。TypeORM 提供 @Tree 装饰器:

ts
import { Tree, TreeChildren, TreeParent } from "typeorm";

@Entity()
@Tree("closure-table") // 四种模式之一
class Category {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @TreeChildren()
  children: Relation<Category[]>;

  @TreeParent()
  parent: Relation<Category>;
}

@Tree 的模式:"adjacency-list" | "closure-table" | "nested-set" | "materialized-path"。

⚠️ 1.1.1 实测重点:TreeRepository 的 findTrees() / findDescendantsTree() 源码里只实现了 closure-table / nested-set / materialized-path,用 adjacency-list 调用会抛 Supported only in tree entities。所以演示使用 closure-table:

ts
const catRepo = ds.getTreeRepository(Category);
const phone = await catRepo.save({ name: "手机", parent: electronics });
const trees = await catRepo.findTrees(); // 整棵树,children 已嵌套好

closure-table 模式除 category 表(含自引用外键 parentId)外,还会自动生成一张闭包表 category_closure 记录所有祖先-后代对,查询整棵树非常高效。