跳到主要内容

ORM 核心原理

问题

ORM 的核心原理是什么?有哪些常见的性能陷阱?

答案

一、ORM 映射策略

表与类的映射

数据库                        应用代码
┌──────────────┐ class User {
│ users │ ←→ id: number
├──────────────┤ name: string
│ id │ email: string
│ name │ posts: Post[]
│ email │ }
│ created_at │
└──────────────┘

关系映射

数据库关系ORM 映射说明
一对一@OneToOne / hasOne用户 ↔ 用户档案
一对多@OneToMany / hasMany用户 → 文章列表
多对一@ManyToOne / belongsTo文章 → 作者
多对多@ManyToMany / 中间表文章 ↔ 标签

二、N+1 查询问题

N+1 问题是 ORM 中最常见的性能陷阱。

问题描述

// 查询 10 个用户和他们的文章
const users = await User.findAll(); // 1 条 SQL:SELECT * FROM users

for (const user of users) {
const posts = await user.getPosts(); // 每个用户 1 条 SQL
// SELECT * FROM posts WHERE user_id = ?
}
// 总计:1 + 10 = 11 条 SQL(N+1)

10 个用户产生了 11 条 SQL。如果有 1000 个用户,就是 1001 条 SQL。

解决方案:Eager Loading(急加载)

// Prisma:使用 include
const users = await prisma.user.findMany({
include: { posts: true }
});
// 只发 2 条 SQL:
// SELECT * FROM users
// SELECT * FROM posts WHERE user_id IN (1, 2, 3, ..., 10)

// TypeORM:使用 relations
const users = await userRepo.find({
relations: ['posts']
});

// Django:使用 prefetch_related
users = User.objects.prefetch_related('posts').all()

// MyBatis-Plus:使用 @Select + resultMap

Lazy Loading vs Eager Loading

对比懒加载(Lazy)急加载(Eager)
加载时机首次访问关联属性时查询时一次性加载
SQL 数量可能 N+1通常 1~2 条
内存占用按需加载,初始低初始较高
适合场景不一定需要关联数据确定需要关联数据
经验法则
  • 默认使用 Eager Loading 来避免 N+1
  • 只有在确定不需要关联数据时才用 Lazy Loading
  • 使用 ORM 的查询日志来检测 N+1 问题

三、查询构建方式

ORM 通常提供多种查询方式:

1. 方法链(Query Builder)

// TypeORM Query Builder
const users = await userRepo
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.where('user.age > :age', { age: 18 })
.orderBy('user.name', 'ASC')
.limit(10)
.getMany();

2. 声明式 API

// Prisma 声明式
const users = await prisma.user.findMany({
where: { age: { gt: 18 } },
include: { posts: true },
orderBy: { name: 'asc' },
take: 10,
});

3. 原始 SQL

// 复杂查询逃逸到原始 SQL
const result = await prisma.$queryRaw`
SELECT u.*, COUNT(p.id) AS post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id
HAVING post_count > 5
`;

四、事务处理

// Prisma 事务
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({ data: { name: 'Alice' } });
await tx.post.create({
data: { title: 'First Post', authorId: user.id }
});
});

// TypeORM 事务
await dataSource.transaction(async (manager) => {
const user = manager.create(User, { name: 'Alice' });
await manager.save(user);
const post = manager.create(Post, { title: 'First Post', author: user });
await manager.save(post);
});

五、常见性能陷阱

1. SELECT *

// ❌ 查所有字段
const users = await prisma.user.findMany();

// ✅ 只查需要的字段
const users = await prisma.user.findMany({
select: { id: true, name: true, email: true }
});

2. 循环内查询

// ❌ 在循环内查询
for (const id of userIds) {
const user = await prisma.user.findUnique({ where: { id } });
}

// ✅ 批量查询
const users = await prisma.user.findMany({
where: { id: { in: userIds } }
});

3. 过度嵌套 Include

// ❌ 多层嵌套加载(可能加载海量数据)
const users = await prisma.user.findMany({
include: {
posts: {
include: {
comments: {
include: { author: true }
}
}
}
}
});

// ✅ 按需加载,或使用分页

六、ORM 的两种风格

风格代表特点
Active RecordRails、Django、Sequelize模型类直接包含 CRUD 方法(user.save()
Data MapperTypeORM、SQLAlchemy、MyBatis模型和数据库操作分离(repo.save(user)

Active Record 更简单,适合小项目;Data Mapper 更灵活,适合复杂业务。


常见面试问题

Q1: 什么是 N+1 问题?如何解决?

答案

N+1 是指查询 N 条主数据时,对每条主数据再发一次查询获取关联数据,导致总共 N+1 条 SQL。

解决方案:

  1. Eager Loading:一次性 JOIN 或 WHERE IN 批量加载
  2. Batch Loading(DataLoader):将多个相同类型的查询合并为一条
  3. 查询优化:使用 JOIN 在一条 SQL 中获取所有数据

Q2: ORM 的缺点是什么?什么时候不应该用 ORM?

答案

缺点

  1. 性能开销:对象映射和类型转换有额外成本
  2. 复杂查询困难:多表 JOIN、子查询、窗口函数等用 ORM 表达很别扭
  3. SQL 不可控:生成的 SQL 可能不是最优的
  4. 隐藏细节:开发者可能不了解底层 SQL,难以优化

不适合的场景

  • 报表查询(复杂聚合、多表关联)
  • 批量数据操作(大量 INSERT/UPDATE)
  • 对 SQL 性能有极致要求的核心路径

Q3: 如何检测和避免 N+1 问题?

答案

  1. 开启 SQL 日志:观察查询数量是否异常
  2. 使用工具:Django Debug Toolbar、nplusone(Python)、bullet(Ruby)
  3. Code Review:检查循环中是否有数据库查询
  4. 集成测试:断言 SQL 查询数量
// Prisma 开启日志
const prisma = new PrismaClient({
log: ['query'], // 控制台输出所有 SQL
});

Q4: Active Record 和 Data Mapper 模式的区别?

答案

对比Active RecordData Mapper
模型职责包含业务逻辑 + 数据库操作只有业务逻辑
数据库操作user.save()repository.save(user)
耦合度高(模型绑定数据库)低(模型与数据库解耦)
测试需 Mock 数据库可 Mock Repository
适合项目小型、快速开发大型、复杂业务

相关链接