TypeORM 是一个成熟的 Node.js 对象关系映射(ORM)框架,支持 TypeScript。它允许开发者通过 类和装饰器 定义数据库模型,支持 Data Mapper 和 Active Record 模式。其核心优势在于强大的类型检查、跨数据库支持(MySQL/PostgreSQL等)以及灵活的 QueryBuilder。
你想了解如何快速在 NestJS 项目中配置 TypeORM 吗?
一、查询类方法——详解 + 示例
1、 find(options?)
✅作用
- 查询列表数据
- 后台分页
- 简单条件筛选
- 不涉及复杂联表 / 子查询
🔧 options 结构-参数
import { EntityRepository, Repository, In, Like, Between } from 'typeorm';
import { User } from './entity/User';
// 假设我们正在一个 Repository 或 Manager 中调用 find
const users = await userRepository.find({
/**
* 1. select: 指定要查询的字段
* 默认会查询所有字段。如果指定,则只返回这些列。
*/
select: {
id: true,
firstName: true,
lastName: true,
role: { id: true,name:true } // 返回给前端role属性(实体)中id和name字段
},
/**
* 2. where: 查询过滤条件
* 可以是对象、对象数组(表示 OR),以及配合特定的查询操作符。
Not(value): 不等于
LessThan(value) / LessThanOrEqual(value): 小于 / 小于等于
MoreThan(value) / MoreThanOrEqual(value): 大于 / 大于等于
Like(value): 模糊查询 `%...%`
ILike(value): 忽略大小写的模糊查询(Postgres)
Between(from, to): 范围查询
IsNull(): 判断空值
*/
where: {
firstName: "Timber",
lastName: "Saw",
age: MoreThan(18), // 使用内置操作符
role: In(["admin", "editor"]), // 范围查询
// 数组形式表示 OR 逻辑: [{ firstName: "A" }, { firstName: "B" }]
},
/**
* 3. relations: 关联加载
* 指定要跟随主表一起查出来的“关联属性名”(即实体类中定义的变量名)。
* 底层会自动执行 LEFT JOIN 操作。
*/
relations: {
role: true, // 加载与该实体关联的 role 属性 即role对应的实体
photos: true, // 加载 photos 数组
videos: { // 进入 videos 实体内部
videoAttributes: true // 继续加载 videos 实体里定义的 videoAttributes 关联
}
}
// 数组字符串写法
// relations: ["role", "photos", ]
/**
* 4. order: 排序规则
* key 为字段名,value 为 "ASC" 或 "DESC"。
*/
order: {
firstName: "ASC",
id: "DESC",
},
/**
* 5. skip: 跳过的记录数 (Offset)
* 通常用于分页,配合 take 使用。
*/
skip: 0,
/**
* 6. take: 获取的记录数 (Limit)
* 指定返回多少条数据。
*/
take: 10,
/**
* 7. cache: 启用查询缓存
* 可以是布尔值,也可以是包含 id 和 milliseconds 的对象。
*/
cache: {
id: "users_list_cache",
milliseconds: 30000 // 缓存30秒
},
/**
* 8. join: 手动 Join 逻辑 (不常用)
* 比 relations 更灵活,可以定义 alias。
*/
join: {
alias: "user",
leftJoinAndSelect: {
profile: "user.profile",
photo: "user.photos"
}
},
/**
* 9. withDeleted: 包含软删除记录
* 如果实体开启了 @DeleteDateColumn,设为 true 会查出被软删除的数据。
*/
withDeleted: false,
/**
* 10. loadRelationIds: 仅加载关联 ID
* 设为 true 时,关联字段会返回 ID 而不是完整的对象。
*/
loadRelationIds: false,
/**
* 11. loadEagerRelations: 加载饿汉式关联
* 默认为 true。如果设为 false,即使实体定义了 eager: true 也不会加载。
*/
loadEagerRelations: true,
/**
* 12. transaction: 事务锁 (高级用法)
* 仅在特定的数据库系统和版本中支持。
*/
// lock: { mode: "optimistic", version: 1 }
});
💡 示例:用户查询
// service层代码
async list(name: string) {
return await this.userRepository.find({
relations:{
role:true // 加载角色信息
},
where: {
name:Like(`%${name}%`) // 模糊查询用户名
},
select: {
id: true,
username: true,
role: {
id: true, // 角色Id
name: true, // 只查出角色名称
code: true,
},
createTime: true,
updateTime: true,
age: true,
status: true,
},
order: { createTime: 'DESC' },
});
}
// 返回结果
[
{
"id": 27,
"username": "user",
"age": 18,
"status": 1,
"role": { // 只返回为true的字段
"id": 2,
"name": "普通用户",
"code": "user",
},
"createTime": "2026-01-28 15:39:02",
"updateTime": "2026-01-28 15:39:02"
},
]
2、 findOne(options)
✅ 特点
- 必须有
where - 返回实体对象,查不到返回
null(不会抛错) - 支持
relations / order / select - 不支持
take / offect
const post = await this.postRepository.findOne({
where: { id },
});
💡 示例:详情页
async detail(id: number) {
const post = await this.postRepository.findOne({ where: { id } });
if (!post) {
throw new NotFoundException('文章不存在');
}
return post;
}
3、 findBy({})(简化版)
✅ 特点
- find查询的简化版不需要where
- 仅支持裸查询裸
{ id: 1,... } relations,select,order等均不支持- 不需要分页 / 排序 / 联表时推荐使用
🔍 本质
findBy === find({ where })
示例
this.postRepository.findBy({
status: 'draft',
});
4、 findOneBy({})(简化版)
✅ 特点
- findOne查询的简化版不需要where
- 仅支持裸查询裸
{ id: 1,... } relations,select,order等均不支持
🔍 本质
repo.findOne({where:{id:1}});
示例
this.postRepository.findOneBy({
id: '1',
});
5、 count(options?)
✅ 常见用途
- 分页总数
- 状态统计
const total = await this.postRepository.count({
where: { status: 'published' },
});
💡 分页实战(基础版)
async list(page: number, size: number) {
const [list, total] = await Promise.all([
this.postRepository.find({
take: size,
skip: (page - 1) * size,
}),
this.postRepository.count(),
]);
return { list, total };
}
6、 findAndCount(options?) ⭐⭐
分页的“黄金方法”
✅ 特点
- 一次调用
- 实际执行 两次 SQL 查数据 、查总数
const [items, total] = await repository.findAndCount(options);
💡 示例:
async getRecordPage(page = 1, pageSize = 10) {
const [data, total] = await this.recordRepo.findAndCount({
relations: ['user', 'device'],
where: { auditStatus: 1 },
order: { id: 'DESC' },
skip: (page - 1) * pageSize,
take: pageSize,
});
return {
list: data,
total,
pageSize,
currentPage: page,
};
}
🤔 为什么不用 find + count?
- 前端分页组件必须知道 total
- 两次查询容易条件不一致
findAndCount= 官方封装的最佳实践,保证了条件的一致性
二、持久化方法(Persistence)——重点
1、 create(entityLike) ⚠️(最容易被误解)
✅ 特点
- create 不会写数据库
- 根据实体类,映射出响应的新增数据
// dto:前端传今天的数据定义
const post = this.postRepository.create(dto);
📌 正确用法
create + save = 标准新增
❌ 永远不要只用 create,不会写入数据库中
2、 save(entity)(最推荐)
✅ 特点
- 可以是新增也可以是更新
- 底层通过 主键(Primary Key):一般是id 来决定执行
INSERT还是UPDATE的
💡 新增
// 使用create实例化
async create(dto: CreatePostDto, authorId: number) {
const post = this.postRepository.create({
...dto,
authorId,
});
return this.postRepository.save(post);
}
💡 修改(强烈推荐)
async update(id: number, dto: UpdatePostDto) {
const post = await this.postRepository.findOne({ where: { id } });
if (!post) throw new NotFoundException();
Object.assign(post, dto);
return this.postRepository.save(post);
}
📌 什么时候一定用 save?
- 有业务校验
- 需要实体钩子(BeforeUpdate)
- 可读性优先
3、 update(criteria, partialEntity)
✅ 特点
- 不校验是否存在
- 不触发钩子
- 不返回实体
// 将 ID 为 1 的用户年龄改为 25
await userRepository.update(1, { age: 25 });
// 将所有状态为 "pending" 的订单改为 "active"
await orderRepository.update({ status: 'pending' }, { status: 'active' });
update vs save 对比表
| 特性 | update() | save() |
|---|---|---|
| SQL 步骤 | 直接 UPDATE | 先 SELECT 再 INSERT/UPDATE |
| 性能 | 极高 | 相对较低(双倍数据库交互) |
| 局部更新 | 仅更新传入字段 | 如果传入不完整对象,可能覆盖其他字段 |
| 生命周期钩子 | 不触发 | 触发 |
| 级联关系 | 不支持 | 支持 |
| 返回值 | UpdateResult (受影响行数等) | 完整的实体对象 |
4、 insert(entityLike)
✅ 特点
- 直接 INSERT SQL
- 不触发钩子
- 不返回实体
await this.userRepository.insert(list);
📌 只在 100% 确定是新增时用(如批量导入)
/**
* 简单的批量导入示例
*/
async function simpleBatchImport() {
const usersToImport = [
{ username: '张三', age: 18, status: 'active' },
{ username: '李四', age: 20, status: 'active' },
{ username: '王五', age: 22, status: 'inactive' },
];
// 直接使用 repository.insert
// 注意:它接受一个对象,或者一个对象数组
const result = await userRepository.insert(usersToImport);
console.log('插入受影响的行数:', result.identifiers.length);
return result;
}
5、 remove(entity)
✅ 特点
- 需要先查实体
- 触发钩子
- 支持级联
// 必须先拿到实例对象
const user = await userRepository.findOneBy({ id: 1 });
if (user) {
await userRepository.remove(user); // 传入的是对象
}
6、 delete(criteria)
✅ 特点
- 不查数据库
- 不触发钩子
- 不支持级联
// 通过 ID 删除
await userRepository.delete(1);
// 通过条件删除
await userRepository.delete({ status: 'inactive' });
三、createQueryBuilder —— 真正拉开差距的地方
✅ 核心链式方法统计表
| 分类 | 方法名 | 用途简述 |
|---|---|---|
| 基础选择 | select(), addSelect() | 指定查询的字段 |
| 主体表 | from() | 指定查询的主表 |
| 关联查询 | leftJoin(), innerJoin(), leftJoinAndSelect() | 关联其他表(含数据加载) |
| 过滤条件 | where(), andWhere(), orWhere() | 设置查询过滤条件 |
| 排序分页 | orderBy(), skip(), take() | 排序与分页(推荐用于实体) |
| 分组聚合 | groupBy(), having() | 分组与聚合过滤 |
| 结果执行 | getOne(), getMany(), getRawOne(), getRawMany() | 执行查询并返回结果 |
select("alias.field"): 初始化要查询的字段。如果不调用,默认查询实体的所有字段。addSelect("alias.field"): 在现有查询字段基础上增加字段(常用于隐藏字段的临时开启)。from(Entity, "alias"): 定义查询的主表及别名。leftJoinAndSelect("user.profile", "profile"): 这是最常用的关联方法。它执行LEFT JOIN并自动将关联表的数据映射到主实体的属性中。leftJoin("user.profile", "profile"): 仅执行关联,但不加载数据。常用于需要根据关联表的字段进行WHERE过滤,但结果里不需要该表数据的场景。where("user.id = :id", { id: 1 }): 设置初始条件。注意:重复调用where()会覆盖之前的条件。andWhere()/orWhere(): 在已有条件基础上增加AND或OR逻辑。建议始终使用参数化占位符(:id)防止 SQL 注入take(n)与skip(m): 在处理relations时,必须使用take和skip而不是limit和offset。这是因为take/skip会正确处理一对多关系导致的重复行问题,而limit可能会截断 不完整的数据
💡 示例 1:复杂的连表与字段筛选
// 查询用户及其角色,要求:用户必须是激活状态,且只返回特定的字段
const users = await userRepository.createQueryBuilder("user") // 定义主表别名 user
.leftJoinAndSelect("user.role", "role") // 关联并加载角色数据
.select([
"user.id",
"user.username",
"role.name" // 只选用户 ID、用户名和角色名
])
.where("user.status = :status", { status: 'active' })
.andWhere("role.code = :code", { code: 'ADMIN' })
.orderBy("user.createTime", "DESC") // 按创建时间倒序
.getMany(); // 获取数组结果
💡 示例 2:聚合统计与分组
// 统计每个角色的用户数量,且只显示人数大于 5 的角色。
const stats = await userRepository.createQueryBuilder("user")
.innerJoin("user.role", "role")
.select("role.name", "roleName") // 选取角色名
.addSelect("COUNT(user.id)", "userCount") // 统计人数
.groupBy("role.id") // 按角色 ID 分组
.having("COUNT(user.id) > :limit", { limit: 5 }) // 过滤统计结果
.getRawMany(); // 注意:聚合查询通常返回原始数据(Raw),而不是实体对象
💡 示例 3:高阶分页查询
// 统计每个角色的用户数量,且只显示人数大于 5 的角色。
const [list, total] = await userRepository.createQueryBuilder("user")
.leftJoinAndSelect("user.photos", "photo")
.where("user.age > :age", { age: 18 })
.skip(0) // 跳过 0 条
.take(10) // 取 10 条
.getManyAndCount(); // 同时返回查询到的数组和总记录数
📌 核心提示
- getMany vs getRawMany:
- 如果你想要嵌套的对象结构(ORM 风格),用
getMany()。 - 如果你在用
SUM,COUNT,MAX等聚合函数,或者自定义了select别名,用getRawMany() - getOne VS getRawOne 同理
- 如果你想要嵌套的对象结构(ORM 风格),用
四、dataSource事务
🔑 事务的特性
- 要么全成功,要么全失败
- 并发事务互不干扰
- 事务前后数据状态合法
✅ 支持的操作
manager.findmanager.findOnemanager.savemanager.updatemanager.deletemanager.createQueryBuilder
➡️ 场景
“只要有两条写操作,而且它们在逻辑上不能分开,就必须上事务。”
👉 dataSource.transaction 的执行流程(非常重要)
你不需要手写 BEGIN / COMMIT / ROLLBACK
- 从连接池中取一个连接
BEGIN- 执行回调函数里的所有操作
- 没有异常 →
COMMIT - 有异常 →
ROLLBACK - 释放连接
💡 示例1:事务中更新 + 插入
在事务中使用findOne(实体,{})之类的方法,需要加上实体,否则找不到数据
await this.dataSource.transaction(async (manager) => {
// 1. 更新操作:第一个参数必须是 Order 实体
// 语法:manager.update(实体, 条件, 内容)
await manager.update(Order, id, { status: 'paid' });
// 2. 插入操作:第一个参数必须是 OrderLog 实体
// 语法:manager.insert(实体, 内容)
await manager.insert(OrderLog, { orderId: id, action: 'pay' });
});
💡 示例2:事务 + createQueryBuilder(最常见组合)
所有 QueryBuilder 都必须来自 manager
// 1. 发起事务:通过 dataSource 开启一个事务环境
// manager 是一个实体的管理器,它被绑定到了当前的事务连接上
await this.dataSource.transaction(async (manager) => {
// --- 第一部分:更新订单状态 ---
await manager
.createQueryBuilder() // 2. 创建一个查询构建器实例
.update(Order) // 3. 指定要更新的实体(Order 表)
.set({ status: 'paid' }) // 4. 设置更新字段:将 status 改为 'paid'
.where('id = :id', { id }) // 5. 设置过滤条件:匹配传入的 id,使用 :id 占位符防注入
.execute(); // 6. 执行 SQL 语句(生成 UPDATE order SET status = 'paid' WHERE id = ...)
// --- 第二部分:插入操作日志 ---
await manager
.createQueryBuilder() // 7. 再次创建一个查询构建器实例
.insert() // 8. 指定本次操作为插入(INSERT)
.into(OrderLog) // 9. 指定插入的目标表(OrderLog 表)
.values({ // 10. 定义要插入的数据对象
orderId: id, // 记录关联的订单 ID
action: 'pay' // 记录操作动作为 'pay'
})
.execute(); // 11. 执行 SQL 语句(生成 INSERT INTO order_log ...)
// 12. 闭包结束:
// 如果上述两步都成功,事务会自动 COMMIT(提交)
// 如果其中任何一步报错,事务会自动 ROLLBACK(回滚),保证数据一致性
});
💡 示例3:使用
await this.dataSource.transaction(async (manager) => {
// 1. 更新操作:第一个参数必须是 Order 实体
// 语法:manager.update(实体, 条件, 内容)
await manager.update(Order, id, { status: 'paid' });
// 2. 插入操作:第一个参数必须是 OrderLog 实体
// 语法:manager.insert(实体, 内容)
await manager.insert(OrderLog, { orderId: id, action: 'pay' });
});
五、乐观锁 vs 悲观锁(高并发必备)
一句话先记住:
乐观锁 = 不锁数据,靠“版本号”防止被别人改过
悲观锁 = 先锁住数据,别人必须等我用完
✅ 乐观锁
- 不锁表
- 用 version 控制
- 读多写少
// 数据库中多一个字段
@VersionColumn()
version: number;
---------------------------
// 如果 version不匹配 TypeORM 抛异常
// 在数据库层面的本质
UPDATE device
SET status = 'active', version = version + 1
WHERE id = 1 AND version = 3;
💡 示例:多个管理员同时编辑设备信息,不希望“后保存的人覆盖前一个人
// TypeORM中实体类的定义
@Entity('tb-device')
export class Device {
@PrimaryGeneratedColumn()
id: number;
@Column()
content: string;
@VersionColumn()
version: number;
}
// service层
/**
1. 打开编辑页 → 读 version
2. 提交保存
3. version 不一致 → 数据已被其他人修改,请刷新重试”
*/
try {
const device = await device.findOneBy({ id: 1 });
device.content = '这是张三修改的';
await device.save(device); // 自动校验 version
} catch (err) {
throw new ConflictException('数据已被其他人修改,请刷新重试');
}
🔒 悲观锁
- 先锁住数据
- 其他事务只能等
- 适合写多 / 高并发 / 不能出错的场景
✅ 悲观锁模式详解表
| 模式名称 | 对应的 SQL | 核心解释 | 适用场景 |
|---|---|---|---|
pessimistic_write | FOR UPDATE | 写锁。事务 A 锁定行后,其他事务不能读(指加锁读)也不能写,必须等待事务 A 提交。 | 最常用。如:电商扣减库存、银行转账,确保数据被你读取到修改完成期间没人能动。 |
pessimistic_read | LOCK IN SHARE MODE | 共享锁。多个事务可以同时读取该行,但都不能修改。只有当所有共享锁都释放后,才能写。 | 防级联删除。如:读取父表数据时防止别人删掉它,但允许别人同时也来读。 |
pessimistic_write_or_fail | FOR UPDATE NOWAIT | 立即失败锁。尝试加写锁,如果该行已经被别人锁住了,不排队等待,直接报错。 | 高响应要求。如:抢购位,如果拿不到锁直接告诉用户“系统繁忙”,而不是让用户转圈等待。 |
pessimistic_partial_write | FOR UPDATE SKIP LOCKED | 跳过锁。尝试锁定多行时,如果某些行被锁了,直接忽略它们,只返回未被锁定的行。 | 消息队列/任务领取。如:多个 Worker 抢任务,谁没被锁就领谁,互不干扰,极大提高并发效率。 |
lock: { mode: 'pessimistic_write' }
💡 示例1:必须在事务中
/** 在事务结束前
其他更新请求会排队等待
保证数据绝对安全
*/
await dataSource.transaction(async (manager) => {
const device = await manager.findOne(Device, {
where: { id: 1 },
lock: { mode: 'pessimistic_write' },
});
device.status = 'busy';
await manager.save(device);
});
💡 示例2:库存扣减
await dataSource.transaction(async (manager) => {
const product = await manager.findOne(Product, {
where: { id: productId },
lock: { mode: 'pessimistic_write' },
});
if (product.stock <= 0) {
throw new BadRequestException('库存不足');
}
product.stock -= 1;
await manager.save(product);
});