服务端6月5日 22:02
TypeORM 查询该用 find 还是 QueryBuilder?三种方式适用场景对比TypeORM 查询数据有三条路:`find` 系列方法、`QueryBuilder`、原生 SQL。很多人上来就用 QueryBuilder,其实 80% 的查询 `find` 就够了——更简洁、类型安全、不容易出错。这篇把三种方式的适用边界和常见坑讲清楚。
## find:日常查询的首选
### 基础查询
```typescript
// 查所有
const users = await userRepository.find();
// 按 ID 查一条
const user = await userRepository.findOne({ where: { id: 1 } });
// 按条件查
const activeUsers = await userRepository.find({
where: { active: true },
});
```
### 条件组合
```typescript
import { And, Or, LessThan, MoreThan, Like, Between, In, IsNull } from 'typeorm';
// AND 条件——多个字段自动 AND
const users = await userRepository.find({
where: {
active: true,
age: MoreThan(18),
},
});
// OR 条件——数组内自动 OR
const users = await userRepository.find({
where: [
{ role: 'admin' },
{ age: MoreThan(30) },
],
});
// 常用 FindOperator
const users = await userRepository.find({
where: {
name: Like('%john%'), // 模糊搜索
id: In([1, 2, 3]), // IN 查询
createdAt: Between(start, end), // 范围查询
deletedAt: IsNull(), // IS NULL
},
});
```
**find 的 OR 有个坑**:数组里每个元素是一个独立的 OR 分支,不是字段级别的 OR。如果你需要 `role = 'admin' AND (age > 30 OR name LIKE '%john%')` 这种混合逻辑,find 写不出来——得上 QueryBuilder。
### 选择字段
```typescript
const users = await userRepository.find({
select: {
id: true,
name: true,
email: true,
// 不选 password 等敏感字段
},
where: { active: true },
});
```
### 排序和分页
```typescript
const users = await userRepository.find({
where: { active: true },
order: { createdAt: 'DESC' },
skip: 0,
take: 20,
});
// 带总数的分页——一次查询拿到数据 + 总数
const [users, total] = await userRepository.findAndCount({
where: { active: true },
order: { createdAt: 'DESC' },
skip: (page - 1) * pageSize,
take: pageSize,
});
```
### 加载关联
```typescript
// 简单关联
const users = await userRepository.find({
relations: { posts: true, profile: true },
});
// 嵌套关联
const users = await userRepository.find({
relations: { posts: { comments: true } },
});
```
### find 的 N+1 陷阱
`find` 加载关联时会发多条 SQL——每个关联单独查一次。如果查 100 个用户且关联了 posts,可能产生几十条查询:
```typescript
// 可能触发 N+1
const users = await userRepository.find({
relations: { posts: true },
});
// SQL 1: SELECT * FROM user
// SQL 2: SELECT * FROM post WHERE userId IN (1, 2, 3, ... 100)
```
TypeORM 0.3+ 已经优化了这个问题——会用 `IN` 批量查而不是逐个查。但如果关联层级很深(用户 → 文章 → 评论 → 作者),查询数仍然会膨胀。深层关联建议用 QueryBuilder 的 `leftJoinAndSelect` 一次性 JOIN。
## QueryBuilder:find 搞不定的场景
### 关联查询带条件过滤
`find` 的 `relations` 只能全量加载,不能过滤。QueryBuilder 可以:
```typescript
// 只查有已发布文章的用户
const users = await userRepository
.createQueryBuilder('user')
.innerJoinAndSelect(
'user.posts',
'post',
'post.isPublished = :published',
{ published: true },
)
.getMany();
```
第三个参数是 JOIN 条件——`find` 做不到这个。
### 条件组合:AND + OR + 括号
```typescript
const users = await userRepository
.createQueryBuilder('user')
.where('user.active = :active', { active: true })
.andWhere(
new Brackets((qb) => {
qb.where('user.role = :admin', { admin: 'admin' }).orWhere(
'user.createdAt > :date',
{ date: '2024-01-01' },
);
}),
)
.getMany();
// WHERE active = true AND (role = 'admin' OR createdAt > '2024-01-01')
```
`Brackets` 生成括号——保证 OR 的优先级正确。没有它,`AND` 和 `OR` 的优先级会让你拿到错误的结果。
### 聚合查询
```typescript
const stats = await userRepository
.createQueryBuilder('user')
.select('user.role', 'role')
.addSelect('COUNT(*)', 'count')
.addSelect('AVG(user.age)', 'avgAge')
.groupBy('user.role')
.getRawMany();
// 返回: [{ role: 'admin', count: '5', avgAge: '32.5' }, ...]
```
聚合查询必须用 `getRawMany()`——返回原始数据库行,字段类型都是字符串。`getMany()` 返回实体对象,但聚合结果不是实体,强用会出错。
### 子查询
```typescript
const users = await userRepository
.createQueryBuilder('user')
.where((qb) => {
const subQuery = qb
.subQuery()
.select('post.authorId')
.from(Post, 'post')
.groupBy('post.authorId')
.having('COUNT(post.id) > :count')
.getQuery();
return `user.id IN ${subQuery}`;
})
.setParameter('count', 5)
.getMany();
```
### QueryBuilder 的类型安全坑
`find` 是完全类型安全的——`where` 里的字段名写错了 TypeScript 直接报错。QueryBuilder 的 SQL 片段是字符串,写错了只有运行时才知道:
```typescript
// 字段名拼错了,TypeScript 不会报错,运行时才炸
.where('user.actve = :active', { active: true })
// ↑ typo: active vs actve
```
**建议**:QueryBuilder 的 SQL 片段尽量短,把条件值用参数传递(`:param`),减少字符串拼写出错的可能。
## 原生 SQL:最后的手段
QueryBuilder 也搞不定时,直接写 SQL:
```typescript
// 查询
const result = await dataSource.query(
'SELECT * FROM user WHERE created_at > $1',
[startDate],
);
// 事务中执行
const result = await queryRunner.query(
'UPDATE user SET last_login = NOW() WHERE id = $1',
[userId],
);
```
**什么时候用原生 SQL**:
- 数据库特有的函数或语法(如 PostgreSQL 的 `JSONB` 操作、`UPSERT`)
- 复杂的窗口函数(`ROW_NUMBER() OVER (...)`)
- 需要极致优化的性能关键查询
- 迁移老项目的 SQL 不想重写
**注意**:`$1` 是 PostgreSQL 占位符,MySQL 用 `?`。原生 SQL 没有方言抽象——换数据库要手动改。
## 性能对比
同样的查询,三种方式的性能差异:
```typescript
// 1. find —— 最慢(多条 SQL + 实体转换开销)
const users = await userRepository.find({ where: { active: true }, relations: { posts: true } });
// 2. QueryBuilder —— 中等(一条 JOIN SQL,但仍有实体转换)
const users = await userRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.where('user.active = :active', { active: true })
.getMany();
// 3. 原生 SQL —— 最快(一条 SQL,跳过实体转换)
const rows = await dataSource.query(
'SELECT u.*, p.id as post_id, p.title FROM user u LEFT JOIN post p ON p.userId = u.id WHERE u.active = $1',
[true],
);
```
差距不大时(毫秒级),优先用 `find` 或 QueryBuilder——可维护性和类型安全比那几毫秒更有价值。只有查询确实是瓶颈时才用原生 SQL 优化。
## 选择决策
| 场景 | 用什么 | 原因 |
|------|--------|------|
| 单表简单查询 | `find` | 简洁、类型安全 |
| 单表条件组合 | `find` + FindOperator | `In`、`Like`、`Between` 够用 |
| 多表关联 + 过滤 | QueryBuilder | `find` 的 relations 不能加条件 |
| 聚合/分组 | QueryBuilder | `find` 不支持 GROUP BY |
| 子查询 | QueryBuilder | `find` 不支持 |
| 复杂 OR + AND 混合 | QueryBuilder | `find` 的 OR 只支持数组级 |
| 数据库特有语法 | 原生 SQL | TypeORM 不覆盖所有特性 |
| 性能关键查询 | 原生 SQL | 跳过实体转换 |
**经验法则**:先试 `find`,搞不定再上 QueryBuilder,最后才用原生 SQL。层级越低灵活性越高,但可维护性和类型安全越差。
标签
TypeORM
TypeORM 是一个面向对象的关系型数据库ORM框架,用于在 Node.js 应用程序中操作数据库。它支持多种数据库,包括 MySQL,PostgreSQL,SQLite,以及 Microsoft SQL Server 等。TypeORM 提供了使用 TypeScript 的完整ORM解决方案,它的主要目标是简化数据库操作,提高开发效率。

