第七部分:第五节 - 数据关系与进阶查询 (TypeORM):仓库里复杂的配料组合
现实世界的系统通常涉及多个实体之间复杂的关联,比如一个用户可以有多篇博客文章(一对多),一篇文章可以有多个标签(多对多),一个用户有一个详细的个人资料(一对一)。在关系型数据库中,我们通过外键来建立这些关系。TypeORM 提供了方便的装饰器,让我们可以在实体类中以面向对象的方式定义和管理这些数据库关系。这就像在原材料仓库中,不仅存放了各种原材料,还有详细的配料清单,说明了不同原材料之间如何组合才能做出特定的菜品。
数据库关系类型与 TypeORM 装饰器:
- 一对一 (One-to-One): 两个实体之间一一对应。例如,一个用户可能有一个且只有一个个人资料。
- 在拥有外键的一方使用
@OneToOne(() => TargetEntity)
和@JoinColumn()
。 - 在另一方使用
@OneToOne(() => TargetEntity)
。
- 在拥有外键的一方使用
- 一对多 (One-to-Many) / 多对一 (Many-to-One): 一个实体可以关联多个另一个实体,但另一个实体只能关联一个该实体。例如,一个用户可以有多篇博客文章,但一篇博客文章只属于一个用户。
- 在“多”的一方(外键所在方)使用
@ManyToOne(() => TargetEntity, target => target.propertyName)
。 - 在“一”的一方使用
@OneToMany(() => TargetEntity, target => target.propertyName)
。
- 在“多”的一方(外键所在方)使用
- 多对多 (Many-to-Many): 两个实体之间互相可以关联多个对方。例如,一篇文章可以有多个标签,一个标签可以应用于多篇文章。
- 在关系的两端都使用
@ManyToMany(() => TargetEntity, target => target.propertyName)
。 - 通常需要在其中一端使用
@JoinTable()
来指定连接表(TypeORM 会自动创建一个额外的表来管理多对多关系)。
- 在关系的两端都使用
小例子:定义用户和文章的一对多关系
假设我们有 User
实体和 Post
实体,一个用户有多篇文章。
创建 src/posts/entities/post.entity.ts
:
// src/posts/entities/post.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne } from 'typeorm';
import { User } from '../../users/entities/user.entity'; // 导入 User 实体@Entity('posts')
export class Post {@PrimaryGeneratedColumn()id: number;@Column()title: string;@Column('text') // text 类型字段content: string;@CreateDateColumn()created_at: Date;// ManyToOne 关系:多篇文章 (Post) 对应一个用户 (User)@ManyToOne(() => User, user => user.posts) // 第一个参数是目标实体,第二个参数是目标实体中对应关系的属性名author: User; // author 列存储关联的 User 实体对象 (在数据库中会有一个 user_id 外键列)
}
修改 src/users/entities/user.entity.ts
:
// src/users/entities/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Unique, OneToMany } from 'typeorm';
import { Post } from '../../posts/entities/post.entity'; // 导入 Post 实体@Entity('users')
export class User {@PrimaryGeneratedColumn()id: number;@Column({ length: 50, unique: true })username: string;@Column({ unique: true, nullable: true })email: string;@Column({ nullable: true })age: number;@CreateDateColumn()created_at: Date;// OneToMany 关系:一个用户 (User) 对应多篇文章 (Post)@OneToMany(() => Post, post => post.author) // 第一个参数是目标实体,第二个参数是目标实体中对应关系的属性名posts: Post[]; // posts 属性将包含该用户的所有 Post 实体数组
}
还需要创建 PostsModule
并在 AppModule
中导入,以及在 PostsModule
中注册 Post
实体。
加载相关数据(延迟加载 vs 急速加载):
当你获取一个包含关系的实体时,TypeORM 默认不会立即加载关联的实体。
- 延迟加载 (Lazy Loading): 默认行为。关联的实体在第一次访问其属性时才会被加载(返回 Promise)。这可以提高主实体的加载速度。
const user = await this.usersRepository.findOne({ where: { id: 1 } }); // user.posts 此时是一个 Promise const posts = await user.posts; // 访问 posts 属性时才执行加载文章的数据库查询
- 急速加载 (Eager Loading): 通过在关系装饰器中设置
{ eager: true }
,关联的实体会在加载主实体时立即加载(通过 JOIN 查询)。这会增加主实体的加载时间,但后续访问关联属性无需额外查询。// 在 User 实体中的 @OneToMany 装饰器上添加 { eager: true } // @OneToMany(() => Post, post => post.author, { eager: true }) // posts: Post[];const user = await this.usersRepository.findOne({ where: { id: 1 } }); // user.posts 此时已经加载好了,是一个 Post[] 数组,不是 Promise console.log(user.posts);
使用 relations
选项加载关系:
即使没有设置 eager: true
,你也可以在查询时通过 relations
选项指定要加载的关联关系。这通常比 eager
更灵活,因为你可以按需加载。
// 在 Service 中获取用户及其文章
const userWithPosts = await this.usersRepository.findOne({where: { id: 1 },relations: ['posts'] // 指定加载 posts 关系
});
console.log(userWithPosts.posts); // posts 已经被加载
使用 Query Builder 或原生 SQL 进行进阶查询:
对于复杂的查询、多表 JOIN、聚合查询等,Repository 的简单方法可能不够用。TypeORM 提供了强大的 Query Builder,可以用面向对象的方式构建复杂的查询语句。你也可以直接执行原生 SQL。
Query Builder 示例:
// 在 Service 中
async findUsersWithPostCount() {return this.usersRepository.createQueryBuilder('user') // 创建一个查询构建器,指定主实体和别名 'user'.leftJoinAndSelect('user.posts', 'post') // LEFT JOIN user 的 posts 关系,并选择 posts (别名 'post').select(['user.id', 'user.username']) // 选择用户 ID 和用户名.addSelect('COUNT(post.id)', 'postCount') // 计算关联的 post 数量,命名为 postCount.groupBy('user.id') // 按用户 ID 分组.getRawMany(); // 获取原始查询结果 (因为使用了聚合函数)
}// 这是一个稍微复杂的例子,展示了 Query Builder 的能力
// 结果可能是一个 { id: number, username: string, postCount: string } 对象的数组
原生 SQL 示例:
// 在 Service 中
async findUsersByNativeSql(minAge: number) {const query = `SELECT * FROM users WHERE age > ?`;const [rows] = await this.usersRepository.query(query, [minAge]); // 执行原生 SQLreturn rows; // 返回原始结果数组
}
原生 SQL 非常灵活,但缺乏 TypeORM 的类型安全和实体映射能力,应谨慎使用。
小结: 在 TypeORM 中,可以使用装饰器 (@OneToOne
, @OneToMany
, @ManyToOne
, @ManyToMany
) 定义实体之间的关系。通过配置 { eager: true }
或使用 relations
选项来加载关联数据。对于复杂的查询,可以使用 Query Builder 或执行原生 SQL。
练习:
- 在你的 NestJS 项目中,为你的资源(博客文章或任务)定义实体之间的关系。例如,如果你选择博客文章,可以定义
Author
和Post
实体,并建立一对多关系(一个作者有多篇文章)。 - 修改相应的实体文件,添加关系装饰器。
- 修改 Service 中的方法,例如获取作者详情时,使用
relations: ['posts']
选项同时加载该作者的所有文章。 - (进阶)尝试使用 Query Builder 编写一个查询,例如:
- 获取所有作者及其文章数量。
- 获取发布年份在某个范围内的所有书籍(如果你选择了书籍项目)。
- 运行应用,测试你带有关系加载的 API 端点,确认能够正确获取关联数据。