第十一章 关系查询与级联
核心概念
前面的章节解决了"关系怎么建",本章解决两个日常问题:
- 怎么把关联数据查出来:像点外卖——默认只送来主食(主表),
relations/ join 是"加购配菜";eager是"每单固定带配菜" - 保存/删除时关联数据怎么办(cascade 级联):像"拎包入住"——保存用户时行李(文章)要不要自动跟着进门、更新、甚至被一起扔掉,都可以逐项配置。
级联选项(用于一对多关系)
cascade?: boolean | ("insert" | "update" | "remove" | "soft-remove" | "recover")[];cascade: true等价于全选:保存/删除父实体时,关系属性里挂着的子实体自动跟着 insert / update / remove / soft-remove / recover。- 数组写法逐项开启,例如
cascade: ["insert"]:只在插入时级联,之后改子实体再 save 父实体,子实体的修改不会落库。
注意
- 以前得是先存没有外键的,再存有外键的.用了级联以后,就可以直接保存了(必须用save)
使用
添加级联选项
在没有外键的那张表上,添加
cascade: trueAuthor 有 Book 关联,添加
cascade: true后,保存 Author 时,会自动保存关联的 Book 数据,Author没有外键,Books有外键Author.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
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方法保存
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加载关联数据
@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 装饰器:
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:
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 记录所有祖先-后代对,查询整棵树非常高效。