服务端6月5日 21:43
TypeORM 查询缓存实战:Redis 配置、主动失效和策略选择数据库查询是后端应用最常见的性能瓶颈。TypeORM 内置了查询缓存,支持内存缓存和 Redis 缓存两种存储后端,能在不改动业务代码的情况下大幅降低数据库负载。这篇讲清楚怎么配、怎么用、以及缓存策略的选择。
## 两种缓存存储:数据库表 vs Redis
### 默认方案:数据库表缓存
不配置任何东西,TypeORM 默认用数据库的一张表存缓存:
```typescript
const dataSource = new DataSource({
type: 'mysql',
host: 'localhost',
username: 'root',
password: 'password',
database: 'myapp',
cache: true, // 开启缓存,默认 1000ms 过期
})
```
TypeORM 会自动创建 `query-result-cache` 表,把查询 SQL 和结果序列化后存进去。下次同样的查询直接从这张表取,不执行 SQL。
**问题**:缓存本身也存数据库里——等于用数据库查数据库,只是从业务表换到了缓存表。单实例够用,分布式部署时每个实例有自己的缓存表,互相看不到。
### 推荐方案:Redis 缓存
```typescript
const dataSource = new DataSource({
type: 'mysql',
cache: {
type: 'redis',
options: {
host: 'localhost',
port: 6379,
password: 'redis-password',
db: 0,
},
duration: 30000, // 默认缓存 30 秒
},
})
```
Redis 的优势:
- **快**:内存读取,微秒级延迟
- **共享**:多个应用实例访问同一个 Redis,缓存一致
- **可控**:Redis 的内存管理、过期策略、持久化都很成熟
Redis 集群场景用 ioredis:
```typescript
cache: {
type: 'ioredis/cluster',
options: {
startupNodes: [
{ host: '10.0.0.1', port: 7000 },
{ host: '10.0.0.2', port: 7000 },
{ host: '10.0.0.3', port: 7000 },
],
},
}
```
## 查询级缓存:精确控制哪些查询缓存
全局开缓存后,不是所有查询都会缓存——需要显式指定。
### Repository 方式
```typescript
// 缓存 30 秒
const users = await userRepository.find({
cache: 30000,
})
// 给缓存一个 ID,方便后续清除
const users = await userRepository.find({
cache: {
id: 'users_list',
milliseconds: 30000,
},
})
// findAndCount 也支持
const [users, count] = await userRepository.findAndCount({
cache: {
id: 'users_paginated',
milliseconds: 30000,
},
})
```
### QueryBuilder 方式
```typescript
const posts = await dataSource
.createQueryBuilder(Post, 'post')
.where('post.isPublished = :published', { published: true })
.cache('published_posts', 60000) // 缓存 ID + 过期时间
.getMany()
```
### 缓存 ID 的作用
缓存 ID 是手动控制缓存的关键——通过 ID 可以精确清除某类查询的缓存:
```typescript
// 清除指定 ID 的缓存
await dataSource.queryResultCache.remove(['users_list'])
// 数据变更后清除相关缓存
async createUser(dto: CreateUserDto) {
const user = await this.userRepo.save(dto)
// 用户列表缓存失效
await this.dataSource.queryResultCache.remove(['users_list', 'users_paginated'])
return user
}
```
**原则**:所有需要缓存的查询都应该指定 ID,否则你无法在数据变更时精确失效缓存——只能等过期。
## 缓存策略选择
### 按数据变化频率决定缓存时长
| 数据特征 | 缓存时长 | 例子 |
|---------|---------|------|
| 几乎不变 | 5-30 分钟 | 省份列表、配置项 |
| 偶尔变化 | 30-60 秒 | 文章列表、商品分类 |
| 频繁变化 | 5-15 秒 | 实时排行榜、库存 |
| 实时性要求高 | 不缓存 | 支付状态、账户余额 |
### 主动失效 vs 被动过期
- **被动过期**:设一个 `duration`,到期自动清除。简单,但数据变更后到过期前这段时间,用户看到的可能是旧数据
- **主动失效**:数据变更时手动 `remove()` 缓存 ID。更精确,但代码更复杂
生产环境推荐**两者结合**:设一个较长的 `duration` 做兜底,数据变更时主动失效。这样即使忘了失效,缓存最多存在 `duration` 时间也不会永远不过期。
```typescript
// 查询时设较长缓存
const users = await this.userRepo.find({
cache: { id: 'users_list', milliseconds: 300000 }, // 5 分钟兜底
})
// 变更时主动失效
async updateUser(id: number, dto: UpdateUserDto) {
await this.userRepo.update(id, dto)
await this.dataSource.queryResultCache.remove(['users_list'])
}
```
## 缓存与事务
TypeORM 的查询缓存在事务内**不会自动失效**。这可能导致一个问题:
```typescript
// 事务外查询 → 走缓存
const user = await userRepo.findOne({ where: { id: 1 }, cache: 30000 })
// 事务内更新
await dataSource.transaction(async (manager) => {
await manager.update(User, 1, { name: 'new name' })
// 此时缓存里还是旧数据!
})
// 事务提交后再查 → 可能还是缓存旧数据
```
解决方案:事务提交后手动清除缓存:
```typescript
await dataSource.transaction(async (manager) => {
await manager.update(User, 1, { name: 'new name' })
})
await dataSource.queryResultCache.remove(['user_1'])
```
## 缓存清理
### 清除指定缓存
```typescript
// 按 ID 清除
await dataSource.queryResultCache.remove(['users_list', 'posts_list'])
// 清除所有缓存
await dataSource.queryResultCache.clear()
```
### 命令行清除
```bash
npx typeorm cache:clear
```
### 自动清理
Redis 的 TTL 机制会自动清理过期缓存,不需要手动管理。数据库表缓存 TypeORM 也会定期清理过期记录。
## ignoreErrors:缓存降级
缓存不可用时不应该让业务请求也失败:
```typescript
cache: {
type: 'redis',
options: { host: 'localhost', port: 6379 },
ignoreErrors: true, // Redis 挂了不报错,直接查数据库
}
```
`ignoreErrors: true` 是生产环境必须加的——Redis 重启或网络抖动时,查询会降级到直接访问数据库,而不是直接 500。
## 完整配置示例
```typescript
// data-source.ts
import { DataSource } from 'typeorm'
export const dataSource = new DataSource({
type: 'mysql',
host: process.env.DB_HOST,
port: 3306,
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: 'myapp',
cache: {
type: 'redis',
options: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
db: 0,
},
duration: 30000, // 默认 30 秒
ignoreErrors: true, // 缓存故障降级
tableName: 'query_result_cache', // 数据库表缓存的表名(如果用数据库缓存)
},
})
```
## 缓存策略速查
| 场景 | 策略 |
|------|------|
| 配置数据、字典表 | 长缓存(5 分钟+)+ 变更时手动失效 |
| 列表查询 | 中缓存(30-60 秒)+ 分页参数加入缓存 ID |
| 详情页 | 短缓存(15-30 秒)+ 更新时失效 |
| 实时数据 | 不缓存,或 5 秒极短缓存 |
| 多实例部署 | 必须用 Redis,数据库表缓存不共享 |
| 缓存故障容忍 | `ignoreErrors: true` |
| 事务内更新 | 事务提交后手动清除缓存 |
服务端6月5日 13:57
TypeORM事务基础:三种写法、常见陷阱和NestJS集成数据库事务保证一组操作要么全部成功、要么全部回滚——转账是最经典的例子:A 扣 100 元和 B 加 100 元必须在同一个事务里,不能只执行一半。TypeORM 提供三种写事务的方式,这篇文章把每种的使用场景和容易踩的坑讲清楚。
## 为什么需要事务
不用事务会怎样:
```typescript
// ❌ 没有事务保护
async function transfer(fromId: number, toId: number, amount: number) {
const from = await userRepo.findOne({ where: { id: fromId } });
from.balance -= amount;
await userRepo.save(from); // 成功
const to = await userRepo.findOne({ where: { id: toId } });
to.balance += amount;
await userRepo.save(to); // 如果这里报错,A 扣了钱但 B 没收到
}
```
事务保证:要么两步都成功,要么两步都回滚。
## 方式一:DataSource.transaction()(日常首选)
最简洁的写法——传一个回调,回调里所有操作自动在一个事务里:
```typescript
await dataSource.transaction(async (manager) => {
const from = await manager.findOne(User, { where: { id: fromId } });
const to = await manager.findOne(User, { where: { id: toId } });
if (from.balance < amount) {
throw new Error('余额不足'); // 抛异常 → 自动 rollback
}
from.balance -= amount;
to.balance += amount;
await manager.save([from, to]); // 正常返回 → 自动 commit
});
```
**最关键的规则**:回调里必须用 `manager` 参数操作数据库,不能用全局的 `userRepository`。
```typescript
// ❌ 错误:全局 repository 不在事务里
await dataSource.transaction(async (manager) => {
await manager.save(from); // 在事务里 ✅
await userRepository.save(to); // 不在事务里 ❌
});
```
全局 Repository 拿到的连接不在事务的连接上,它的操作不受事务保护。这是最常见的事务 bug——代码不报错,但数据不一致。
### 回调正常返回 → commit
```typescript
await dataSource.transaction(async (manager) => {
await manager.save(user);
// 没有 throw → 自动 commit
});
```
### 回调抛异常 → rollback
```typescript
await dataSource.transaction(async (manager) => {
await manager.save(user);
throw new Error('出错了'); // → 自动 rollback,上面的 save 被撤销
});
```
不需要手动 commit 或 rollback——TypeORM 自动处理。
## 方式二:QueryRunner(精细控制)
当你需要手动控制 commit/rollback 时机、设隔离级别、或同一个连接上跑多个事务时:
```typescript
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect(); // 获取数据库连接
await queryRunner.startTransaction(); // 开启事务
try {
await queryRunner.manager.save(user1);
await queryRunner.manager.save(user2);
// 可以在这里做其他判断,决定 commit 还是 rollback
if (someCondition) {
await queryRunner.commitTransaction();
} else {
await queryRunner.rollbackTransaction();
}
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release(); // 必须释放连接!
}
```
`release()` 是必须的——QueryRunner 持有数据库连接,不释放会泄漏连接池。`finally` 块保证即使 commit 失败也能释放。
### 什么时候用 QueryRunner
- 需要手动控制 commit 时机(如根据业务逻辑决定)
- 需要设置隔离级别:`queryRunner.startTransaction('SERIALIZABLE')`
- 需要在同一个连接上执行原生 SQL 和 ORM 操作
- 批量操作需要分批 commit(避免单个大事务锁表太久)
### 分批 commit 的模式
```typescript
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
const users = await queryRunner.manager.find(User, { take: 10000 });
for (let i = 0; i < users.length; i += 1000) {
await queryRunner.startTransaction();
try {
const batch = users.slice(i, i + 1000);
for (const user of batch) {
user.processed = true;
await queryRunner.manager.save(user);
}
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
}
}
await queryRunner.release();
```
每 1000 条一个事务,而不是 10000 条一个大事务。大事务持有锁太久会阻塞其他查询。
## 方式三:NestJS + TypeORM
NestJS 项目里注入 DataSource 用事务:
```typescript
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
@Injectable()
export class OrderService {
constructor(
@InjectDataSource()
private dataSource: DataSource,
) {}
async createOrder(dto: CreateOrderDto) {
return this.dataSource.transaction(async (manager) => {
const order = manager.create(Order, dto);
await manager.save(order);
for (const item of dto.items) {
const product = await manager.findOne(Product, {
where: { id: item.productId },
});
product.stock -= item.quantity;
await manager.save(product);
}
return order;
});
}
}
```
## 常见陷阱
### 事务里调用了事务外的方法
```typescript
// ❌ 事务里调用了不在事务里的方法
async function updateProfile(userId: number) {
await userRepo.update(userId, { name: 'new' }); // 不在事务里
}
await dataSource.transaction(async (manager) => {
await manager.save(order);
await updateProfile(userId); // 这步不在事务保护下!
});
```
解决:把需要事务保护的操作都放在回调里,或者把 `manager` 传进去:
```typescript
async function updateProfile(manager: EntityManager, userId: number) {
await manager.update(User, userId, { name: 'new' });
}
await dataSource.transaction(async (manager) => {
await manager.save(order);
await updateProfile(manager, userId); // ✅ 在事务里
});
```
### 事务里做了网络请求
```typescript
// ❌ 事务里调第三方 API
await dataSource.transaction(async (manager) => {
await manager.save(order);
await fetch('https://payment-gateway/charge', { ... }); // 网络请求可能超时
await manager.save(payment);
});
```
网络请求可能耗时几秒甚至超时——事务期间数据库锁一直持有,其他请求被阻塞。正确做法:先完成数据库事务,再调外部 API,失败时用补偿机制回滚。
### 忘记 await
```typescript
// ❌ 没有 await,事务还没 commit 就返回了
await dataSource.transaction(async (manager) => {
manager.save(user); // 没有 await!
});
```
`manager.save()` 返回 Promise,没有 `await` 的话,事务 commit 时 save 可能还没执行完。TypeScript 的 `async` 函数里忘记 `await` 不会报错——这是个无声的 bug。
## 事务 vs 批量操作
不是所有操作都需要事务。批量插入、批量更新如果不需要原子性,可以不用事务——更快:
```typescript
// 不需要事务:批量插入日志
await logRepo.insert(logs);
// 需要事务:转账(必须保证原子性)
await dataSource.transaction(async (manager) => {
// ...
});
```
判断标准:这些操作是否必须"全部成功或全部失败"?是 → 用事务,否 → 不用。服务端6月5日 13:56
TypeORM迁移完整指南:自动生成、数据迁移和生产部署策略`synchronize: true` 在开发时很方便——改实体自动同步表结构。但生产环境这么做会丢数据:删字段时直接 DROP COLUMN,重命名字段被当作"删旧的加新的"。迁移(Migration)是生产环境管理数据库结构变更的唯一正确方式。这篇文章把迁移的完整流程和常见坑都讲清楚。
## 迁移的工作原理
迁移就是一个类,有 `up()` 和 `down()` 两个方法——`up()` 执行变更,`down()` 回滚变更。TypeORM 按顺序执行迁移文件,并在数据库里记录哪些已经跑过。
```
数据表 _migration:
┌──────────────────────────────┬────────────┐
│ id │ timestamp │
├──────────────────────────────┼────────────┤
│ InitSchema1700000000000 │ 2024-01-01 │
│ AddUserEmail1700000000001 │ 2024-01-02 │
└──────────────────────────────┴────────────┘
```
每次 `migration:run`,TypeORM 对比已执行的迁移和待执行的迁移文件,只跑新的。
## 创建迁移
### 方式一:自动生成(推荐)
修改实体后,让 TypeORM 自动对比生成迁移:
```bash
npx typeorm migration:generate src/migration/AddUserEmail -d src/data-source.ts
```
TypeORM 会对比实体定义和当前数据库结构,生成差量的迁移文件。这是最安全的方式——不会漏字段、不会写错类型。
前提:数据库必须和当前代码的实体一致(上次迁移已执行)。如果数据库和实体不同步,生成会报错。
### 方式二:手动创建
```bash
npx typeorm migration:create src/migration/AddUserEmail
```
创建空模板,自己写 SQL:
```typescript
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUserEmail1700000000001 implements MigrationInterface {
name = 'AddUserEmail1700000000001';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'user',
new TableColumn({
name: 'email',
type: 'varchar',
length: '255',
isUnique: true,
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('user', 'email');
}
}
```
手动迁移用于自动生成搞不定的场景:数据迁移(把数据从一列搬到另一列)、复杂的 schema 重构。
## 执行迁移
```bash
# 执行所有未执行的迁移
npx typeorm migration:run -d src/data-source.ts
# 回滚最后一次迁移
npx typeorm migration:revert -d src/data-source.ts
# 查看迁移状态
npx typeorm migration:show -d src/data-source.ts
```
`migration:show` 输出:
```
[X] InitSchema1700000000000 # 已执行
[X] AddUserEmail1700000000001 # 已执行
[ ] AddPostTags1700000000002 # 未执行
```
### 在 NestJS 里执行
```typescript
// main.ts 或专门的迁移脚本
import { DataSource } from 'typeorm';
import { AppDataSource } from './data-source';
async function runMigrations() {
await AppDataSource.initialize();
await AppDataSource.runMigrations();
await AppDataSource.destroy();
}
runMigrations();
```
或在启动时自动跑:
```typescript
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 启动时自动执行迁移
const dataSource = app.get(DataSource);
await dataSource.runMigrations();
await app.listen(3000);
}
```
**注意**:生产环境自动跑迁移有风险——如果迁移有 bug,服务启动就失败。更安全的做法是在部署流程里单独跑迁移,确认成功后再部署新代码。
## 常见迁移场景
### 加列
```typescript
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn('user', new TableColumn({
name: 'email',
type: 'varchar',
isNullable: true, // 先允许 NULL,后续填充数据后再设 NOT NULL
}));
}
```
**安全做法**:新列先 `isNullable: true`,应用层填充数据后,再用另一个迁移改为 `NOT NULL`。直接 `NOT NULL` 会导致已有行插入失败。
### 删列
```typescript
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('user', 'deprecatedField');
}
```
删列不可逆——数据丢了就没了。确保没有代码引用这个列后再删。建议先在代码里移除对列的引用,部署一版,确认没有报错,再加迁移删列。
### 改列类型
```typescript
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.changeColumn('user', 'age', new TableColumn({
name: 'age',
type: 'int', // 从 smallint 改为 int
isNullable: false,
}));
}
```
类型变更可能导致数据丢失——`varchar(255)` 改 `varchar(50)` 会截断数据。改之前检查现有数据是否都在新范围内。
### 重命名列
```typescript
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.renameColumn('user', 'name', 'fullName');
}
```
`renameColumn` 比 "删旧列+加新列" 安全——它用 `ALTER TABLE RENAME COLUMN`,数据不丢失。
### 数据迁移
纯 schema 迁移只改表结构,不改数据。数据迁移需要在 `up()` 里写 SQL:
```typescript
public async up(queryRunner: QueryRunner): Promise<void> {
// 把 name 拆成 firstName 和 lastName
await queryRunner.addColumn('user', new TableColumn({ name: 'firstName', type: 'varchar', isNullable: true }));
await queryRunner.addColumn('user', new TableColumn({ name: 'lastName', type: 'varchar', isNullable: true }));
// 数据迁移
await queryRunner.query(`
UPDATE "user" SET "firstName" = split_part("name", ' ', 1),
"lastName" = split_part("name", ' ', 2)
`);
// 数据填充完后删除旧列
await queryRunner.dropColumn('user', 'name');
}
```
数据迁移要注意性能——百万级表的 UPDATE 可能跑几十分钟。大表迁移分批执行:
```typescript
// 分批更新,每批 1000 行
await queryRunner.query(`
UPDATE "user" SET "firstName" = split_part("name", ' ', 1)
WHERE "firstName" IS NULL
LIMIT 1000
`);
```
## 迁移最佳实践
1. **每个迁移只做一件事**——加列是一个迁移,改类型是另一个。出错时可以精确回滚
2. **先加列后删列**——加列不影响现有代码,删列必须先改代码再跑迁移
3. **生产迁移先在 staging 测试**——同样的迁移在测试环境跑一遍确认没有报错
4. **永远不要手动修改 `_migration` 表**——TypeORM 靠它判断哪些迁移已执行,手动改会导致迁移混乱
5. **迁移文件提交到 git**——团队成员 pull 后跑 `migration:run` 就能同步数据库结构
6. **`down()` 必须正确实现**——回滚时 `down()` 是唯一的恢复手段
7. **部署流程里迁移先行**——先跑迁移再部署新代码,避免新代码期望新列但列还不存在服务端6月5日 13:55
TypeORM N+1查询:relations、leftJoinAndSelect和DataLoader方案对比N+1 查询是 ORM 里最经典的性能坑:查 100 个用户,再查 100 次每个用户的文章——1 次主查询 + N 次关联查询 = N+1 次数据库往返。TypeORM 默认不加载关联数据,所以 N+1 不是 bug 而是默认行为——你得主动告诉 TypeORM 你要哪些关联数据。这篇文章讲清楚 N+1 怎么产生的、怎么解决、以及各种方案的取舍。
## N+1 是怎么产生的
```typescript
// 查 100 个用户:1 次 SQL
const users = await userRepository.find();
// 每个用户查文章:100 次 SQL
for (const user of users) {
user.posts = await postRepository.find({ where: { authorId: user.id } });
}
// 总共 101 次数据库查询
```
100 个用户 101 次查询,1000 个用户 1001 次——线性增长。数据库连接池很快耗尽,API 响应从 50ms 飙到 5s。
## 解决方案一:relations 选项
最简单的方案,在 `find` 时声明要加载的关联:
```typescript
const users = await userRepository.find({
relations: ['posts', 'profile'],
});
```
生成的 SQL:
```sql
SELECT * FROM user;
SELECT * FROM post WHERE authorId IN (1, 2, 3, ...);
SELECT * FROM profile WHERE userId IN (1, 2, 3, ...);
```
3 条 SQL——不管有多少用户。TypeORM 先查主表,拿 ID 列表,再用 `IN` 查关联表,最后在内存里组装关系。
### 嵌套关联
```typescript
const users = await userRepository.find({
relations: ['posts', 'posts.comments'],
});
```
加载用户的文章,以及文章的评论。每多一层嵌套多一条 SQL,但仍是固定数量。
### 只加载部分关联字段
```typescript
const users = await userRepository.find({
relations: ['posts'],
select: {
id: true,
name: true,
posts: {
id: true,
title: true, // 只加载文章的 id 和 title
},
},
});
```
## 解决方案二:leftJoinAndSelect
QueryBuilder 版本,更灵活:
```typescript
const users = await userRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.leftJoinAndSelect('user.profile', 'profile')
.getMany();
```
和 `relations` 的区别:
| | relations | leftJoinAndSelect |
|---|---|---|
| SQL 数量 | 多条(主查询 + 每个关联一条) | 一条(JOIN 合并) |
| 大数据量性能 | 更好(IN 查询,无重复行) | 可能差(JOIN 产生笛卡尔积) |
| 灵活性 | 只能加载,不能过滤 | 可以加 WHERE、ORDER BY |
| 去重 | 自动 | 自动(TypeORM 处理) |
**什么时候用 leftJoinAndSelect**:需要对关联数据做过滤或排序时。
```typescript
// 只加载已发布的文章
const users = await userRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post', 'post.published = :published', { published: true })
.getMany();
```
第三个参数是 JOIN 条件——只有 published=true 的文章会被加载。`relations` 做不到这个。
**什么时候用 relations**:只是加载数据不过滤时。`relations` 在数据量大时性能更好——JOIN 1000 个用户每人 10 篇文章会产生 10000 行中间结果,`IN` 查询只有 1000 + 10 = 1010 行。
## 解决方案三:Eager 加载
在实体定义里设置 `eager: true`,每次查用户自动加载文章:
```typescript
@Entity()
export class User {
@OneToMany(() => Post, post => post.author, { eager: true })
posts: Post[];
}
```
```typescript
// 自动加载 posts,不需要显式声明
const users = await userRepository.find();
```
**不推荐**。`eager: true` 让你失去控制——有时候你只需要用户名,结果把文章也查出来了。而且 eager 关联嵌套时,层层加载,性能不可预测。
只在一种场景下用 eager:关联数据**总是**需要一起加载。比如 `User` 和 `UserProfile`(一对一,用户资料总是要一起展示的)。
## 解决方案四:DataLoader(GraphQL 场景)
GraphQL 的查询深度不确定——客户端可能查用户文章,也可能不查。用 `relations` 或 `leftJoinAndSelect` 会过度加载。DataLoader 按需批量加载:
```typescript
import DataLoader from 'dataloader';
const postLoader = new DataLoader(async (authorIds: number[]) => {
// 一次查出所有作者的文章
const posts = await postRepository.find({
where: { authorId: In(authorIds) },
});
// 按 authorId 分组
return authorIds.map(id => posts.filter(p => p.authorId === id));
});
// 在 resolver 里使用
const resolvers = {
User: {
posts: (user) => postLoader.load(user.id),
},
};
```
DataLoader 自动合并同一个请求里的多次 `load()` 调用——10 个用户查文章,只触发 1 次数据库查询。
## 常见陷阱
### 忘记加 relations 就访问关联属性
```typescript
const users = await userRepository.find();
console.log(users[0].posts); // undefined 或 []
```
没加 `relations`,关联属性不会自动加载。TypeORM 不会报错——它返回 `undefined` 或空数组,你的代码以为没有数据。
### leftJoin vs leftJoinAndSelect
```typescript
// leftJoin:只 JOIN 不过 SELECT,关联数据不返回
.leftJoin('user.posts', 'post')
// leftJoinAndSelect:JOIN 并且 SELECT,关联数据返回
.leftJoinAndSelect('user.posts', 'post')
```
用了 `leftJoin` 但没 `addSelect`,关联属性始终为空——很多人卡在这里。
### 过度加载
```typescript
// ❌ 加载了所有关联,但只需要文章数
const users = await userRepository.find({ relations: ['posts'] });
const postCounts = users.map(u => u.posts.length);
// ✅ 用子查询只拿计数
const users = await userRepository
.createQueryBuilder('user')
.loadRelationCountAndMap('user.postCount', 'user.posts')
.getMany();
```
`loadRelationCountAndMap` 只查 COUNT,不加载完整的关联数据——1000 个用户只需 1 条额外的 COUNT 查询。服务端6月5日 00:31
TypeORM事务处理:三种API、隔离级别、悲观锁乐观锁和分布式SagaTypeORM 提供三种写事务的方式:`@Transaction` 装饰器(0.3.x 已移除)、`EntityManager.transaction`、和 `QueryRunner`。选哪种取决于你的场景——简单事务用 `EntityManager.transaction`,需要精细控制用 `QueryRunner`。这篇文章把三种方式、隔离级别、锁机制都讲清楚,以及分布式事务的替代方案。
## 三种事务 API
### 方式一:EntityManager.transaction(推荐日常使用)
最常用的事务写法——传一个回调函数,回调里所有操作在同一个事务里:
```typescript
await dataSource.transaction(async (manager) => {
// 所有操作必须用 manager,不能用全局 repository
const user = await manager.findOne(User, { where: { id: 1 } });
user.balance -= 100;
await manager.save(user);
const target = await manager.findOne(User, { where: { id: 2 } });
target.balance += 100;
await manager.save(target);
});
```
**关键规则**:回调里必须用 `manager` 参数(`EntityManager`),不能用 `dataSource.getRepository()`。全局 Repository 不在事务里,它的操作不受事务保护。
回调函数正常返回 → 自动 commit。抛异常 → 自动 rollback。
### 方式二:QueryRunner(需要精细控制时)
当你需要手动控制 commit/rollback 时机、设置隔离级别、或者同一个 QueryRunner 上执行多个事务时:
```typescript
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.save(User, { name: 'Alice', balance: 100 });
await queryRunner.manager.save(User, { name: 'Bob', balance: 200 });
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release(); // 必须释放,否则连接泄漏
}
```
`release()` 很关键——QueryRunner 持有数据库连接,不释放连接池会耗尽。`finally` 块里 release 保证即使 commit/rollback 抛异常也能释放。
### 方式三:NestJS 里的 @TransactionEntityManager
NestJS + TypeORM 项目用 `@Transaction()` 装饰器(TypeORM 0.3.x 已移除,改用 `DataSource.transaction`):
```typescript
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
@Injectable()
export class TransferService {
constructor(
@InjectDataSource()
private dataSource: DataSource,
) {}
async transfer(fromId: number, toId: number, amount: number) {
return this.dataSource.transaction(async (manager) => {
const from = await manager.findOne(User, { where: { id: fromId } });
const to = await manager.findOne(User, { where: { id: toId } });
if (from.balance < amount) {
throw new Error('余额不足');
}
from.balance -= amount;
to.balance += amount;
await manager.save([from, to]);
});
}
}
```
## 隔离级别
隔离级别决定事务之间能看到彼此的修改到什么程度。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 | 适用场景 |
|----------|------|-----------|------|------|----------|
| READ UNCOMMITTED | ✅ 可能 | ✅ 可能 | ✅ 可能 | 最快 | 几乎不用 |
| READ COMMITTED | ❌ 不会 | ✅ 可能 | ✅ 可能 | 快 | PostgreSQL 默认 |
| REPEATABLE READ | ❌ 不会 | ❌ 不会 | ✅ 可能 | 中 | MySQL 默认 |
| SERIALIZABLE | ❌ 不会 | ❌ 不会 | ❌ 不会 | 最慢 | 金融交易 |
### 设置隔离级别
```typescript
// QueryRunner 方式
await queryRunner.startTransaction('SERIALIZABLE');
// DataSource.transaction 不支持直接设隔离级别
// 需要先用 QueryRunner 设
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction('REPEATABLE READ');
```
### 实际场景
**READ COMMITTED**:大多数 Web 应用。你的 API 请求处理用这个级别够了——一个请求不会因为另一个请求的未提交数据而出错。
**REPEATABLE READ**:同一个事务里多次读同一行数据,结果必须一致。比如生成报表时,事务期间数据不能变。
**SERIALIZABLE**:转账、库存扣减等绝对不能出错的场景。性能代价大,只在关键业务上用。
## 锁机制
### 悲观锁:数据库层面加锁
查询时直接锁行,其他事务不能修改:
```typescript
// 排他锁(FOR UPDATE):其他事务不能读也不能写
const user = await manager.findOne(User, {
where: { id: 1 },
lock: { mode: 'pessimistic_write' },
});
// 共享锁(FOR SHARE):其他事务可读不可写
const user = await manager.findOne(User, {
where: { id: 1 },
lock: { mode: 'pessimistic_read' },
});
```
**使用场景**:扣库存、转账——先锁住行,再修改,防止两个事务同时读到同一个余额然后覆盖。
```typescript
await dataSource.transaction(async (manager) => {
// 先锁行
const account = await manager.findOne(Account, {
where: { id: 1 },
lock: { mode: 'pessimistic_write' },
});
// 再修改
account.balance -= amount;
await manager.save(account);
});
```
不加锁的并发问题:事务 A 和事务 B 同时读到余额 1000,各扣 100,都写入 900——应该是 800。加 `pessimistic_write` 后,事务 B 等 A commit 后才能读,拿到 900 再扣,最终 800。
### 乐观锁:应用层面检查
不锁行,更新时检查数据是否被改过。实体加 `@Version()` 列:
```typescript
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@VersionColumn()
version: number; // 每次更新自动 +1
}
```
```typescript
const user = await manager.findOne(User, { where: { id: 1 } });
// user.version = 1
user.name = 'Alice';
await manager.save(user);
// SQL: UPDATE user SET name = 'Alice', version = 2 WHERE id = 1 AND version = 1
// 如果另一个事务已经改了这行(version 变成 2),WHERE 条件不匹配,影响行数为 0
// TypeORM 抛出 OptimisticLockVersionMismatchError
```
乐观锁不锁行,不阻塞读,适合读多写少的场景。缺点是冲突时需要重试。
### 乐观锁重试模式
```typescript
async function updateWithRetry(id: number, updateFn: (user: User) => void, retries = 3) {
for (let i = 0; i < retries; i++) {
const user = await dataSource.getRepository(User).findOne({ where: { id } });
updateFn(user);
try {
await dataSource.getRepository(User).save(user);
return;
} catch (err) {
if (err.name === 'OptimisticLockVersionMismatchError' && i < retries - 1) {
continue; // 重试
}
throw err;
}
}
}
```
## 死锁
两个事务互相等对方释放锁,永远卡住。TypeORM 不会自动检测死锁——依赖数据库的死锁检测机制。MySQL 和 PostgreSQL 会自动检测并回滚其中一个事务。
避免死锁的原则:
- 事务尽量短——快进快出,不持有锁太久
- 按固定顺序访问资源——总是先锁 A 再锁 B,不要一个先 A 后 B,另一个先 B 后 A
- 不要在事务里做网络请求——网络慢会长时间持有锁
## 分布式事务
TypeORM 没有内置分布式事务支持。如果两个操作分别在不同数据库或不同服务上,不能用 TypeORM 的事务保证原子性。
### 替代方案:Saga 模式
把分布式事务拆成一系列本地事务,每步成功后触发下一步,失败时执行补偿操作:
```typescript
async function createOrderSaga(orderData: OrderData) {
// 步骤1:创建订单(本地事务)
const order = await orderService.createOrder(orderData);
try {
// 步骤2:扣库存(远程调用)
await inventoryService.deduct(orderData.items);
try {
// 步骤3:扣款(远程调用)
await paymentService.charge(orderData.amount);
} catch (err) {
// 步骤3失败:补偿步骤2(还库存)
await inventoryService.restore(orderData.items);
throw err;
}
} catch (err) {
// 步骤2失败:补偿步骤1(取消订单)
await orderService.cancelOrder(order.id);
throw err;
}
}
```
Saga 不保证强一致性——中间状态对外可见(订单创建了但库存还没扣)。但这是分布式系统里唯一的实用方案——两阶段提交(2PC)性能太差,跨服务基本不用。服务端6月5日 00:30
TypeORM QueryBuilder高级查询:条件组合、子查询、分页和批量操作`find()` 和 `findBy()` 只能处理简单查询——多表关联、条件组合、子查询、聚合分组,都得用 QueryBuilder。但 QueryBuilder 的 API 设计有时让人困惑:`leftJoin` 和 `innerJoin` 有什么区别?`where` 和 `andWhere` 能不能混用?子查询怎么写?这篇文章用实际场景把 QueryBuilder 的高级用法讲清楚。
## QueryBuilder 基础回顾
```typescript
// 简单查询:find() 能搞定的
const users = await userRepository.find({
where: { active: true },
order: { createdAt: 'DESC' },
take: 20,
});
// 复杂查询:必须用 QueryBuilder
const users = await userRepository
.createQueryBuilder('user')
.where('user.active = :active', { active: true })
.orderBy('user.createdAt', 'DESC')
.limit(20)
.getMany();
```
`'user'` 是别名,后续引用这个表都用这个别名。SQL 里变成 `SELECT ... FROM user user`。
## 条件组合:OR、AND、嵌套
### OR 条件
```typescript
// 错误:连续写 where,后面的会覆盖前面的
.where('user.active = :active', { active: true })
.where('user.role = :role', { role: 'admin' })
// 生成:WHERE user.role = 'admin'(第一个 where 被覆盖!)
// 正确:用 orWhere
.where('user.active = :active', { active: true })
.orWhere('user.role = :role', { role: 'admin' })
// 生成:WHERE user.active = true OR user.role = 'admin'
```
### 混合 AND + OR
```typescript
// 需求:活跃用户,且(管理员 或 创建于2024年之后)
.where('user.active = :active', { active: true })
.andWhere(
new Brackets(qb => {
qb.where('user.role = :role', { role: 'admin' })
.orWhere('user.createdAt > :date', { date: '2024-01-01' });
})
)
// 生成:WHERE user.active = true AND (user.role = 'admin' OR user.createdAt > '2024-01-01')
```
`Brackets` 是关键——不加的话 OR 会和前面的 AND 混在一起,逻辑不对。需要括号的地方就用 `Brackets`。
### 动态条件
根据用户输入动态拼查询条件:
```typescript
function buildUserQuery(filters: {
name?: string;
role?: string;
minAge?: number;
maxAge?: number;
}) {
const qb = userRepository.createQueryBuilder('user');
if (filters.name) {
qb.andWhere('user.name LIKE :name', { name: `%${filters.name}%` });
}
if (filters.role) {
qb.andWhere('user.role = :role', { role: filters.role });
}
if (filters.minAge) {
qb.andWhere('user.age >= :minAge', { minAge: filters.minAge });
}
if (filters.maxAge) {
qb.andWhere('user.age <= :maxAge', { maxAge: filters.maxAge });
}
return qb;
}
```
**注意参数占位符**:用 `:paramName` + 对象传参,不要字符串拼接——防止 SQL 注入:
```typescript
// ❌ SQL 注入风险
qb.where(`user.name = '${filters.name}'`);
// ✅ 参数化查询
qb.where('user.name = :name', { name: filters.name });
```
## 关联查询:leftJoin vs innerJoin
### leftJoin:左连接,左表数据全保留
```typescript
const users = await userRepository
.createQueryBuilder('user')
.leftJoin('user.posts', 'post')
.getMany();
// 即使没有文章的用户也会返回,post 字段为 []
```
### innerJoin:内连接,只返回有关联数据的行
```typescript
const usersWithPosts = await userRepository
.createQueryBuilder('user')
.innerJoin('user.posts', 'post')
.getMany();
// 只返回至少有一篇文章的用户
```
### 关联数据的选择和过滤
```typescript
// 只查用户的文章标题,不加载整个 post 对象
const users = await userRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.where('post.published = :published', { published: true })
.getMany();
```
`leftJoinAndSelect` 会把关联数据一起查出来(自动 SELECT),`leftJoin` 只 JOIN 不 SELECT。如果你后面要用 `post.title` 做过滤但不需要返回 post 数据,用 `leftJoin`;如果需要返回 post 数据,用 `leftJoinAndSelect`。
## 子查询
### WHERE 里的子查询
查"有超过 5 篇文章的用户":
```typescript
const users = await userRepository
.createQueryBuilder('user')
.where((qb) => {
const subQuery = qb
.subQuery()
.select('post.authorId')
.from(Post, 'post')
.groupBy('post.authorId')
.having('COUNT(post.id) > :count')
.getQuery();
return `user.id IN ${subQuery}`;
})
.setParameter('count', 5)
.getMany();
```
### SELECT 里的子查询(关联计数)
查每个用户的文章数:
```typescript
const users = await userRepository
.createQueryBuilder('user')
.leftJoin('user.posts', 'post')
.groupBy('user.id')
.select('user.id', 'id')
.addSelect('user.name', 'name')
.addSelect('COUNT(post.id)', 'postCount')
.getRawMany();
// 返回: [{ id: 1, name: 'Alice', postCount: '3' }, ...]
```
注意 `getRawMany()` 返回原始数据库行,字段名是 select 里指定的别名,类型都是字符串。`getMany()` 返回实体对象,但聚合查询返回的不是实体,所以必须用 `getRawMany()`。
## 分页
TypeORM 的 `skip` + `take` 是最简单的分页:
```typescript
const [users, total] = await userRepository
.createQueryBuilder('user')
.orderBy('user.createdAt', 'DESC')
.skip((page - 1) * pageSize)
.take(pageSize)
.getManyAndCount();
```
`getManyAndCount()` 返回 `[数据, 总数]`——一次查询做 SELECT + COUNT,比分开两次高效。
**深度分页的坑**:`skip(100000).take(20)` 在 MySQL 上很慢——数据库要扫描前 100020 行再丢弃前 100000 行。深度分页用 keyset pagination:
```typescript
// 不用 offset,用上一页最后一条的 ID
const users = await userRepository
.createQueryBuilder('user')
.where('user.id > :lastId', { lastId: lastIdOfPrevPage })
.orderBy('user.id', 'ASC')
.take(20)
.getMany();
```
ID 有索引时,不管翻到第几页都是 O(1)。
## 聚合和分组
```typescript
// 每个分类的文章数
const stats = await postRepository
.createQueryBuilder('post')
.select('post.category', 'category')
.addSelect('COUNT(*)', 'count')
.addSelect('MAX(post.createdAt)', 'latestPost')
.groupBy('post.category')
.having('COUNT(*) > :minCount', { minCount: 1 })
.getRawMany();
```
`where` 过滤行(分组前),`having` 过滤组(分组后)。
## 窗口函数
TypeORM 的 QueryBuilder 不直接支持窗口函数语法。用 `.getRawMany()` + 原生 SQL:
```typescript
// PostgreSQL:查每个用户的文章数和排名
const ranked = await dataSource.createQueryRunner()
.query(`
SELECT
u.id,
u.name,
COUNT(p.id) AS post_count,
RANK() OVER (ORDER BY COUNT(p.id) DESC) AS rank
FROM "user" u
LEFT JOIN post p ON p."authorId" = u.id
GROUP BY u.id
ORDER BY rank
`);
```
或者用 QueryBuilder 的 `select` 写原始片段:
```typescript
const result = await userRepository
.createQueryBuilder('user')
.leftJoin('user.posts', 'post')
.groupBy('user.id')
.select('user.id', 'id')
.addSelect('user.name', 'name')
.addSelect('COUNT(post.id)', 'postCount')
.addSelect('RANK() OVER (ORDER BY COUNT(post.id) DESC)', 'rank')
.getRawMany();
```
窗口函数只有 PostgreSQL 和 SQL Server 完整支持,MySQL 8+ 也支持。SQLite 不支持。
## 批量操作
### 批量插入
```typescript
await userRepository.insert([
{ name: 'Alice', email: 'a@test.com' },
{ name: 'Bob', email: 'b@test.com' },
{ name: 'Charlie', email: 'c@test.com' },
]);
```
比循环 `save()` 快得多——一条 INSERT 语句插入多行。
### 批量更新
```typescript
await userRepository
.createQueryBuilder()
.update(User)
.set({ active: false })
.where('lastLoginAt < :date', { date: sixMonthsAgo })
.execute();
```
### Upsert(PostgreSQL / MySQL)
```typescript
// PostgreSQL: ON CONFLICT
await userRepository
.createQueryBuilder()
.insert()
.into(User)
.values([{ id: 1, name: 'Alice', email: 'new@test.com' }])
.onConflict('("id") DO UPDATE SET "email" = EXCLUDED."email"')
.execute();
// MySQL: ON DUPLICATE KEY UPDATE
await userRepository
.createQueryBuilder()
.insert()
.into(User)
.values([{ id: 1, name: 'Alice', email: 'new@test.com' }])
.orUpdate(['email'], ['id'])
.execute();
```
Upsert 的语法在 PostgreSQL 和 MySQL 上不同——这是 TypeORM "一套代码多数据库" 最大的例外之一。服务端6月5日 00:29
TypeORM多数据库支持:配置差异、MongoDB限制和多数据源方案TypeORM 的卖点之一是"一套代码跑在多种数据库上"。实际体验下来,SQL 数据库之间迁移确实顺畅,但 MongoDB 是另一回事——文档模型和关系模型的 API 差异很大。这篇文章聚焦实际项目中的数据库选择、配置、以及多数据源场景。
## 支持的数据库一览
TypeORM 支持的数据库分两类:
**SQL 数据库**(API 统一,切换成本低):
- MySQL / MariaDB
- PostgreSQL
- SQLite
- SQL Server
- Oracle
- CockroachDB
- SAP Hana
**文档数据库**(API 有差异):
- MongoDB
关键区别:SQL 数据库共享同一套 QueryBuilder API,切换只改 DataSource 配置。MongoDB 不支持 QueryBuilder 的大部分方法,也不支持事务(TypeORM 层面)、关系懒加载等特性。
## 基础配置
### MySQL
```typescript
import { DataSource } from 'typeorm';
export const appDataSource = new DataSource({
type: 'mysql',
host: process.env.DB_HOST || 'localhost',
port: 3306,
username: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'myapp',
entities: ['src/entity/*.ts'],
synchronize: process.env.NODE_ENV !== 'production', // 生产环境禁止
logging: ['error'], // 只记录错误 SQL
});
```
### PostgreSQL
```typescript
export const appDataSource = new DataSource({
type: 'postgres',
host: process.env.DB_HOST || 'localhost',
port: 5432,
username: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'myapp',
entities: ['src/entity/*.ts'],
synchronize: process.env.NODE_ENV !== 'production',
// PostgreSQL 特有配置
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
schema: 'public',
});
```
MySQL 和 PostgreSQL 的配置几乎一样——改 `type` 和端口就行。实体代码不需要任何修改。
### SQLite(本地开发和测试)
```typescript
export const appDataSource = new DataSource({
type: 'sqlite',
database: 'data/myapp.db', // 文件数据库
// database: ':memory:', // 内存数据库(测试用)
entities: ['src/entity/*.ts'],
synchronize: true,
});
```
SQLite 的优势:零配置、零依赖、单文件。适合桌面应用(Electron)、CLI 工具、本地开发。缺点:不支持并发写入、不支持 `RETURNING`、JSON 函数有限。
### MongoDB
```typescript
export const appDataSource = new DataSource({
type: 'mongodb',
url: process.env.MONGODB_URL || 'mongodb://localhost:27017/myapp',
entities: ['src/entity/*.ts'],
synchronize: process.env.NODE_ENV !== 'production',
// MongoDB 特有配置
authSource: 'admin',
replicaSet: 'rs0', // 如果用了副本集
});
```
MongoDB 的实体定义和 SQL 不同——没有 `@Column`,用 `@ObjectIdColumn` 和 `@Field`:
```typescript
import { Entity, ObjectIdColumn, ObjectId, Column as Field } from 'typeorm';
@Entity()
export class User {
@ObjectIdColumn()
_id: ObjectId;
@Field()
name: string;
@Field()
email: string;
@Field(type => [String]) // 数组字段
tags: string[];
}
```
**MongoDB 的限制**:
- 不支持 `@JoinColumn`、`@ManyToMany` 等 SQL 关系装饰器
- 不支持 QueryBuilder 的 `leftJoin`、`subQuery`
- `Repository.find()` 的 `where` 语法不同(用 MongoDB 查询对象)
- 不支持数据库层面的约束(唯一约束、外键)
## 数据库选择指南
| 场景 | 推荐 | 原因 |
|------|------|------|
| Web 后端 API | PostgreSQL | 功能最全(JSON、全文搜索、数组、窗口函数) |
| 已有 MySQL 基础设施 | MySQL | 不需要额外学习,TypeORM 完全支持 |
| 桌面应用(Electron) | SQLite | 零依赖,单文件分发 |
| 测试 | SQLite :memory: | 最快,每个测试隔离 |
| 文档型数据、灵活 schema | MongoDB | 无需预定义表结构 |
| 高并发读 + 简单写 | MySQL + 读写分离 | MySQL 主从复制成熟 |
PostgreSQL 是新项目的默认推荐——JSON 支持、全文搜索、数组类型、窗口函数,比 MySQL 功能丰富很多,而且 TypeORM 的 PostgreSQL 支持最完善。
## 多数据源配置
一个项目需要连接多个数据库的场景:读写分离(主从)、跨库查询、迁移过渡期。
### 注册多个 DataSource
```typescript
// data-sources.ts
export const primaryDataSource = new DataSource({
name: 'primary', // 必须有 name
type: 'postgres',
host: 'primary-db.example.com',
// ...
entities: ['src/entity/*.ts'],
});
export const secondaryDataSource = new DataSource({
name: 'secondary', // 必须有 name
type: 'mysql',
host: 'legacy-db.example.com',
// ...
entities: ['src/entity-legacy/*.ts'],
});
```
### NestJS 里使用多数据源
```typescript
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
name: 'primary',
type: 'postgres',
host: 'primary-db.example.com',
entities: [User, Post],
}),
TypeOrmModule.forRoot({
name: 'secondary',
type: 'mysql',
host: 'legacy-db.example.com',
entities: [LegacyUser],
}),
// 模块指定使用哪个数据源
TypeOrmModule.forFeature([User, Post], 'primary'),
TypeOrmModule.forFeature([LegacyUser], 'secondary'),
],
})
export class AppModule {}
```
第二个参数 `'primary'` / `'secondary'` 指定该模块用哪个数据源。不同模块可以用不同数据源。
### 跨库查询
TypeORM 不支持跨数据源的 JOIN。需要手动查两个库再在代码里合并:
```typescript
async function getUserWithLegacy(userId: number) {
const user = await primaryDataSource.getRepository(User)
.findOne({ where: { id: userId } });
const legacyUser = await secondaryDataSource.getRepository(LegacyUser)
.findOne({ where: { email: user.email } });
return { ...user, legacyData: legacyUser };
}
```
## 数据库切换和迁移
### 从 MySQL 切换到 PostgreSQL
1. 修改 DataSource 配置(`type: 'mysql'` → `type: 'postgres'`)
2. 检查实体里的 MySQL 特有类型(如 `tinyint` 改成 `boolean`)
3. 生成迁移文件:`npx typeorm migration:generate -d src/data-source.ts src/migration/InitPg`
4. 跑迁移:`npx typeorm migration:run -d src/data-source.ts`
5. 检查 QueryBuilder 里有没有 MySQL 专有语法(如 `` `backtick` `` 改成 `"double quote"`)
大部分情况下只需改配置和迁移,实体代码不用动。
### synchronize 的正确用法
`synchronize: true` 在开发时方便——改实体自动同步表结构。但**生产环境必须关闭**,否则:
- 删字段时直接 `ALTER TABLE DROP COLUMN`,数据丢失
- 重命名字段被当作"删旧列 + 加新列",数据丢失
- 并发启动多个实例可能同时执行 schema 变更
生产环境用迁移:`typeorm migration:run`。服务端6月5日 00:27
TypeORM实体继承:单表、类表和具体表继承的选择与实现TypeORM 支持三种实体继承模式,名字听着抽象,但对应的是数据库设计里真实的问题:不同类型的数据,是放一张表还是分多张表?放一张表会有大量 NULL 列,分多张表 JOIN 查询变慢。这篇文章把三种模式的实现方式、优缺点、以及什么时候选哪种,都讲清楚。
## 三种模式对比
| 模式 | 表结构 | 查询性能 | NULL 列 | 适用场景 |
|------|--------|----------|---------|----------|
| 单表继承(STI) | 一张表,鉴别列区分类型 | 最好(无 JOIN) | 多 | 子类字段差异小 |
| 类表继承(CTI) | 父类一张 + 每个子类一张 | 中(需 JOIN) | 少 | 子类字段差异大,但有关联查询需求 |
| 具体表继承(CTI-var) | 每个子类独立一张表 | 差(UNION 查所有类型) | 无 | 子类完全独立,很少跨类型查询 |
## 单表继承(Single Table Inheritance)
所有子类存在同一张表里,用鉴别列(discriminator column)标识类型。子类独有的列在不对应的行里为 NULL。
### 实现
```typescript
import { Entity, PrimaryGeneratedColumn, Column, ChildEntity, DiscriminatorValue } from 'typeorm';
@Entity()
@DiscriminatorColumn({ name: 'type' })
export class Content {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column({ type: 'text' })
body: string;
}
@ChildEntity()
@DiscriminatorValue('article')
export class Article extends Content {
@Column({ nullable: true }) // 必须设 nullable: true
author: string;
@Column({ nullable: true })
category: string;
}
@ChildEntity()
@DiscriminatorValue('video')
export class Video extends Content {
@Column({ nullable: true })
url: string;
@Column({ nullable: true })
duration: number;
}
```
生成的表结构:
```
content
┌────┬───────┬───────────┬────────┬──────┬──────────┬─────┬──────────┐
│ id │ type │ title │ body │ url │ duration │ author │ category │
├────┼───────┼───────────┼────────┼──────┼──────────┼─────┼──────────┤
│ 1 │ article│ ... │ ... │ NULL │ NULL │ Alice│ tech │
│ 2 │ video │ ... │ ... │ a.mp4│ 600 │ NULL │ NULL │
└────┴───────┴───────────┴────────┴──────┴──────────┴─────┴──────────┘
```
### 查询方式
```typescript
// 查所有内容(不管类型)
const all = await dataSource.getRepository(Content).find();
// 只查文章(TypeORM 自动加 WHERE type = 'article')
const articles = await dataSource.getRepository(Article).find();
// 按 type 过滤
const videos = await dataSource.getRepository(Content)
.find({ where: { type: 'video' } });
```
### NULL 列问题
单表继承最大的问题:子类独有字段在不对应的行里全是 NULL。3 个子类各 5 个独有字段,表里就有 15 个可能为 NULL 的列。如果子类差异大,一张表 20+ 列一半是 NULL,可读性和索引效率都差。
**适合场景**:子类字段少、差异小。比如 `User` 分 `Admin`/`Member`,独有字段各 2-3 个。
## 类表继承(Class Table Inheritance)
父类一张表,每个子类各一张表,子类表通过外键关联父类表。没有 NULL 列问题,但查询需要 JOIN。
### 实现
```typescript
@Entity()
@TableInheritance({ column: { type: 'varchar', name: 'type' } })
export class Person {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
email: string;
}
@ChildEntity()
export class Employee extends Person {
@Column()
position: string;
@Column({ type: 'decimal', precision: 10, scale: 2 })
salary: number;
}
@ChildEntity()
export class Customer extends Person {
@Column()
companyName: string;
@Column({ default: 0 })
loyaltyPoints: number;
}
```
生成的表结构:
```
person employee customer
┌────┬───────┬──────┬───────────┐ ┌────┬──────────┬────────┐ ┌────┬─────────────┬───────────────┐
│ id │ type │ name │ email │ │ id │ position │ salary │ │ id │ companyName │ loyaltyPoints │
├────┼───────┼──────┼───────────┤ ├────┼──────────┼────────┤ ├────┼─────────────┼───────────────┤
│ 1 │ employee│ Alice│ a@t.com│ │ 1 │ Engineer │ 80000 │ │ │ │ │
│ 2 │ customer│ Bob │ b@t.com│ │ │ │ │ │ 2 │ Acme Inc │ 100 │
└────┴───────┴──────┴───────────┘ └────┴──────────┴────────┘ └────┴─────────────┴───────────────┘
```
`person.id` 和 `employee.id`/`customer.id` 是同一个值——子类表的主键同时也是父类表的外键。
### 查询方式
```typescript
// 查所有人(TypeORM 自动 JOIN)
const all = await dataSource.getRepository(Person).find();
// 只查员工(自动 JOIN person + employee)
const employees = await dataSource.getRepository(Employee).find();
// 保存
const emp = new Employee();
emp.name = 'Alice';
emp.email = 'alice@test.com';
emp.position = 'Engineer';
emp.salary = 80000;
await dataSource.getRepository(Employee).save(emp);
// 自动向 person 和 employee 两张表插入数据
```
### JOIN 的性能代价
每次查子类都要 JOIN 父类表。数据量大时 JOIN 比单表查询慢。但如果你的查询经常只查父类字段(如按 name 搜索),不需要 JOIN,直接查 person 表即可。
**适合场景**:子类字段差异大,且经常需要跨类型查询公共字段。
## 具体表继承(Concrete Table Inheritance)
每个子类一张独立的表,父类不建表。公共字段在每个子类表里重复。TypeORM 用 `@Column()` 在抽象类里定义,子类继承后各自建表。
### 实现
```typescript
export abstract class Payment {
@PrimaryGeneratedColumn()
id: number;
@Column()
amount: number;
@Column()
currency: string;
@Column()
createdAt: Date;
}
@Entity()
export class CreditCardPayment extends Payment {
@Column()
cardNumber: string;
@Column()
expiryDate: string;
}
@Entity()
export class BankTransferPayment extends Payment {
@Column()
bankName: string;
@Column()
accountNumber: string;
}
```
生成的表结构:
```
credit_card_payment bank_transfer_payment
┌────┬────────┬──────────┬──────┬───────────┬────────────┐ ┌────┬────────┬──────────┬──────┬──────────┬───────────────┐
│ id │ amount │ currency │ date │ cardNumber│ expiryDate│ │ id │ amount │ currency │ date │ bankName │ accountNumber │
└────┴────────┴──────────┴──────┴───────────┴────────────┘ └────┴────────┴──────────┴──────┴──────────┴───────────────┘
```
公共字段 `amount`、`currency`、`createdAt` 在每张表里都有。没有父类表,没有 JOIN,也没有 NULL 列。
### 查询所有支付类型
具体表继承最大的问题:查"所有类型的支付"需要 UNION:
```typescript
// TypeORM 不直接支持跨子类的 UNION 查询
// 需要手动查每张表再合并
const [creditCards, bankTransfers] = await Promise.all([
dataSource.getRepository(CreditCardPayment).find(),
dataSource.getRepository(BankTransferPayment).find(),
]);
const allPayments = [...creditCards, ...bankTransfers]
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
```
没有统一的 Repository 可以查所有子类——因为根本没有父类表。
**适合场景**:子类完全独立,几乎不需要跨类型查询。比如支付方式、通知渠道。
## 选择决策
```
子类独有字段多吗?
├─ 少(每个子类 2-3 个独有字段)→ 单表继承(STI)
└─ 多(每个子类 5+ 个独有字段)→ 经常跨类型查询吗?
├─ 是 → 类表继承(CTI)
└─ 否 → 具体表继承
```
### 迁移时的注意事项
- 单表继承加新子类:加列就行,`ALTER TABLE ADD COLUMN`,不用建新表
- 类表继承加新子类:新建一张子类表,父类表不变
- 具体表继承加新子类:新建一张独立表
单表继承加子类最灵活——只加列,不动已有数据。类表和具体表继承加子类需要新表,但不会影响已有表结构。
如果未来子类数量不确定或可能频繁增加,优先选单表继承。服务端6月5日 00:26
TypeORM测试:Mock Repository、SQLite内存数据库和NestJS集成TypeORM 的测试分两层:不依赖数据库的纯逻辑测试(单元测试),和需要真实数据库交互的测试(集成测试)。很多人所有测试都连数据库,跑得又慢又不稳定;也有人不连数据库,Mock 了一大堆,测完发现线上还是出 bug。这篇文章把两种策略的使用场景和实现方式都讲清楚。
## 测试环境:SQLite 内存数据库
集成测试需要真实数据库,但不需要用 MySQL/PostgreSQL——SQLite 内存数据库够用,速度快 10 倍以上,每个测试文件启动不到 100ms:
```typescript
import { DataSource } from 'typeorm';
import { User } from '../entity/User';
import { Post } from '../entity/Post';
export const testDataSource = new DataSource({
type: 'sqlite',
database: ':memory:',
entities: [User, Post],
synchronize: true, // 自动建表
logging: false,
});
```
为什么用 SQLite 而不是真实数据库:
- **零配置**:不需要装数据库、建测试库、管连接字符串
- **隔离性**:内存数据库每次测试完自动销毁,测试之间无干扰
- **速度**:内存操作,无网络 IO,单测 < 50ms
但 SQLite 不支持某些 MySQL/PostgreSQL 特性(如 `RETURNING`、`ON CONFLICT`、JSON 函数)。如果你的查询用了这些特性,需要用真实的 PostgreSQL 做集成测试。
### 测试基础设施
```typescript
// test/setup.ts
import { DataSource } from 'typeorm';
let dataSource: DataSource;
beforeAll(async () => {
dataSource = new DataSource({
type: 'sqlite',
database: ':memory:',
entities: [__dirname + '/../src/entity/*.ts'],
synchronize: true,
});
await dataSource.initialize();
});
afterAll(async () => {
await dataSource.destroy();
});
// 每个测试前清空所有表
afterEach(async () => {
const entities = dataSource.entityMetadatas;
for (const entity of entities) {
const repository = dataSource.getRepository(entity.name);
await repository.clear();
}
});
```
## 单元测试:不连数据库
单元测试只测业务逻辑,不测数据库交互。Repository 方法用 Mock 替代。
### Mock Repository
```typescript
import { UsersService } from './users.service';
import { Repository } from 'typeorm';
import { User } from './user.entity';
describe('UsersService', () => {
let service: UsersService;
let mockRepository: jest.Mocked<Repository<User>>;
beforeEach(() => {
// 创建 Mock Repository
mockRepository = {
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
remove: jest.fn(),
count: jest.fn(),
} as any;
service = new UsersService(mockRepository);
});
it('should return all users', async () => {
const mockUsers = [
{ id: 1, name: 'Alice', email: 'alice@test.com' },
{ id: 2, name: 'Bob', email: 'bob@test.com' },
];
mockRepository.find.mockResolvedValue(mockUsers as User[]);
const result = await service.findAll();
expect(result).toEqual(mockUsers);
expect(mockRepository.find).toHaveBeenCalled();
});
it('should create a user', async () => {
const dto = { name: 'Alice', email: 'alice@test.com' };
const savedUser = { id: 1, ...dto };
mockRepository.create.mockReturnValue(savedUser as User);
mockRepository.save.mockResolvedValue(savedUser as User);
const result = await service.create(dto);
expect(result).toEqual(savedUser);
expect(mockRepository.create).toHaveBeenCalledWith(dto);
expect(mockRepository.save).toHaveBeenCalled();
});
it('should throw when user not found', async () => {
mockRepository.findOne.mockResolvedValue(null);
await expect(service.findOne(999)).rejects.toThrow('User not found');
});
});
```
Mock 的核心原则:**只 Mock 外部依赖(数据库),不 Mock 被测代码本身的逻辑**。如果 Service 里有个计算函数,直接测它,不要 Mock。
### 什么时候用 Mock,什么时候不用
| 场景 | 策略 | 原因 |
|------|------|------|
| 纯逻辑函数 | 不 Mock | 没有外部依赖 |
| Service → Repository | Mock Repository | 隔离数据库,测试快 |
| 复杂查询逻辑 | 不 Mock,用集成测试 | Mock 查询结果无法验证 SQL 正确性 |
| 第三方 API | Mock HTTP | 不能调外部服务 |
**关键判断**:你的测试目的是验证"代码逻辑对不对"还是"SQL 查询对不对"?前者用 Mock,后者用集成测试。
## 集成测试:连真实数据库
集成测试验证 SQL 是否正确、事务是否生效、约束是否有效——这些 Mock 测不了。
### 事务回滚策略
每个测试跑在一个事务里,测完回滚,数据库回到干净状态:
```typescript
describe('User integration tests', () => {
let dataSource: DataSource;
let userRepository: Repository<User>;
beforeAll(async () => {
dataSource = new DataSource({
type: 'sqlite',
database: ':memory:',
entities: [User],
synchronize: true,
});
await dataSource.initialize();
userRepository = dataSource.getRepository(User);
});
afterAll(async () => {
await dataSource.destroy();
});
it('should create and find a user', async () => {
const user = userRepository.create({
name: 'Alice',
email: 'alice@test.com',
});
await userRepository.save(user);
const found = await userRepository.findOne({
where: { email: 'alice@test.com' },
});
expect(found).toBeDefined();
expect(found!.name).toBe('Alice');
});
it('should enforce unique email constraint', async () => {
await userRepository.save({ name: 'Alice', email: 'alice@test.com' });
await expect(
userRepository.save({ name: 'Bob', email: 'alice@test.com' })
).rejects.toThrow(); // SQLite 会抛 UNIQUE constraint 错误
});
});
```
### 测试复杂查询
Query Builder 的复杂查询必须用集成测试——Mock 的 `find` 返回值证明不了 SQL 写对了:
```typescript
it('should find users with post count > 5', async () => {
// 准备数据
const user = await userRepository.save({ name: 'Alice', email: 'a@test.com' });
for (let i = 0; i < 6; i++) {
await postRepository.save({ title: `Post ${i}`, authorId: user.id });
}
// 执行复杂查询
const result = await userRepository
.createQueryBuilder('user')
.leftJoin('user.posts', 'post')
.groupBy('user.id')
.having('COUNT(post.id) > 5')
.getMany();
expect(result).toHaveLength(1);
expect(result[0].name).toBe('Alice');
});
```
## NestJS + TypeORM 测试
NestJS 项目里,TypeORM 通过模块注入,测试需要用 `@nestjs/testing`:
### 单元测试(Mock DataSource)
```typescript
import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { User } from './user.entity';
describe('UsersService', () => {
let service: UsersService;
let mockRepo: any;
beforeEach(async () => {
mockRepo = {
find: jest.fn().mockResolvedValue([]),
findOne: jest.fn().mockResolvedValue(null),
save: jest.fn().mockResolvedValue({}),
create: jest.fn().mockReturnValue({}),
};
const module = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: mockRepo },
],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
```
`getRepositoryToken(User)` 是关键——NestJS 用这个 token 注入 Repository,Mock 时也用同一个 token。
### E2E 测试(真实数据库)
```typescript
import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import * as request from 'supertest';
describe('Users API (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [User],
synchronize: true,
}),
TypeOrmModule.forFeature([User]),
UsersModule,
],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
it('POST /users', () => {
return request(app.getHttpServer())
.post('/users')
.send({ name: 'Alice', email: 'alice@test.com' })
.expect(201);
});
it('GET /users', () => {
return request(app.getHttpServer())
.get('/users')
.expect(200);
});
});
```
E2E 测试用 SQLite 内存数据库,请求走完整的 HTTP → Controller → Service → Repository → 数据库链路,最接近真实场景。
## 测试策略总结
| 测试类型 | 用途 | 速度 | 数据库 | 覆盖范围 |
|----------|------|------|--------|----------|
| 单元测试 | 验证业务逻辑 | < 10ms | 不需要(Mock) | Service 层逻辑 |
| 集成测试 | 验证 SQL 和约束 | 50-200ms | SQLite 内存 | Repository 查询 |
| E2E 测试 | 验证完整请求链路 | 100-500ms | SQLite 内存 | HTTP → 数据库 |
**经验法则**:单元测试占 70%,集成测试占 20%,E2E 测试占 10%。核心业务逻辑用单元测试覆盖,复杂查询用集成测试验证,API 入口用少量 E2E 测试保证链路畅通。服务端6月2日 23:06
TypeORM 实体关系怎么定义?一对一、一对多、多对多装饰器详解TypeORM 用装饰器定义实体间的关系:一对一、一对多、多对多。关键是搞清谁是"拥有方"(存外键的一方),谁是"反方"(只声明关系不存外键)。
## 一对一(OneToOne)
一个人只有一个档案,一个档案只属于一个人:
```typescript
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@OneToOne(() => Profile, profile => profile.user)
@JoinColumn() // 外键加在这一侧(拥有方)
profile: Profile;
}
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@OneToOne(() => User, user => user.profile)
user: User; // 反方,不加 @JoinColumn
}
```
`@JoinColumn()` 标记拥有方——数据库会在 User 表加一个 `profileId` 外键列。一对一关系只有一方能加 `@JoinColumn`,另一方是反向引用。
## 一对多 / 多对一(OneToMany / ManyToOne)
一个用户有多篇文章,一篇文章属于一个用户:
```typescript
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@OneToMany(() => Post, post => post.author)
posts: Post[];
}
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => User, user => user.posts)
@JoinColumn({ name: 'authorId' }) // 外键在 Post 表
author: User;
}
```
**ManyToOne 总是拥有方**——外键存在"多"的一侧的表里。OneToMany 只是反向引用,不能单独存在,必须配对 ManyToOne。
数据库里 Post 表有一个 `authorId` 列指向 User.id。
## 多对多(ManyToMany)
一个文章有多个标签,一个标签属于多篇文章:
```typescript
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@ManyToMany(() => Tag, tag => tag.posts)
@JoinTable() // 拥有方加 @JoinTable,自动创建中间表
tags: Tag[];
}
@Entity()
export class Tag {
@PrimaryGeneratedColumn()
id: number;
@ManyToMany(() => Post, post => post.tags)
posts: Post[]; // 反方,不加 @JoinTable
}
```
`@JoinTable()` 让 TypeORM 自动创建中间表 `post_tags_tags`(post_id + tag_id)。只有一方加 `@JoinTable`,另一方是反向引用。
## 加载关联数据
定义了关系不代表查询时自动加载。必须显式指定:
```typescript
// 方式一:find 时指定 relations
const user = await userRepo.findOne({
where: { id: 1 },
relations: ['posts', 'posts.tags'] // 支持嵌套
});
// 方式二:QueryBuilder join
const user = await userRepo
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.leftJoinAndSelect('post.tags', 'tag')
.where('user.id = :id', { id: 1 })
.getOne();
```
不加 `relations` 或 `leftJoinAndSelect`,返回的 user.posts 就是 undefined。这是 TypeORM 的设计选择——避免无意义的 JOIN 查询。
## 常见坑
**循环引用**:User 引用 Post,Post 引用 User。TypeScript 用 `() => Post` 延迟求值避免循环依赖。不要直接写 `Post`,必须用箭头函数。
**级联操作**:`cascade: true` 让保存 User 时自动保存关联的 Post。小心使用——可能在你不知情时写入大量数据。建议只在明确的父子关系上使用。
**删除行为**:`onDelete: 'CASCADE'` 在数据库层面级联删除。删除 User 时自动删除其所有 Post。比应用层级联更可靠。服务端6月2日 23:04
TypeORM Active Record 和 Data Mapper 有什么区别?怎么选?TypeORM 支持两种数据访问模式:Active Record(实体自带 CRUD 方法)和 Data Mapper(通过 Repository 操作实体)。选哪种影响代码组织方式,但不影响功能。
## Active Record 模式
实体继承 `BaseEntity`,直接调用静态方法操作数据:
```typescript
@Entity()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
}
// 使用:实体自带 find/save/remove
const users = await User.find();
const user = await User.findOne({ where: { email: 'test@test.com' } });
user.name = 'Updated';
await user.save();
await user.remove();
```
优点:代码简洁,一个对象既承载数据又负责持久化。适合简单 CRUD 和小项目。
缺点:实体和数据库操作耦合——同一个类既定义数据结构又包含查询逻辑,业务复杂后类会膨胀。
## Data Mapper 模式
实体是纯数据对象,通过 Repository 操作:
```typescript
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
}
// 使用:通过 Repository 操作
const userRepo = dataSource.getRepository(User);
const users = await userRepo.find();
const user = await userRepo.findOne({ where: { email: 'test@test.com' } });
user.name = 'Updated';
await userRepo.save(user);
await userRepo.remove(user);
```
优点:关注点分离——实体只管数据结构,Repository 负责持久化逻辑。业务代码不依赖数据库 API,方便单元测试(mock Repository)。
缺点:多一层间接,简单操作代码量稍多。
## 怎么选
**Active Record 适合**:
- 小项目,实体少,查询逻辑简单
- 快速原型,不想写太多样板代码
- Rails/Django 背景,习惯模型自带操作
**Data Mapper 适合**:
- 中大型项目,业务逻辑复杂
- 需要单元测试,要 mock 数据层
- 领域驱动设计(DDD)风格,实体只表达业务概念
NestJS 项目推荐 Data Mapper——NestJS 的 `@InjectRepository()` 天然就是 Data Mapper 风格,依赖注入也让测试更方便。
## 两种模式可以混用
TypeORM 不强制二选一。同一个实体可以同时用两种方式:
```typescript
@Entity()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
// Active Record 方式
static findActive() {
return this.find({ where: { active: true } });
}
}
// Data Mapper 方式也可以
const repo = dataSource.getRepository(User);
const users = await repo.find({ where: { active: true } });
```
但不建议混用——团队应该统一风格,避免同一项目里两种模式交叉出现。
## 自定义查询放哪里
Active Record 模式的常见问题:复杂查询方法堆积在实体类上。
```typescript
// 不推荐:实体类越来越胖
export class User extends BaseEntity {
static findWithStats() { ... }
static findActiveUsers() { ... }
static findByDepartment() { ... }
static searchByName() { ... }
}
// 推荐:抽取到单独的 Service 或自定义 Repository
export class UserService {
constructor(@InjectRepository(User) private repo: Repository<User>) {}
async findWithStats() {
return this.repo.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.loadRelationCountAndMap('user.postCount', 'user.posts')
.getMany();
}
}
```
Data Mapper 模式自然避免这个问题——查询逻辑在 Service 里,实体保持干净。服务端6月2日 01:50
TypeORM 查询缓存怎么配?Redis 缓存和缓存一致性实战数据库查询是后端应用最常见的性能瓶颈。缓存是第一道防线——能从缓存拿的数据就不查数据库。TypeORM 支持查询级缓存和 Redis 缓存,配置简单但有几个细节要注意。
## TypeORM 查询缓存
开启缓存后,相同的 SQL 查询结果会被缓存,指定时间内不重复查数据库:
```typescript
TypeOrmModule.forRoot({
type: 'postgres',
cache: {
type: 'redis',
options: {
host: 'localhost',
port: 6379,
},
duration: 30000, // 缓存 30 秒
},
}),
```
在查询时指定缓存:
```typescript
const users = await userRepo.find({
cache: true, // 使用全局 duration(30 秒)
});
// 或自定义缓存时间
const users = await userRepo.find({
cache: 60000, // 缓存 60 秒
});
// QueryBuilder 也可以
const users = await createQueryBuilder('user')
.cache(true)
.getMany();
```
缓存 key 基于 SQL 语句自动生成——相同 SQL 共享缓存,不同 SQL 各自缓存。
## 什么时候用缓存
**适合缓存的查询**:
- 配置数据(很少变)
- 用户信息(变更频率低)
- 热门文章列表(可以接受短暂不一致)
**不适合缓存的查询**:
- 实时数据(库存、余额)
- 分页偏移大的查询(缓存命中率低)
- 写多读少的数据(缓存频繁失效)
## 缓存一致性
缓存的经典问题:数据库更新了但缓存还是旧数据。TypeORM 不会自动在数据变更时清除缓存——需要手动处理。
**方案一:写入后清除缓存**
```typescript
async function updateUser(id: number, data: Partial<User>) {
await userRepo.update(id, data);
// 清除与 User 相关的所有缓存
await getConnection().queryResultCache.clear();
}
```
`queryResultCache.clear()` 清除所有缓存,比较粗暴但简单。精细清除需要知道具体的缓存 key,TypeORM 没有直接提供按表清除的 API。
**方案二:短过期时间 + 接受短暂不一致**
```typescript
const users = await userRepo.find({
cache: 5000, // 5 秒过期,最多延迟 5 秒
});
```
简单有效,大部分场景够用。5 秒的不一致对用户体验几乎没有影响。
**方案三:手动管理缓存(Redis 直接操作)**
绕过 TypeORM 的缓存机制,用 ioredis 自己管理:
```typescript
import Redis from 'ioredis';
const redis = new Redis();
async function getUser(id: string) {
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached);
const user = await userRepo.findOne(id);
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 60);
return user;
}
async function updateUser(id: string, data: Partial<User>) {
await userRepo.update(id, data);
await redis.del(`user:${id}`); // 精确清除
}
```
自己管理更灵活——可以按 key 精确清除、设置不同过期时间、做缓存预热。但需要更多代码。
## 缓存命中率监控
Redis 缓存命中率反映了缓存是否有效:
```bash
redis-cli info stats | grep keyspace_hits
redis-cli info stats | grep keyspace_misses
```
命中率低于 50% 说明缓存策略有问题——要么过期时间太短,要么查询太分散没有复用。
## TypeORM 缓存的局限
- 缓存粒度是 SQL 级别,不是实体级别。同一个 User 的不同查询(列表查询 vs 详情查询)有各自独立的缓存
- 没有 Cache Aside 模式的内置支持(先查缓存再查数据库)
- 分布式环境下需要用 Redis 共享缓存,内存缓存(默认)会导致各实例缓存不一致
如果缓存需求复杂,建议绕过 TypeORM 缓存,直接用 Redis + 自定义缓存层。服务端6月2日 01:49
TypeORM 软删除怎么用?@DeleteDateColumn 配置和唯一约束冲突解决软删除不是真的删除数据,而是标记 `deletedAt` 字段,查询时自动过滤已删除的记录。TypeORM 内置支持软删除,一行配置开启。
## 开启软删除
在实体上加 `@DeleteDateColumn()`:
```typescript
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@DeleteDateColumn()
deletedAt: Date; // null = 未删除,有值 = 已删除
}
```
`delete` 操作变成 UPDATE:
```typescript
await userRepo.softDelete(1);
// SQL: UPDATE user SET "deletedAt" = NOW() WHERE id = 1
await userRepo.remove(user);
// 同样效果,设置 deletedAt 而不是 DELETE
```
`find` 自动排除已删除记录:
```typescript
const users = await userRepo.find();
// SQL: SELECT * FROM user WHERE "deletedAt" IS NULL
```
## 查询包含已删除的记录
```typescript
// 包含已删除的记录
const allUsers = await userRepo.find({
withDeleted: true
});
// 只查已删除的记录
const deletedUsers = await userRepo.find({
where: { deletedAt: Not(IsNull()) },
withDeleted: true
});
```
`withDeleted: true` 告诉 TypeORM 不要加 `WHERE "deletedAt" IS NULL` 条件。
## 恢复已删除的记录
```typescript
await userRepo.restore(1);
// SQL: UPDATE user SET "deletedAt" = NULL WHERE id = 1
```
`restore` 把 `deletedAt` 设回 NULL,记录恢复正常。
## 软删除的坑
**1. 唯一约束冲突**
软删除后,唯一字段(如 email)仍然占用唯一约束。删除 user@email.com 后,新建同名用户会报唯一冲突。
解决方案:唯一约束包含 deletedAt。
```typescript
@Entity()
@Unique(['email', 'deletedAt'])
export class User {
@Column()
email: string;
@DeleteDateColumn()
deletedAt: Date;
}
```
但这样未删除记录的 deletedAt 是 NULL,多条 NULL 在 PostgreSQL 的唯一约束里不冲突(符合 SQL 标准),MySQL 则需要用条件索引或去掉唯一约束。
**2. 关联数据不会级联软删除**
```typescript
@OneToMany(() => Post, post => post.author)
posts: Post[];
```
软删除 User 时,Post 不会被自动软删除。需要手动处理:
```typescript
async function softDeleteUser(id: number) {
await postRepo.softDelete({ authorId: id });
await userRepo.softDelete(id);
}
```
**3. 物理删除仍然是需要的**
软删除的数据积累会膨胀表。定期清理:
```typescript
// 物理删除 30 天前软删除的记录
await userRepo
.createQueryBuilder()
.delete()
.where("deletedAt < :date", { date: new Date(Date.now() - 30 * 86400000) })
.execute();
```
## 不用软删除的替代方案
- **事件溯源**:不删除数据,而是追加"已删除"事件。查询时通过事件回放构建当前状态
- **归档表**:DELETE 前把数据 INSERT 到归档表,然后物理删除。查询只看主表,归档表做审计
软删除适合"需要恢复、需要审计追踪"的场景。如果只是为了安全,备份 + 物理删除更干净。服务端6月2日 01:49
Node.js ORM 怎么选?TypeORM、Prisma、Sequelize、MikroORM 对比Node.js 的 ORM 主要有四个:TypeORM、Prisma、Sequelize、MikroORM。2025 年的格局:Prisma 增长最快,TypeORM 用量最大但有退潮趋势,Sequelize 已经老化,MikroORM 是小众但口碑好。
## 四个 ORM 对比
| 维度 | TypeORM | Prisma | Sequelize | MikroORM |
|------|---------|--------|-----------|----------|
| 类型安全 | 弱(装饰器 + any) | 强(自动生成类型) | 弱 | 强 |
| 查询方式 | 装饰器 + QueryBuilder | 链式 API | 链式 API | 链式 API + QB |
| 迁移工具 | 内置,体验一般 | 最好 | 内置 | 内置 |
| 关联查询 | 容易 N+1 | include 清晰 | include | 自动 JOIN |
| 社区 | 最大 | 增长最快 | 缩小中 | 小但活跃 |
| 学习曲线 | 中 | 低 | 高 | 中 |
| NestJS 集成 | 官方推荐 | 社区包 | 社区包 | 社区包 |
## Prisma:类型安全的赢家
Prisma 的核心优势是类型推导——schema 定义一次,查询自动补全、返回类型自动推导,几乎不需要手写类型。
```prisma
model User {
id String @id @default(uuid())
email String @unique
posts Post[]
}
```
```typescript
const user = await prisma.user.findUnique({
where: { email: 'test@test.com' },
include: { posts: true }
});
// user 的类型自动包含 posts,不需要手动标注
```
迁移体验也是最好的:`npx prisma migrate dev --name init` 自动生成迁移 SQL,`prisma migrate deploy` 在生产环境执行。
Prisma 的缺点:schema 用自己的 DSL(不是 TypeScript),复杂查询要写 raw SQL,N+1 问题用 include 解决但不如 DataLoader 灵活。
## TypeORM:装饰器风格的老牌
TypeORM 用装饰器定义实体,和 NestJS 风格统一:
```typescript
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@OneToMany(() => Post, post => post.author)
posts: Post[];
}
```
优势:和 NestJS 深度集成,装饰器风格统一。QueryBuilder 灵活,复杂查询不依赖 raw SQL。
劣势:类型安全弱——`findOne` 返回的是 `User | undefined`,关联字段需要手动标注类型。N+1 问题严重,`relations` 的加载策略容易踩坑。迁移工具粗糙,自动生成的迁移经常需要手动修改。
## Sequelize:该退休了
Sequelize 是最老的 Node.js ORM,v5 及以前用 callback 风格,v6 改成了 Promise 但 API 设计仍然笨重。类型安全最差(TypeScript 支持是后加的)。不建议新项目使用。
## MikroORM:小众精品
MikroORM 的设计理念更接近 Doctrine(PHP),强调 Unit of Work 模式——所有修改先在内存中记录,`flush()` 时一次性写入数据库。好处是自动处理关联关系,不需要手动 save 每个实体。
```typescript
const user = em.create(User, { email: 'test@test.com' });
await em.flush(); // 一次性写入
```
类型安全比 TypeORM 好很多,自动推导关联类型。但社区小,遇到问题不好查。适合对 ORM 设计有洁癖的开发者。
## 怎么选
- **新项目**:Prisma(类型安全 + 迁移体验),或 MikroORM(如果你喜欢 Unit of Work)
- **NestJS 项目**:TypeORM 仍是官方默认,但 Prisma 在 NestJS 社区的采用率快速上升
- **已有 TypeORM 项目**:不必迁移。TypeORM 能正常工作,迁移成本 > 收益
- **不要选 Sequelize**:除非你在维护老项目服务端6月2日 01:46
TypeORM 查询太慢怎么优化?N+1、索引和批量插入实战TypeORM 性能问题主要来自三个地方:N+1 查询、缺少索引、大量数据操作用了逐条插入。逐一解决后,大部分应用的数据库性能就够了。
## 1. 解决 N+1 查询
最常见也最致命的性能问题。查询用户列表后逐个查用户的文章:
```typescript
// N+1:1 次查用户 + N 次查文章
const users = await userRepo.find();
for (const user of users) {
user.posts = await postRepo.find({ where: { authorId: user.id } });
}
```
100 个用户 = 101 条 SQL。解决方案:用 `relations` 或 `join` 一次性查出。
```typescript
// 方案一:relations(发 2 条 SQL,但比 N+1 好很多)
const users = await userRepo.find({ relations: ['posts'] });
// 方案二:QueryBuilder join(1 条 SQL)
const users = await userRepo
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.getMany();
```
`leftJoinAndSelect` 同时查出关联数据。`leftJoin` 只 join 不 select——用于过滤条件但不返回关联数据。
## 2. 只查需要的字段
`find()` 默认 SELECT 所有列。大表里有 TEXT/BLOB 列时,查全列浪费网络带宽和内存:
```typescript
// 查全列
const users = await userRepo.find();
// 只查需要的列
const users = await userRepo.find({
select: ['id', 'name', 'email']
});
// QueryBuilder 方式
const users = await userRepo
.createQueryBuilder('user')
.select(['user.id', 'user.name', 'user.email'])
.getMany();
```
列表页只需要 id 和 name,详情页才需要所有字段。按场景选择查询范围。
## 3. 分页不要用 skip/take 处理大偏移
```typescript
// 大偏移时很慢:MySQL 要扫描前 10000 行然后丢掉
const posts = await postRepo.find({
skip: 10000,
take: 20
});
```
改用游标分页(cursor-based pagination):
```typescript
// 基于 ID 的游标分页
const posts = await postRepo.find({
where: { id: LessThan(lastId) },
order: { id: 'DESC' },
take: 20
});
```
游标分页不依赖偏移量,性能与页码无关。缺点是不能跳转到指定页码。
## 4. 批量插入
逐条插入 1000 条数据 = 1000 条 INSERT 语句,极慢:
```typescript
// 慢:逐条插入
for (const item of data) {
await repo.save(item);
}
```
```typescript
// 快:批量插入
await repo.save(data); // TypeORM 自动合并成批量 INSERT
```
`save` 传入数组时,TypeORM 会合并成一条 INSERT 语句(部分数据库支持)。更大数据量用 `createQueryBuilder` 的 INSERT:
```typescript
await createQueryBuilder()
.insert()
.into(User)
.values(data)
.execute();
```
极大数据量(10 万+)分批插入,每批 1000 条,避免单条 SQL 过长。
## 5. 索引优化
TypeORM 的 `@Index` 装饰器声明索引:
```typescript
@Entity()
@Index(['email']) // 单列索引
@Index(['firstName', 'lastName']) // 复合索引
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
@Column()
firstName: string;
@Column()
lastName: string;
}
```
索引设计原则:
- WHERE 条件里的列加索引
- 复合索引把区分度高的列放前面(`@Index(['status', 'createdAt'])` 如果 status 只有 3 个值就不如 `@Index(['createdAt', 'status'])`)
- 不要过度索引——每个索引增加写入开销
查看慢查询:`EXPLAIN ANALYZE` + SQL 日志。TypeORM 开启 SQL 日志:
```typescript
TypeOrmModule.forRoot({
logging: true, // 打印所有 SQL
})
```
## 6. 连接池配置
默认连接池大小可能不够用:
```typescript
TypeOrmModule.forRoot({
type: 'postgres',
extra: {
max: 20, // 最大连接数
idleTimeoutMillis: 30000,
},
})
```
`max` 设为 CPU 核心数 * 2 + 磁盘数是常见公式。太多连接反而增加数据库锁争用。服务端2月18日 22:20
TypeORM 的事件系统如何工作?包括实体监听器和订阅者TypeORM 的事件系统允许开发者在实体操作的生命周期中执行自定义逻辑,提供了强大的扩展能力。
## 事件类型
### 1. 实体生命周期事件
TypeORM 提供了以下实体生命周期事件:
* `BeforeInsert` - 在实体插入之前触发
* `AfterInsert` - 在实体插入之后触发
* `BeforeUpdate` - 在实体更新之前触发
* `AfterUpdate` - 在实体更新之后触发
* `BeforeRemove` - 在实体删除之前触发
* `AfterRemove` - 在实体删除之后触发
* `BeforeSoftRemove` - 在实体软删除之前触发
* `AfterSoftRemove` - 在实体软删除之后触发
* `BeforeRecover` - 在实体恢复之前触发
* `AfterRecover` - 在实体恢复之后触发
### 2. 订阅者事件
订阅者可以监听所有实体的特定事件。
## 使用实体监听器
### 基本用法
```typescript
import { Entity, PrimaryGeneratedColumn, Column, BeforeInsert, BeforeUpdate, AfterInsert, AfterUpdate } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
email: string;
@Column({ type: 'timestamp' })
createdAt: Date;
@Column({ type: 'timestamp' })
updatedAt: Date;
@Column({ default: 0 })
version: number;
@BeforeInsert()
beforeInsert() {
this.createdAt = new Date();
this.updatedAt = new Date();
this.version = 1;
}
@BeforeUpdate()
beforeUpdate() {
this.updatedAt = new Date();
this.version++;
}
@AfterInsert()
afterInsert() {
console.log(`User ${this.name} inserted with ID ${this.id}`);
}
@AfterUpdate()
afterUpdate() {
console.log(`User ${this.name} updated to version ${this.version}`);
}
}
```
### 复杂逻辑处理
```typescript
import { Entity, PrimaryGeneratedColumn, Column, BeforeInsert, BeforeUpdate } from 'typeorm';
import { hash } from 'bcrypt';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
email: string;
@Column()
password: string;
@Column({ default: false })
emailVerified: boolean;
@Column({ type: 'timestamp', nullable: true })
emailVerifiedAt: Date;
@Column({ type: 'timestamp' })
createdAt: Date;
@Column({ type: 'timestamp' })
updatedAt: Date;
@BeforeInsert()
async beforeInsert() {
this.createdAt = new Date();
this.updatedAt = new Date();
// 加密密码
if (this.password) {
this.password = await hash(this.password, 10);
}
// 验证邮箱格式
if (!this.validateEmail(this.email)) {
throw new Error('Invalid email format');
}
}
@BeforeUpdate()
async beforeUpdate() {
this.updatedAt = new Date();
// 如果密码被修改,重新加密
if (this.password && this.isPasswordModified()) {
this.password = await hash(this.password, 10);
}
// 如果邮箱被验证,记录验证时间
if (this.emailVerified && !this.emailVerifiedAt) {
this.emailVerifiedAt = new Date();
}
}
private validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
private isPasswordModified(): boolean {
// 实现密码修改检测逻辑
return true;
}
}
```
## 使用订阅者
### 基本订阅者
```typescript
import { EntitySubscriberInterface, EventSubscriber, InsertEvent, UpdateEvent, RemoveEvent } from 'typeorm';
import { User } from '../entity/User';
@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
// 指定监听的实体
listenTo() {
return User;
}
// 插入前
beforeInsert(event: InsertEvent<User>) {
console.log(`Before inserting user: ${event.entity.name}`);
// 可以修改实体
event.entity.createdAt = new Date();
}
// 插入后
afterInsert(event: InsertEvent<User>) {
console.log(`After inserting user with ID: ${event.entity.id}`);
// 发送欢迎邮件
this.sendWelcomeEmail(event.entity);
}
// 更新前
beforeUpdate(event: UpdateEvent<User>) {
console.log(`Before updating user: ${event.entity.name}`);
// 记录变更
this.logChanges(event);
}
// 更新后
afterUpdate(event: UpdateEvent<User>) {
console.log(`After updating user: ${event.entity.name}`);
// 发送通知
this.sendUpdateNotification(event.entity);
}
// 删除前
beforeRemove(event: RemoveEvent<User>) {
console.log(`Before removing user: ${event.entity.name}`);
// 检查是否可以删除
if (event.entity.posts && event.entity.posts.length > 0) {
throw new Error('Cannot delete user with posts');
}
}
// 删除后
afterRemove(event: RemoveEvent<User>) {
console.log(`After removing user: ${event.entity.name}`);
// 清理相关数据
this.cleanupUserData(event.entity.id);
}
private sendWelcomeEmail(user: User) {
// 发送欢迎邮件逻辑
console.log(`Sending welcome email to ${user.email}`);
}
private sendUpdateNotification(user: User) {
// 发送更新通知逻辑
console.log(`Sending update notification to ${user.email}`);
}
private logChanges(event: UpdateEvent<User>) {
// 记录变更逻辑
console.log('Changes:', event.updatedColumns);
}
private cleanupUserData(userId: number) {
// 清理用户数据逻辑
console.log(`Cleaning up data for user ${userId}`);
}
}
```
### 全局订阅者
```typescript
import { EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm';
@EventSubscriber()
export class AuditSubscriber implements EntitySubscriberInterface {
// 监听所有实体
listenTo() {
return Object;
}
// 所有实体的插入操作
afterInsert(event: InsertEvent<any>) {
console.log(`Entity ${event.metadata.name} inserted with ID ${event.entity.id}`);
// 记录审计日志
this.logAudit({
action: 'INSERT',
entity: event.metadata.name,
entityId: event.entity.id,
timestamp: new Date(),
});
}
private logAudit(log: any) {
// 记录审计日志逻辑
console.log('Audit log:', log);
}
}
```
## 注册订阅者
### 在 DataSource 中注册
```typescript
import { DataSource } from 'typeorm';
import { UserSubscriber } from './subscriber/UserSubscriber';
import { AuditSubscriber } from './subscriber/AuditSubscriber';
const dataSource = new DataSource({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'myapp',
entities: [User, Post],
synchronize: false,
logging: true,
// 注册订阅者
subscribers: [UserSubscriber, AuditSubscriber],
});
```
### 动态注册订阅者
```typescript
import { DataSource } from 'typeorm';
const dataSource = new DataSource({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'myapp',
entities: [User, Post],
synchronize: false,
logging: true,
});
// 初始化后动态注册订阅者
dataSource.initialize().then(() => {
const userSubscriber = new UserSubscriber();
dataSource.subscribers.push(userSubscriber);
});
```
## 高级事件处理
### 事务中的事件
```typescript
@EventSubscriber()
export class TransactionSubscriber implements EntitySubscriberInterface<User> {
listenTo() {
return User;
}
afterInsert(event: InsertEvent<User>) {
// 检查是否在事务中
if (event.queryRunner?.isTransactionActive) {
console.log('Insert operation is part of a transaction');
}
// 使用事务执行器
if (event.queryRunner) {
event.queryRunner.manager.getRepository(AuditLog).save({
action: 'USER_INSERT',
userId: event.entity.id,
timestamp: new Date(),
});
}
}
}
```
### 异步事件处理
```typescript
@EventSubscriber()
export class AsyncSubscriber implements EntitySubscriberInterface<User> {
listenTo() {
return User;
}
async afterInsert(event: InsertEvent<User>) {
// 异步发送邮件
await this.sendEmailAsync(event.entity);
// 异步生成用户资料
await this.generateUserProfileAsync(event.entity);
}
private async sendEmailAsync(user: User) {
// 模拟异步邮件发送
return new Promise((resolve) => {
setTimeout(() => {
console.log(`Email sent to ${user.email}`);
resolve(null);
}, 1000);
});
}
private async generateUserProfileAsync(user: User) {
// 模拟异步用户资料生成
return new Promise((resolve) => {
setTimeout(() => {
console.log(`Profile generated for user ${user.id}`);
resolve(null);
}, 500);
});
}
}
```
### 条件事件处理
```typescript
@EventSubscriber()
export class ConditionalSubscriber implements EntitySubscriberInterface<User> {
listenTo() {
return User;
}
beforeUpdate(event: UpdateEvent<User>) {
// 只在特定条件下执行
if (this.shouldProcessUpdate(event)) {
this.processUpdate(event);
}
}
private shouldProcessUpdate(event: UpdateEvent<User>): boolean {
// 检查是否更新了特定字段
const updatedFields = event.updatedColumns.map(col => col.propertyName);
return updatedFields.includes('email') || updatedFields.includes('password');
}
private processUpdate(event: UpdateEvent<User>) {
// 处理更新逻辑
console.log('Processing critical update:', event.entity);
}
}
```
## 事件最佳实践
### 1. 保持事件处理简单
```typescript
// ✅ 好的做法:事件处理简单直接
@EventSubscriber()
export class SimpleSubscriber implements EntitySubscriberInterface<User> {
listenTo() {
return User;
}
afterInsert(event: InsertEvent<User>) {
// 简单的日志记录
console.log(`User created: ${event.entity.name}`);
}
}
// ❌ 不好的做法:事件处理过于复杂
@EventSubscriber()
export class ComplexSubscriber implements EntitySubscriberInterface<User> {
listenTo() {
return User;
}
async afterInsert(event: InsertEvent<User>) {
// 复杂的业务逻辑
const user = event.entity;
// 发送邮件
await this.sendEmail(user);
// 创建用户资料
await this.createProfile(user);
// 初始化用户设置
await this.initializeSettings(user);
// 发送欢迎消息
await this.sendWelcomeMessage(user);
// 记录统计
await this.recordStatistics(user);
// 更新缓存
await this.updateCache(user);
// 触发其他事件
await this.triggerEvents(user);
}
}
```
### 2. 避免循环事件
```typescript
// ✅ 好的做法:避免循环事件
@EventSubscriber()
export class SafeSubscriber implements EntitySubscriberInterface<User> {
listenTo() {
return User;
}
async afterInsert(event: InsertEvent<User>) {
// 使用标志位避免循环
if (event.entity.processed) {
return;
}
// 处理逻辑
await this.processUser(event.entity);
// 标记为已处理
event.entity.processed = true;
}
}
// ❌ 不好的做法:可能导致循环事件
@EventSubscriber()
export class CircularSubscriber implements EntitySubscriberInterface<User> {
listenTo() {
return User;
}
async afterInsert(event: InsertEvent<User>) {
// 更新用户,可能触发 afterUpdate 事件
await event.manager.save(User, {
id: event.entity.id,
processed: true,
});
}
}
```
### 3. 错误处理
```typescript
// ✅ 好的做法:适当的错误处理
@EventSubscriber()
export class ErrorHandlingSubscriber implements EntitySubscriberInterface<User> {
listenTo() {
return User;
}
async afterInsert(event: InsertEvent<User>) {
try {
await this.sendWelcomeEmail(event.entity);
} catch (error) {
console.error('Failed to send welcome email:', error);
// 记录错误,但不影响主流程
await this.logError(error, event.entity);
}
}
private async logError(error: any, user: User) {
// 记录错误到数据库
await event.manager.getRepository(ErrorLog).save({
error: error.message,
userId: user.id,
timestamp: new Date(),
});
}
}
```
### 4. 性能考虑
```typescript
// ✅ 好的做法:批量处理
@EventSubscriber()
export class BatchSubscriber implements EntitySubscriberInterface<User> {
private batch: User[] = [];
private timer: NodeJS.Timeout | null = null;
listenTo() {
return User;
}
afterInsert(event: InsertEvent<User>) {
// 添加到批次
this.batch.push(event.entity);
// 设置定时器
if (!this.timer) {
this.timer = setTimeout(() => {
this.processBatch();
}, 1000); // 1 秒后处理
}
}
private async processBatch() {
if (this.batch.length === 0) {
return;
}
const usersToProcess = [...this.batch];
this.batch = [];
this.timer = null;
// 批量处理
await this.sendBatchNotifications(usersToProcess);
}
private async sendBatchNotifications(users: User[]) {
console.log(`Sending notifications to ${users.length} users`);
// 批量发送通知逻辑
}
}
```
## 实际应用场景
### 1. 审计日志
```typescript
@EventSubscriber()
export class AuditLogSubscriber implements EntitySubscriberInterface {
listenTo() {
return Object;
}
afterInsert(event: InsertEvent<any>) {
this.logAudit('INSERT', event.entity);
}
afterUpdate(event: UpdateEvent<any>) {
this.logAudit('UPDATE', event.entity, event.updatedColumns);
}
afterRemove(event: RemoveEvent<any>) {
this.logAudit('DELETE', event.entity);
}
private async logAudit(action: string, entity: any, columns?: any[]) {
const auditLog = {
action,
entityName: entity.constructor.name,
entityId: entity.id,
changes: columns ? columns.map(col => col.propertyName) : null,
timestamp: new Date(),
};
await event.manager.getRepository(AuditLog).save(auditLog);
}
}
```
### 2. 缓存失效
```typescript
@EventSubscriber()
export class CacheInvalidationSubscriber implements EntitySubscriberInterface<User> {
listenTo() {
return User;
}
afterUpdate(event: UpdateEvent<User>) {
// 清除用户缓存
this.clearUserCache(event.entity.id);
// 清除相关缓存
this.clearRelatedCache(event.entity.id);
}
afterRemove(event: RemoveEvent<User>) {
// 清除所有相关缓存
this.clearAllUserCache(event.entity.id);
}
private clearUserCache(userId: number) {
// 清除用户缓存逻辑
console.log(`Clearing cache for user ${userId}`);
}
private clearRelatedCache(userId: number) {
// 清除相关缓存逻辑
console.log(`Clearing related cache for user ${userId}`);
}
private clearAllUserCache(userId: number) {
// 清除所有用户缓存逻辑
console.log(`Clearing all cache for user ${userId}`);
}
}
```
### 3. 通知系统
```typescript
@EventSubscriber()
export class NotificationSubscriber implements EntitySubscriberInterface<Post> {
listenTo() {
return Post;
}
afterInsert(event: InsertEvent<Post>) {
// 通知关注者
this.notifyFollowers(event.entity);
// 通知作者
this.notifyAuthor(event.entity);
}
afterUpdate(event: UpdateEvent<Post>) {
// 如果文章被发布,通知关注者
if (this.isPublished(event)) {
this.notifyFollowers(event.entity);
}
}
private isPublished(event: UpdateEvent<Post>): boolean {
const updatedFields = event.updatedColumns.map(col => col.propertyName);
return updatedFields.includes('status') && event.entity.status === 'published';
}
private async notifyFollowers(post: Post) {
// 通知关注者逻辑
console.log(`Notifying followers of post ${post.id}`);
}
private async notifyAuthor(post: Post) {
// 通知作者逻辑
console.log(`Notifying author of post ${post.id}`);
}
}
```
TypeORM 的事件系统提供了强大的扩展能力,合理使用事件可以简化业务逻辑,提高代码的可维护性。
服务端2月18日 22:20
TypeORM 中如何定义和使用关系映射?包括一对一、一对多、多对多关系的详细配置TypeORM 提供了四种主要的关系映射类型,每种关系都有其特定的使用场景和配置方式。理解这些关系映射对于构建复杂的数据模型至关重要。
## 四种关系类型
### 1. One-to-One (一对一)
一对一关系表示两个实体之间存在唯一的对应关系。例如,一个用户只能有一个个人资料。
```typescript
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column()
gender: string;
@Column()
bio: string;
@OneToOne(() => User, user => user.profile)
@JoinColumn()
user: User;
}
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToOne(() => Profile, profile => profile.user, { cascade: true })
profile: Profile;
}
```
**关键点**:
* 使用 `@OneToOne()` 装饰器定义关系
* 在拥有方使用 `@JoinColumn()` 指定外键列
* `cascade: true` 允许级联操作(保存、删除等)
### 2. One-to-Many / Many-to-One (一对多/多对一)
一对多关系是最常见的关系类型。例如,一个用户可以发表多篇文章。
```typescript
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
content: string;
@ManyToOne(() => User, user => user.posts)
author: User;
}
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToMany(() => Post, post => post.author)
posts: Post[];
}
```
**关键点**:
* `@ManyToOne()` 放在"多"的一方,包含外键
* `@OneToMany()` 放在"一"的一方,不需要 `@JoinColumn()`
* 外键自动添加到"多"的一方表中
### 3. Many-to-Many (多对多)
多对多关系需要中间表来连接两个实体。例如,一篇文章可以有多个标签,一个标签也可以属于多篇文章。
```typescript
@Entity()
export class Tag {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Post, post => post.tags)
posts: Post[];
}
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@ManyToMany(() => Tag, tag => tag.posts, { cascade: true })
@JoinTable()
tags: Tag[];
}
```
**关键点**:
* 使用 `@JoinTable()` 在关系的一方定义中间表
* 中间表自动创建,包含两个外键
* 可以自定义中间表名称和列名
## 关系配置选项
### Eager 和 Lazy 加载
```typescript
@Entity()
export class User {
@OneToMany(() => Post, post => post.author, {
eager: true // 立即加载关联数据
})
posts: Post[];
}
// 或者使用懒加载
@Entity()
export class User {
@OneToMany(() => Post, post => post.author)
posts: Promise<Post[]>;
}
```
### 级联操作 (Cascade)
```typescript
@OneToMany(() => Post, post => post.author, {
cascade: ['insert', 'update', 'remove', 'soft-remove', 'recover']
})
posts: Post[];
```
级联操作选项:
* `insert`: 保存父实体时自动保存子实体
* `update`: 更新父实体时自动更新子实体
* `remove`: 删除父实体时自动删除子实体
* `soft-remove`: 软删除
* `recover`: 恢复软删除的实体
### OnDelete 和 OnUpdate
```typescript
@ManyToOne(() => User, user => user.posts, {
onDelete: 'CASCADE', // 删除用户时级联删除文章
onUpdate: 'CASCADE' // 更新用户ID时级联更新文章
})
author: User;
```
## 关系查询
### 使用 FindOptions 查询关联数据
```typescript
const userRepository = dataSource.getRepository(User);
// 加载关联数据
const users = await userRepository.find({
relations: ['posts', 'profile']
});
// 条件查询关联数据
const usersWithPosts = await userRepository.find({
relations: {
posts: true
},
where: {
posts: {
title: Like('%TypeORM%')
}
}
});
```
### 使用 QueryBuilder
```typescript
const users = await dataSource
.getRepository(User)
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.leftJoinAndSelect('user.profile', 'profile')
.where('post.title = :title', { title: 'TypeORM Guide' })
.getMany();
```
## 自定义关系
### 自定义 JoinTable
```typescript
@ManyToMany(() => Tag, tag => tag.posts)
@JoinTable({
name: 'post_tags',
joinColumn: {
name: 'postId',
referencedColumnName: 'id'
},
inverseJoinColumn: {
name: 'tagId',
referencedColumnName: 'id'
}
})
tags: Tag[];
```
### 自定义 JoinColumn
```typescript
@ManyToOne(() => User, user => user.posts)
@JoinColumn({
name: 'author_id',
referencedColumnName: 'id'
})
author: User;
```
## 最佳实践
1. **合理选择关系类型**: 根据业务需求选择最合适的关系类型
2. **避免过度使用 Eager 加载**: 可能导致 N+1 查询问题
3. **谨慎使用级联删除**: 确保不会意外删除重要数据
4. **使用索引优化查询**: 为外键列添加索引
5. **考虑性能影响**: 复杂关系查询可能影响性能
TypeORM 的关系映射系统提供了强大而灵活的方式来处理实体之间的关系,掌握这些概念对于构建高效、可维护的应用程序至关重要。
服务端2月18日 22:19
TypeORM 的 QueryBuilder 如何使用?包括复杂查询、关联查询、分页排序等高级功能QueryBuilder 是 TypeORM 中最强大、最灵活的查询工具,它允许开发者构建复杂的 SQL 查询,同时保持类型安全和可读性。
## QueryBuilder 基础用法
### 创建 QueryBuilder
```typescript
import { DataSource } from 'typeorm';
const dataSource = new DataSource(/* 配置 */);
// 方式1: 通过 Repository 创建
const userRepository = dataSource.getRepository(User);
const queryBuilder = userRepository.createQueryBuilder('user');
// 方式2: 通过 DataSource 创建
const queryBuilder = dataSource.createQueryBuilder(User, 'user');
```
### 基本查询操作
```typescript
// 查询所有用户
const users = await dataSource
.createQueryBuilder('user')
.getMany();
// 查询单个用户
const user = await dataSource
.createQueryBuilder('user')
.where('user.id = :id', { id: 1 })
.getOne();
// 查询并计数
const count = await dataSource
.createQueryBuilder('user')
.getCount();
```
## 条件查询
### Where 子句
```typescript
// 简单条件
const users = await dataSource
.createQueryBuilder('user')
.where('user.age > :age', { age: 18 })
.getMany();
// 多个条件 (AND)
const users = await dataSource
.createQueryBuilder('user')
.where('user.age > :age', { age: 18 })
.andWhere('user.isActive = :isActive', { isActive: true })
.getMany();
// OR 条件
const users = await dataSource
.createQueryBuilder('user')
.where('user.age > :age', { age: 18 })
.orWhere('user.role = :role', { role: 'admin' })
.getMany();
// 复杂条件组合
const users = await dataSource
.createQueryBuilder('user')
.where(
new Brackets(qb => {
qb.where('user.age > :age', { age: 18 })
.orWhere('user.role = :role', { role: 'admin' });
})
)
.andWhere('user.isActive = :isActive', { isActive: true })
.getMany();
```
### 操作符
```typescript
import { Like, Between, In, MoreThan, LessThan } from 'typeorm';
// LIKE 查询
const users = await dataSource
.createQueryBuilder('user')
.where('user.name LIKE :name', { name: '%John%' })
.getMany();
// 或者使用 Like 操作符
const users = await dataSource
.createQueryBuilder('user')
.where('user.name = :name', { name: Like('%John%') })
.getMany();
// BETWEEN 查询
const users = await dataSource
.createQueryBuilder('user')
.where('user.age = :age', { age: Between(18, 30) })
.getMany();
// IN 查询
const users = await dataSource
.createQueryBuilder('user')
.where('user.id IN :ids', { ids: [1, 2, 3] })
.getMany();
// 或者使用 In 操作符
const users = await dataSource
.createQueryBuilder('user')
.where('user.id = :ids', { ids: In([1, 2, 3]) })
.getMany();
// 比较操作符
const users = await dataSource
.createQueryBuilder('user')
.where('user.age = :age', { age: MoreThan(18) })
.andWhere('user.score = :score', { score: LessThan(100) })
.getMany();
```
## 关联查询
### Left Join 和 Inner Join
```typescript
// Left Join (包含没有关联数据的记录)
const users = await dataSource
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.where('user.id = :id', { id: 1 })
.getMany();
// Inner Join (只包含有关联数据的记录)
const users = await dataSource
.createQueryBuilder('user')
.innerJoinAndSelect('user.posts', 'post')
.where('user.id = :id', { id: 1 })
.getMany();
// 多层关联
const users = await dataSource
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.leftJoinAndSelect('post.comments', 'comment')
.leftJoinAndSelect('comment.author', 'commentAuthor')
.getMany();
```
### Join 条件
```typescript
const users = await dataSource
.createQueryBuilder('user')
.leftJoin('user.posts', 'post', 'post.status = :status', { status: 'published' })
.addSelect(['post.title', 'post.createdAt'])
.getMany();
```
## 排序和分页
### 排序
```typescript
// 单字段排序
const users = await dataSource
.createQueryBuilder('user')
.orderBy('user.createdAt', 'DESC')
.getMany();
// 多字段排序
const users = await dataSource
.createQueryBuilder('user')
.orderBy('user.createdAt', 'DESC')
.addOrderBy('user.name', 'ASC')
.getMany();
// 随机排序 (MySQL)
const users = await dataSource
.createQueryBuilder('user')
.orderBy('RAND()')
.getMany();
```
### 分页
```typescript
// 基本分页
const page = 1;
const pageSize = 10;
const users = await dataSource
.createQueryBuilder('user')
.skip((page - 1) * pageSize)
.take(pageSize)
.getMany();
// 获取总数和分页数据
const [users, total] = await dataSource
.createQueryBuilder('user')
.skip((page - 1) * pageSize)
.take(pageSize)
.getManyAndCount();
console.log(`Total: ${total}, Page: ${page}, PageSize: ${pageSize}`);
```
## 聚合查询
### Group By 和 Having
```typescript
// 按角色分组统计用户数
const result = await dataSource
.createQueryBuilder('user')
.select('user.role', 'role')
.addSelect('COUNT(*)', 'count')
.groupBy('user.role')
.getRawMany();
// 使用 Having 过滤分组
const result = await dataSource
.createQueryBuilder('user')
.select('user.role', 'role')
.addSelect('COUNT(*)', 'count')
.groupBy('user.role')
.having('COUNT(*) > :minCount', { minCount: 5 })
.getRawMany();
```
### 聚合函数
```typescript
// 统计总数
const count = await dataSource
.createQueryBuilder('user')
.select('COUNT(*)', 'count')
.getRawOne();
// 计算平均值
const avgAge = await dataSource
.createQueryBuilder('user')
.select('AVG(user.age)', 'avgAge')
.getRawOne();
// 求和
const totalScore = await dataSource
.createQueryBuilder('user')
.select('SUM(user.score)', 'totalScore')
.getRawOne();
// 最大值和最小值
const result = await dataSource
.createQueryBuilder('user')
.select('MAX(user.age)', 'maxAge')
.addSelect('MIN(user.age)', 'minAge')
.getRawOne();
```
## 子查询
### 使用 SubQueryFactory
```typescript
import { SubQueryFactory } from 'typeorm';
const users = await dataSource
.createQueryBuilder('user')
.where((qb: SelectQueryBuilder<User>) => {
const subQuery = qb
.subQuery()
.select('post.userId')
.from(Post, 'post')
.where('post.title LIKE :title', { title: '%TypeORM%' })
.getQuery();
return 'user.id IN ' + subQuery;
})
.setParameter('title', '%TypeORM%')
.getMany();
```
### 使用 EXISTS
```typescript
const users = await dataSource
.createQueryBuilder('user')
.where((qb: SelectQueryBuilder<User>) => {
const subQuery = qb
.subQuery()
.select('1')
.from(Post, 'post')
.where('post.userId = user.id')
.getQuery();
return 'EXISTS ' + subQuery;
})
.getMany();
```
## 更新和删除
### 更新操作
```typescript
// 简单更新
await dataSource
.createQueryBuilder(User, 'user')
.update()
.set({ name: 'Updated Name' })
.where('id = :id', { id: 1 })
.execute();
// 条件更新
await dataSource
.createQueryBuilder(User, 'user')
.update()
.set({ isActive: false })
.where('user.lastLoginAt < :date', { date: new Date('2024-01-01') })
.execute();
// 基于子查询的更新
await dataSource
.createQueryBuilder(User, 'user')
.update()
.set({ score: () => 'score + 10' })
.where('user.id IN :ids', { ids: [1, 2, 3] })
.execute();
```
### 删除操作
```typescript
// 简单删除
await dataSource
.createQueryBuilder(User, 'user')
.delete()
.where('id = :id', { id: 1 })
.execute();
// 条件删除
await dataSource
.createQueryBuilder(User, 'user')
.delete()
.where('user.createdAt < :date', { date: new Date('2023-01-01') })
.andWhere('user.isActive = :isActive', { isActive: false })
.execute();
```
## 高级特性
### 原生 SQL
```typescript
const users = await dataSource
.createQueryBuilder(User, 'user')
.where('user.id = :id', { id: 1 })
.andWhere('JSON_CONTAINS(user.preferences, :preferences)', {
preferences: JSON.stringify({ theme: 'dark' })
})
.getMany();
```
### 缓存
```typescript
const users = await dataSource
.createQueryBuilder('user')
.where('user.isActive = :isActive', { isActive: true })
.cache(60000) // 缓存 60 秒
.getMany();
```
### 事务
```typescript
await dataSource.transaction(async transactionalEntityManager => {
const queryRunner = transactionalEntityManager.queryRunner;
await queryRunner.manager
.createQueryBuilder(User, 'user')
.insert()
.values({ name: 'John', email: 'john@example.com' })
.execute();
await queryRunner.manager
.createQueryBuilder(Post, 'post')
.insert()
.values({ title: 'New Post', authorId: 1 })
.execute();
});
```
## 性能优化建议
1. **避免 N+1 查询**: 使用 `leftJoinAndSelect` 一次性加载关联数据
2. **只选择需要的字段**: 使用 `select()` 明确指定需要的列
3. **合理使用索引**: 为常用查询条件添加数据库索引
4. **使用缓存**: 对不常变化的数据启用查询缓存
5. **限制返回结果**: 使用 `take()` 和 `skip()` 实现分页
6. **监控查询性能**: 使用 `getQuery()` 和 `getSql()` 查看生成的 SQL
QueryBuilder 是 TypeORM 中最强大的查询工具,掌握它的使用可以让你构建出高效、灵活的数据库查询。
服务端2月18日 22:19
TypeORM 的核心概念是什么?包括 Entity、Repository、DataSource 等主要组件的详细说明TypeORM 是一个基于 TypeScript 和 JavaScript 的 ORM 框架,它采用 Active Record 和 Data Mapper 两种设计模式,让开发者能够使用面向对象的方式来操作关系型数据库。
## 核心概念
### 1. Entity (实体)
Entity 是 TypeORM 的核心概念,它对应数据库中的表。每个 Entity 类都使用 `@Entity()` 装饰器标记,并映射到数据库表。
```typescript
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
```
### 2. Column (列)
Column 装饰器用于定义实体属性如何映射到数据库列:
* `@Column()`: 基本列定义
* `@PrimaryGeneratedColumn()`: 自增主键
* `@CreateDateColumn()`: 自动创建时间
* `@UpdateDateColumn()`: 自动更新时间
* `@Generated()`: 自动生成值
### 3. Repository (仓储)
Repository 是用于操作实体的主要接口,提供了 CRUD 操作:
```typescript
const userRepository = dataSource.getRepository(User);
// 创建
const user = userRepository.create({ name: 'John', email: 'john@example.com' });
await userRepository.save(user);
// 查询
const users = await userRepository.find();
const user = await userRepository.findOne({ where: { id: 1 } });
// 更新
await userRepository.update(1, { name: 'John Updated' });
// 删除
await userRepository.delete(1);
```
### 4. DataSource (数据源)
DataSource 是数据库连接的配置和管理中心:
```typescript
const dataSource = new DataSource({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'test',
entities: [User],
synchronize: true,
});
```
### 5. Relation (关系)
TypeORM 支持四种主要的关系类型:
* **One-to-One (一对一)**: 使用 `@OneToOne()` 装饰器
* **One-to-Many (一对多)**: 使用 `@OneToMany()` 装饰器
* **Many-to-One (多对一)**: 使用 `@ManyToOne()` 装饰器
* **Many-to-Many (多对多)**: 使用 `@ManyToMany()` 装饰器
```typescript
@Entity()
export class Profile {
@OneToOne(() => User, user => user.profile)
user: User;
}
@Entity()
export class User {
@OneToOne(() => Profile, profile => profile.user)
profile: Profile;
@OneToMany(() => Post, post => post.author)
posts: Post[];
}
```
## 设计模式
### Active Record 模式
在 Active Record 模式中,实体本身包含业务逻辑和数据访问方法:
```typescript
@Entity()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
static async findByName(name: string) {
return this.find({ where: { name } });
}
}
// 使用
const users = await User.findByName('John');
```
### Data Mapper 模式
在 Data Mapper 模式中,数据访问逻辑与实体分离,通过 Repository 操作:
```typescript
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
// 使用 Repository
const userRepository = dataSource.getRepository(User);
const users = await userRepository.find({ where: { name: 'John' } });
```
## 关键特性
1. **类型安全**: 完全支持 TypeScript,提供编译时类型检查
2. **装饰器驱动**: 使用装饰器简化配置
3. **迁移系统**: 支持数据库迁移和版本控制
4. **查询构建器**: 提供灵活的查询构建 API
5. **事务支持**: 完整的事务管理
6. **缓存支持**: 内置查询缓存机制
7. **多数据库支持**: MySQL, PostgreSQL, SQLite, SQL Server, Oracle 等
TypeORM 的这些核心概念使其成为 Node.js 生态中最流行的 ORM 框架之一,特别适合需要类型安全和面向对象编程风格的项目。