TypeORM invalidWhereValuesBehavior 深入解析:WHERE 条件中 null 与 undefined 的处理机制
本篇指南聚焦 TypeORM 中 invalidWhereValuesBehavior 数据源配置项,讲解它如何决定 find、update、delete、softDelete、restore 等高层 API 在遇到 null/undefined 查询条件时的行为(报错、跳过或转为 SQL NULL)。读完本文,你将掌握三种行为的配置方式、适用边界,以及 QueryBuilder 底层 .where() 不受该配置影响时的正确 null 匹配写法,并能对照仓库源码理解其完整实现链路。
背景:WHERE 条件里的 null 与 undefined 是“危险值”
在 SQL 的语义里,NULL 表示“未知”,而 column = NULL 永远为假——没有任何行能满足这个条件。如果把 JavaScript 的 null 直接放进 TypeORM 的 where 条件,到底应该匹配数据库中的 NULL 值、被静默忽略,还是直接报错?TypeORM 认为这是一个应该由用户显式决策的问题,因此:
- 在 TypeScript 开启
strictNullChecks时,编译期就禁止显式传入null; - 运行期默认行为是抛出
TypeORMError,帮助你尽早发现潜在 bug; - 该行为可以通过数据源选项
invalidWhereValuesBehavior自定义。
这一配置只作用于高层操作:find 系列操作、Repository 方法、EntityManager 的 update / delete / softDelete / restore。它不影响 QueryBuilder 的 .where()、.andWhere()、.orWhere()——那是一层“原样透传”的底层 API(后文有专门章节说明)。
默认行为:抛错 + 使用 IsNull() 匹配 NULL
默认(未配置时,两个子项均按 "throw" 处理)TypeORM 会在 where 条件中出现 null 或 undefined 时直接抛出错误,而不是默默生成 col = NULL 这样的无效条件:
// 两个查询都会抛出错误
const posts1 = await repository.find({
where: {
text: null,
},
})
// Error: Null value encountered in property 'text' of a where condition.
const posts2 = await repository.find({
where: {
text: undefined,
},
})
// Error: Undefined value encountered in property 'text' of a where condition.
如果确实想匹配数据库中的 NULL 值,请使用 IsNull 操作符(详见 Find Options 文档):
const posts = await repository.find({
where: {
text: IsNull(),
},
})
这一默认策略在 SelectQueryBuilder 的 buildWhere 实现 中可以确认:逐键遍历 where 对象时,先检查 undefined,再检查 null,默认值通过 ?? "throw" 兜底。
配置项:invalidWhereValuesBehavior
在数据源配置中通过 invalidWhereValuesBehavior 自定义 null 与 undefined 的处理方式,两者相互独立:
const dataSource = new DataSource({
// ... 其他选项
invalidWhereValuesBehavior: {
null: "ignore" | "sql-null" | "throw",
undefined: "ignore" | "throw",
},
})
其类型定义位于 InvalidFindOptionsWhereBehavior.ts:
export type InvalidFindOptionsWhereBehavior = {
/**
* How to handle null values in where conditions.
* - 'ignore': Skip null properties
* - 'sql-null': Transform null to SQL NULL
* - 'throw': Throw an error when null is encountered (default)
*/
readonly null?: "ignore" | "sql-null" | "throw"
/**
* How to handle undefined values in where conditions.
* - 'ignore': Skip undefined properties
* - 'throw': Throw an error when undefined is encountered (default)
*/
readonly undefined?: "ignore" | "throw"
}
两个字段都是可选的,因此可以只覆盖其中一个维度。该选项声明在 BaseDataSourceOptions.ts 中,注释明确说明其作用范围:
Controls how null/undefined values in where criteria are handled by find and write methods (update/delete/softDelete/restore). Defaults to "throw".
null 的三种行为
'ignore' —— 跳过该属性
where 条件中的 null 值被忽略,相当于该条件不存在:
const dataSource = new DataSource({
// ... 其他选项
invalidWhereValuesBehavior: {
null: "ignore",
},
})
// 返回所有 post:text 属性被跳过,没有生成任何过滤条件
const posts = await repository.find({
where: {
text: null,
},
})
'sql-null' —— 转换为 IS NULL 条件
JavaScript null 会被转换为 SQL NULL 条件,只返回该列为 NULL 的行:
const dataSource = new DataSource({
// ... 其他选项
invalidWhereValuesBehavior: {
null: "sql-null",
},
})
// 只返回 text 列为 NULL 的 post
const posts = await repository.find({
where: {
text: null,
},
})
从源码看(SelectQueryBuilder.ts),"sql-null" 处理分两条路径:
- 目标属性是普通列时,条件直接生成为
`${aliasPath} IS NULL`; - 目标属性是关系时(如
where: { category: null }),生成`${alias}.${propertyPath} IS NULL`,见 关系分支的实现。
此外,写路径(update/delete 等)的实现 OrmUtils.normalizeWhereCriteria 在遇到 "sql-null" 时会把 null 值直接替换为 IsNull() 操作符,效果一致。
'throw' —— 抛错(默认)
const dataSource = new DataSource({
// ... 其他选项
invalidWhereValuesBehavior: {
null: "throw",
},
})
// 抛出错误
const posts = await repository.find({
where: {
text: null,
},
})
// Error: Null value encountered in property 'text' of a where condition.
// To match with SQL NULL, the IsNull() operator must be used.
// Set 'invalidWhereValuesBehavior.null' to 'ignore' or 'sql-null' in data source options to skip or handle null values.
undefined 的两种行为
'ignore' —— 跳过该属性
const dataSource = new DataSource({
// ... 其他选项
invalidWhereValuesBehavior: {
undefined: "ignore",
},
})
// 返回所有 post:text 属性被跳过
const posts = await repository.find({
where: {
text: undefined,
},
})
注意:这只针对显式赋值的 undefined;属性本身被省略(不在对象里)是另一种情况,本来就参与不了过滤,二者不要混淆。
'throw' —— 抛错(默认)
const dataSource = new DataSource({
// ... 其他选项
invalidWhereValuesBehavior: {
undefined: "throw",
},
})
// 抛出错误
const posts = await repository.find({
where: {
text: undefined,
},
})
// Error: Undefined value encountered in property 'text' of a where condition.
// Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in data source options to skip properties with undefined values.
从源码结构看,buildWhere 实现 还覆盖了一个嵌套场景:当关系子条件对象的所有属性都是 undefined 时(例如 { category: { name: undefined } }),若 undefined 行为是 "throw" 也会抛错,避免生成无谓的 join 且误判为有效条件。
两种选项组合使用
两个维度可以独立配置,以获得精细化控制:
const dataSource = new DataSource({
// ... 其他选项
invalidWhereValuesBehavior: {
null: "sql-null",
undefined: "throw",
},
})
这一组合的效果是:
- where 条件中的 JavaScript
null被转换为 SQLNULL(IS NULL); - 遇到任何
undefined立即抛错; - 未在 where 中提供的属性依然被忽略。
它适合这样的诉求:显式搜索数据库中的 NULL 值,同时把 undefined(通常意味着变量未赋值的编程错误)当作需要立即暴露的 bug。
哪些操作会受该配置影响
invalidWhereValuesBehavior 作用于高层 API,不作用于 QueryBuilder 的直接 .where() 调用。
Find 系列操作
// Repository.find() / findOne() / findBy() / findOneBy()
await repository.find({ where: { text: null } }) // 受 invalidWhereValuesBehavior 控制
// EntityManager.find() / findOne() / findBy() / findOneBy()
await manager.find(Post, { where: { text: null } }) // 受 invalidWhereValuesBehavior 控制
Repository 与 EntityManager 的写操作
// Repository.update()
await repository.update({ text: null }, { title: "Updated" }) // 受 invalidWhereValuesBehavior 控制
// Repository.delete()
await repository.delete({ text: null }) // 受 invalidWhereValuesBehavior 控制
// EntityManager.update()
await manager.update(Post, { text: null }, { title: "Updated" }) // 受 invalidWhereValuesBehavior 控制
// EntityManager.delete()
await manager.delete(Post, { text: null }) // 受 invalidWhereValuesBehavior 控制
// EntityManager.softDelete()
await manager.softDelete(Post, { text: null }) // 受 invalidWhereValuesBehavior 控制
写路径的实现与读路径不同:它不直接走 buildWhere,而是先经过 OrmUtils.normalizeWhereCriteria 归一化。该函数的行为要点(由其源码注释与实现确认):
- 顶层数组(OR 列表)逐元素归一化;
- 只对纯对象(plain object)逐键处理:
null/undefined按配置抛错、跳过或转为IsNull();嵌套纯对象递归处理,全被跳过的空嵌套会被移除; - 其他值——实体类实例、
FindOperator、Date、Buffer、原始id——原样透传、不做校验。因此实体实例中可空列为null时会渲染成col = NULL(匹配不到任何行),而不会抛错或转换;如需 null 语义,请传带IsNull()的纯对象(如{ text: IsNull() })。 - 未配置行为时同样默认
"throw",与读路径保持一致。
归一化之后的校验由 EntityManager.normalizeAndValidateWhereCriteria 完成,并带来一条重要规则:
空条件会被拒绝。
update、delete、softDelete、restore要求非空条件——空条件会渲染成WHERE 1=1,波及每一行。由于"ignore"会剥离null/undefined属性,当条件对象的所有键都被剥离后就会变成空对象,此时操作被拒绝而不是执行一次无过滤的写:
const dataSource = new DataSource({
// ... 其他选项
invalidWhereValuesBehavior: { null: "ignore", undefined: "ignore" },
})
// { text: null } 剥离后变成 {} -> 被拒绝,表不会被清空
await manager.delete(Post, { text: null })
// Error: Empty criteria(s) are not allowed for the delete method.
源码中 rendersNoPredicate 判定还覆盖了更多形态:空对象 {}、空数组 []、OR 数组中的空分支或裸原始值(如 [1, { id: 2 }] 中的 1,因为它不会生成谓词)。如果你确实要影响所有行,应显式使用专用的 updateAll() / deleteAll() 方法。
QueryBuilder.setFindOptions 走 find-options 路径
// setFindOptions 走 find-options 路径,因此受该配置控制
await dataSource
.createQueryBuilder(Post, "post")
.setFindOptions({ where: { text: null } }) // 受 invalidWhereValuesBehavior 控制
.getMany()
不受影响:QueryBuilder 的 .where()
QueryBuilder 的 .where()、.andWhere()、.orWhere() 是底层 API,不受 invalidWhereValuesBehavior 影响,null/undefined 原样透传:
// 此处不遵循 invalidWhereValuesBehavior —— null 原样透传
await dataSource
.createQueryBuilder()
.update(Post)
.set({ title: "Updated" })
.where({ text: null })
.execute()
QueryBuilder .where() 中 null / undefined 的实际行为
正因为 QueryBuilder 不做校验和转换,理解其行为对避免“查不到数据”的坑很关键。
null 传入对象式 .where()
null 会生成针对 NULL 的等值比较:
await dataSource
.createQueryBuilder(Post, "post")
.where({ text: null })
.getMany()
// 生成: WHERE post.text = NULL
而 SQL 中 column = NULL 恒为假,这条查询必然返回 0 行,通常不是你想要的。要匹配 NULL 请使用 IsNull():
import { IsNull } from "typeorm"
await dataSource
.createQueryBuilder(Post, "post")
.where({ text: IsNull() })
.getMany()
// 生成: WHERE post.text IS NULL
或者直接用字符串条件:
await dataSource
.createQueryBuilder(Post, "post")
.where("post.text IS NULL")
.getMany()
undefined 传入 .where()
同样生成 WHERE column = NULL,恒为假:
await dataSource
.createQueryBuilder(Post, "post")
.where({ text: undefined })
.getMany()
// 生成: WHERE post.text = NULL
// 返回: 0 行
行为对照表
| 值 | 高层 API(find/repository/manager) | QueryBuilder .where() |
|---|---|---|
null + "ignore" |
属性被跳过,无过滤 | WHERE col = NULL —— 0 行结果 |
null + "sql-null" |
WHERE col IS NULL |
WHERE col = NULL —— 0 行结果 |
null + "throw"(默认) |
抛出错误 | WHERE col = NULL —— 0 行结果 |
undefined + "ignore" |
属性被跳过,无过滤 | WHERE col = NULL —— 0 行结果 |
undefined + "throw"(默认) |
抛出错误 | WHERE col = NULL —— 0 行结果 |
IsNull() |
WHERE col IS NULL |
WHERE col IS NULL |
无论使用哪一层 API,只要想匹配数据库中的 NULL 值,都应该使用 IsNull() 操作符——它在高层和 QueryBuilder 上下文中都能正确工作。
仓库中的功能测试佐证
仓库在 test/functional/null-undefined-handling/ 目录下为这一机制提供了完整的测试覆盖,可作为行为验证与回归参考:
- find-options.test.ts:验证
repository.find与setFindOptions在默认行为下对null/undefined抛错,以及ignore、sql-null行为下的实际查询结果; - query-builders.test.ts:验证
EntityManager.update()、delete()、softDelete()等写操作在null/undefined下遵循invalidWhereValuesBehavior(例如配置null: "throw"后update(Post, { text: null }, ...)抛出包含 “Null value encountered” 的TypeORMError); - parameter-types.test.ts:覆盖实体实例、
Date、Buffer等非纯对象条件的透传边界。
测试中的 Post/Category 实体与 Post 实体定义 位于同目录 entity/ 子目录,可配合阅读。
小结
invalidWhereValuesBehavior是数据源级配置,独立控制null("ignore"/"sql-null"/"throw")与undefined("ignore"/"throw"),未配置时两者默认"throw";- 它覆盖 find 系列、Repository 以及 EntityManager 的
update/delete/softDelete/restore;写路径经OrmUtils.normalizeWhereCriteria归一化,且空条件会被拒绝以防误伤全表; - QueryBuilder 的
.where()系列不受影响,null/undefined会渲染成恒假的col = NULL; - 需要匹配 NULL 值时,无论哪一层 API,
IsNull()都是唯一可靠的写法。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00