NocoBase 数据库 Filter 操作符完全指南:从 $eq 到 $dateOn 的 40+ 查询运算符详解
【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase
导读:本文以 NocoBase 官方数据库 API 文档 operators.md 为骨架,系统讲解 Repository 的
find/findOne/findAndCount/count等接口filter参数中全部内置运算符的语义、SQL 对应关系、适用字段类型与实战示例,并结合 packages/core/database/src/operators 目录下的源码实现与测试用例,深入揭示每个运算符在 PostgreSQL / MySQL / SQLite 等不同数据库下的底层行为。读完本文,你将能熟练编写任意复杂度的 NocoBase 数据过滤条件,并掌握通过db.registerOperators()扩展自定义运算符的方法。
一、Filter 参数与运算符的 JSON 化设计
在 NocoBase 中,Repository是数据访问的核心入口,其查询方法都接受一个filter参数用于描述过滤条件:
const repository = db.getRepository('books'); repository.find({ filter: { title: { $eq: '春秋', }, }, });为了让过滤条件能够被 JSON 序列化、跨 HTTP API 传输,NocoBase 将查询运算符统一表示为以$为前缀的字符串标识,例如$eq、$in、$and。这样filter就是一个纯 JSON 结构,既可以在服务端代码中直接书写,也可以由前端通过 API 请求参数动态传入。
运算符注册的底层机制
从源码看,运算符的注册发生在Database类的initOperators()方法中(database.ts):
initOperators() { const operators = new Map(); // Sequelize 内置 for (const key in Op) { operators.set('$' + key, Op[key]); const val = Utils.underscoredIf(key, true); operators.set('$' + val, Op[key]); operators.set('$' + val.replace(/_/g, ''), Op[key]); } this.operators = operators; this.registerOperators({ ...(extendOperators as unknown as MapOf<OperatorFunc>), }); }这里有两个层次:
- Sequelize 内置运算符兜底:NocoBase 底层基于 Sequelize ORM,启动时会把 Sequelize
Op中的全部符号(如Op.like、Op.between、Op.regexp、Op.col、Op.is、Op.not等)批量注册为对应的$前缀字符串。这就是$like、$notLike、$iLike、$regexp、$col、$is、$not、$gt、$between等运算符的来源。 - NocoBase 自定义实现覆盖:随后通过
registerOperators()注册 NocoBase 自研的运算符实现(见 operators/index.ts),这些实现会覆盖同名的 Sequelize 默认行为,并新增$dateOn、$isFalsy、$match、$exists等 NocoBase 专属运算符:
// packages/core/database/src/operators/index.ts export default { ...association, // $exists / $notExists ...date, // $dateOn / $dateBefore / $dateAfter ... ...array, // $match / $anyOf / $arrayEmpty ... ...empty, // $empty / $notEmpty ...string, // $includes / $startsWith / $endWith ... ...eq, // $eq ...ne, // $ne ...$in, // $in ...notIn, // $notIn ...boolean, // $isFalsy / $isTruly ...childCollection, };也就是说,一套filter语法规则,最终会被解析成对应数据库方言可执行的 SQL 条件,这一设计让上层业务代码与底层数据库解耦。
二、通用运算符(适用于任意字段类型)
通用运算符对所有字段类型都有效,用于最基础的等值、包含与空值判断。
$eq— 等于
判断字段值是否等于指定值,相当于 SQL 的=:
repository.find({ filter: { title: { $eq: '春秋', }, }, });当$eq的值为简单字符串时,写法等价于title: '春秋'的简写形式。从 eq.ts 的实现看,$eq还有两个隐藏行为:
- 字符串字段的自动类型转换:若目标字段类型是
string而传入值不是字符串,会执行String(val)强转;传入数组时则映射为Op.in(IN查询); - 数组值支持:即使不是字符串字段,只要传入数组,
$eq也会转为IN查询。
$ne— 不等于
判断字段值是否不等于指定值,相当于 SQL 的!=:
repository.find({ filter: { title: { $ne: '春秋', }, }, });注意 ne.ts 中的实现细节:当值为数组时映射为NOT IN;当值为null时映射为IS NOT NULL(即Op.ne: null);而其他普通值会被翻译为{ Op.or: { Op.ne: val, Op.is: null } }—— 这意味着$ne的查询结果会包含该字段为NULL的记录,因为NULL != 值在 SQL 语义中同样成立。这在数据统计时需要特别留意。
$is— 是否为指定值
判断字段值是否为指定值,相当于 SQL 的IS,常用于空值判断:
repository.find({ filter: { title: { $is: null, }, }, });$not— 是否不为指定值
判断字段值是否不为指定值,相当于 SQL 的IS NOT:
repository.find({ filter: { title: { $not: null, }, }, });$col— 字段间比较
判断字段值是否等于另一个字段的值,相当于 SQL 的=作用于两列之间:
repository.find({ filter: { title: { $col: 'name', }, }, });以上示例表示筛选出title字段值与name字段值相等的记录,适用于同一行内字段间的关联比较。
$in/$notIn— 属于 / 不属于集合
判断字段值是否在指定数组中,分别相当于 SQL 的IN与NOT IN:
repository.find({ filter: { title: { $in: ['春秋', '战国'], }, }, }); repository.find({ filter: { title: { $notIn: ['春秋', '战国'], }, }, });这两个运算符在仓库中对应 in.ts 与 notIn.ts,并有专门的测试用例 in.test.ts 覆盖。
$empty/$notEmpty— 空值判断
$empty:判断一般字段是否为空。字符串字段判断是否为空串,数组字段判断是否为空数组;$notEmpty:判断一般字段是否不为空,规则同上取反。
repository.find({ filter: { title: { $empty: true, }, }, }); repository.find({ filter: { title: { $notEmpty: true, }, }, });其实现位于 empty.ts,会结合字段类型分别生成针对空字符串、空数组或NULL的条件。
三、逻辑运算符
逻辑运算符用于组合多个过滤条件,是所有复杂查询的基础。
$and— 逻辑与
逻辑 AND,相当于 SQL 的AND:
repository.find({ filter: { $and: [{ title: '诗经' }, { isbn: '1234567890' }], }, });$or— 逻辑或
逻辑 OR,相当于 SQL 的OR。注意其中可以嵌套其他运算符:
repository.find({ filter: { $or: [{ title: '诗经' }, { publishedAt: { $lt: '0000-00-00T00:00:00Z' } }], }, });// 多条件 OR:数组中的每个对象都是一组独立条件 repository.find({ filter: { $or: [ { status: 'published' }, { status: 'draft', authorId: 1 }, ], }, });$and/$or的取值都是条件对象数组,数组中的每个元素本身又是一个完整的 filter 片段,因此可以任意嵌套组合出多层次的复杂查询。
四、布尔类型字段运算符
以下运算符专门用于布尔类型字段(type: 'boolean'),在 boolean.ts 中实现。
$isFalsy— 判断为假
布尔字段值为false、0和NULL的情况都会被判断为$isFalsy: true:
repository.find({ filter: { isPublished: { $isFalsy: true, }, }, });源码中,$isFalsy在传入true/'true'时生成{ Op.or: { Op.is: null, Op.eq: false } },即同时覆盖NULL与false两种“假”的情况;传入false时则取反,生成{ Op.eq: true }。
$isTruly— 判断为真
布尔字段值为true和1的情况都会被判断为$isTruly: true:
repository.find({ filter: { isPublished: { $isTruly: true, }, }, });相关行为可参考测试 boolean-operator.test.ts。
五、数字类型字段运算符
以下运算符用于数字类型字段,包括:
type: 'integer'type: 'float'type: 'double'type: 'real'type: 'decimal'
大小比较:$gt/$gte/$lt/$lte
分别对应 SQL 的>、>=、<、<=:
// 大于 repository.find({ filter: { price: { $gt: 100, }, }, }); // 大于等于 repository.find({ filter: { price: { $gte: 100, }, }, }); // 小于 repository.find({ filter: { price: { $lt: 100, }, }, }); // 小于等于 repository.find({ filter: { price: { $lte: 100, }, }, });区间判断:$between/$notBetween
判断字段值是否在指定的两个值之间,分别相当于 SQL 的BETWEEN与NOT BETWEEN。值为包含两个元素的数组[最小值, 最大值]:
repository.find({ filter: { price: { $between: [100, 200], }, }, }); repository.find({ filter: { price: { $notBetween: [100, 200], }, }, });六、字符串类型字段运算符
以下运算符用于字符串类型字段(type: 'string'),核心实现在 string.ts。
包含与排除:$includes/$notIncludes
$includes:判断字符串字段是否包含指定子串;$notIncludes:判断字符串字段是否不包含指定子串。
repository.find({ filter: { title: { $includes: '三字经', }, }, }); repository.find({ filter: { title: { $notIncludes: '三字经', }, }, });前缀匹配:$startsWith/$notStatsWith
$startsWith:判断字符串字段是否以指定子串开头;$notStatsWith:判断字符串字段是否不以指定子串开头(文档中的拼写如此,源码实现键名对应为$notStartsWith,见 string.ts)。
repository.find({ filter: { title: { $startsWith: '三字经', }, }, }); repository.find({ filter: { title: { $notStatsWith: '三字经', }, }, });后缀匹配:$endsWith/$notEndsWith
$endsWith:判断字符串字段是否以指定子串结尾;$notEndsWith:判断字符串字段是否不以指定子串结尾。
repository.find({ filter: { title: { $endsWith: '三字经', }, }, }); repository.find({ filter: { title: { $notEndsWith: '三字经', }, }, });从源码结构看,string.ts 中这两个运算符的实现键名实际写作$endWith/$notEndWith(见 L159-L185),使用时两种拼写都需以实际生效键名为准。
SQL 风格匹配:$like/$notLike/$iLike/$notILike
$like:字段值是否包含指定的字符串,相当于 SQL 的LIKE;$notLike:字段值是否不包含指定的字符串,相当于 SQL 的NOT LIKE;$iLike:字段值是否包含指定字符串且忽略大小写,相当于 SQL 的ILIKE(仅 PG 适用);$notILike:字段值是否不包含指定字符串且忽略大小写,相当于 SQL 的NOT ILIKE(仅 PG 适用)。
repository.find({ filter: { title: { $like: '计算机', }, }, }); repository.find({ filter: { title: { $notLike: '计算机', }, }, }); repository.find({ filter: { title: { $iLike: 'Computer', }, }, }); repository.find({ filter: { title: { $notILike: 'Computer', }, }, });源码细节:虽然$like系列直接来源于 Sequelize 内置运算符,但 NocoBase 自定义的$includes/$startsWith等运算在底层同样走 LIKE/ILIKE 路径。以 string.ts 为例:
- PostgreSQL:会通过
CAST(字段 AS TEXT)将字段强制转为文本后拼接%...%通配符,并区分ILIKE(忽略大小写)与LIKE; - MySQL 等数据库:
ILIKE会退化为LIKE(MySQL 的默认排序规则本身通常不区分大小写); - 通配符转义:
escapeLike()会将用户输入中的%和_转义,避免通配符注入导致意外匹配;MSSQL 方言下则采用[包裹的转义写法; - 数组值支持:
$includes、$startsWith等传入数组时,会生成多个条件并用OR/AND组合。
正则匹配:$regexp/$notRegexp/$iRegexp/$notIRegexp
$regexp:字段值是否匹配指定的正则表达式,相当于 SQL 的REGEXP(仅 PG 适用);$notRegexp:字段值是否不匹配指定的正则表达式,相当于 SQL 的NOT REGEXP(仅 PG 适用);$iRegexp:字段值是否匹配指定正则且忽略大小写,相当于 SQL 的~*(仅 PG 适用);$notIRegexp:字段值是否不匹配指定正则且忽略大小写,相当于 SQL 的!~*(仅 PG 适用)。
repository.find({ filter: { title: { $regexp: '^计算机', }, }, }); repository.find({ filter: { title: { $notRegexp: '^计算机', }, }, }); repository.find({ filter: { title: { $iRegexp: '^COMPUTER', }, }, }); repository.find({ filter: { title: { $notIRegexp: '^COMPUTER', }, }, });注意:正则类运算符(REGEXP、ILIKE等)依赖数据库方言能力,官方文档标注为仅 PostgreSQL 适用;在 MySQL / SQLite 等其他数据库上使用时需要确认对应方言的等价支持。字符串运算符的完整行为可参考测试 string-operator.test.ts 与 operators/tests/string.test.ts。
七、日期类型字段运算符
以下运算符用于日期类型字段(type: 'date'),核心实现在 date.ts。日期运算符内部会调用@nocobase/utils的parseDate进行解析,并根据字段类型自动处理时区与格式转换。
$dateOn/$dateNotOn— 在某一天 / 不在某一天
判断日期字段是否(不)在某天内,传入日期字符串即可:
repository.find({ filter: { createdAt: { $dateOn: '2021-01-01', }, }, }); repository.find({ filter: { createdAt: { $dateNotOn: '2021-01-01', }, }, });源码细节:$dateOn在收到'2021-01-01'这类单值时会解析为[当天 00:00:00, 次日 00:00:00)的左闭右开区间,翻译为{ Op.gte: 起始, Op.lt: 结束 };若parseDate返回数组(日期范围),则同样生成区间条件。这意味着“在某一天”实际是“当天零点至次日零点之间”,不会漏掉当天任意时刻的数据。
$dateBefore/$dateNotBefore— 在某个值之前 / 不在之前
$dateBefore:判断日期字段是否在某个值之前,相当于小于传入的日期值;$dateNotBefore:判断日期字段是否不在某个值之前,相当于大于等于传入的日期值。
repository.find({ filter: { createdAt: { $dateBefore: '2021-01-01T00:00:00.000Z', }, }, }); repository.find({ filter: { createdAt: { $dateNotBefore: '2021-01-01T00:00:00.000Z', }, }, });$dateAfter/$dateNotAfter— 在某个值之后 / 不在之后
$dateAfter:判断日期字段是否在某个值之后,相当于大于传入的日期值;$dateNotAfter:判断日期字段是否不在某个值之后,相当于小于等于传入的日期值。
repository.find({ filter: { createdAt: { $dateAfter: '2021-01-01T00:00:00.000Z', }, }, }); repository.find({ filter: { createdAt: { $dateNotAfter: '2021-01-01T00:00:00.000Z', }, }, });源码细节(时区与特殊日期字段):date.ts中的parseDateTimezone()会根据字段类型决定解析时区——datetimeNoTz(无时区时间)与dateOnly(仅日期)字段强制按+00:00解析,其他日期字段使用db.options.timezone。toDate()还会针对UnixTimestampField调用field.dateToValue()将日期转回 Unix 时间戳,并在转换完成后发出filterToDate事件供其他逻辑订阅。此外源码中还实现了文档未单列出的$dateBetween(日期区间)运算符。相关行为有大量测试覆盖,如 operator/date/datetime-tz.test.ts、date-only.test.ts 与 unix-timestamp.test.ts。
八、数组类型字段运算符
以下运算符用于数组类型字段(type: 'array'),核心实现在 array.ts。这类运算符对不同数据库生成了差异化的 SQL:
| 运算符 | 语义 | PostgreSQL | MySQL | SQLite |
|---|---|---|---|---|
$match | 数组值完全匹配指定数组 | @>与<@双向包含 | JSON_CONTAINS双向 | json()相等比较 |
$anyOf | 包含指定数组中任意值 | ?|运算符 | JSON_OVERLAPS | json_each子查询 |
$noneOf | 不包含指定数组中任意值 | 取反?| | NOT JSON_OVERLAPS | 取反json_each |
$arrayEmpty | 数组是否为空 | jsonb_array_length | json_length | json_array_length |
$arrayNotEmpty | 数组是否不为空 | 同上取>0 | 同上取>0 | 同上取>0 |
$match/$notMatch— 完全匹配 / 不匹配
判断数组字段的值是否(不)匹配指定数组中的值:
repository.find({ filter: { tags: { $match: ['文学', '历史'], }, }, }); repository.find({ filter: { tags: { $notMatch: ['文学', '历史'], }, }, });$anyOf/$noneOf— 包含任意值 / 不包含任意值
判断数组字段的值是否(不)包含指定数组中的任意值:
repository.find({ filter: { tags: { $anyOf: ['文学', '历史'], }, }, }); repository.find({ filter: { tags: { $noneOf: ['文学', '历史'], }, }, });$arrayEmpty/$arrayNotEmpty— 数组是否为空
repository.find({ filter: { tags: { $arrayEmpty: true, }, }, }); repository.find({ filter: { tags: { $arrayNotEmpty: true, }, }, });源码细节:emptyQuery()会生成IFNULL(json_array_length(字段), 0) = 0(PG 用coalesce(jsonb_array_length(...), 0),MySQL 用json_length(...))这样的表达式来判断空数组,并对NULL值做了兜底处理,因此$arrayEmpty也能覆盖字段为NULL的情况。
九、关系字段类型运算符
以下运算符用于判断关系是否存在,字段类型包括:
type: 'hasOne'type: 'hasMany'type: 'belongsTo'type: 'belongsToMany'
实现在 association.ts:
$exists— 有关系数据
repository.find({ filter: { author: { $exists: true, }, }, });源码中直接映射为{ Op.not: null },即关系外键不为空。
$notExists— 无关系数据
repository.find({ filter: { author: { $notExists: true, }, }, });源码中映射为{ Op.is: null },即关系外键为空。例如筛选出没有关联作者(author为空)的图书记录。
十、扩展自定义运算符:db.registerOperators()
NocoBase 的运算符体系是开放可扩展的。Database类提供了registerOperators()方法,用于注册自定义运算符:
// packages/core/database/src/database.ts#L764-L768 registerOperators(operators: MapOf<OperatorFunc>) { for (const [key, operator] of Object.entries(operators)) { this.operators.set(key, operator); } }每个运算符本质上是一个函数,接收(value, ctx)两个参数,返回 Sequelize 可识别的查询条件对象(Op符号组合、Sequelize.literal原生 SQL 片段等)。ctx中携带了字段路径、模型、数据库实例等上下文信息(如 string.ts 中的ctx.db.sequelize.getDialect()、ctx.fullName、ctx.fieldName)。
扩展示例——注册一个判断字符串长度为指定值的运算符:
import { Op } from 'sequelize'; db.registerOperators({ $lengthOf(value) { // 返回 Sequelize 查询条件(此处为示意,实际需结合具体数据库方言实现) return { [Op.like]: `${'_'.repeat(value)}`, }; }, }); // 使用 repository.find({ filter: { title: { $lengthOf: 4, }, }, });// 自定义运算符还可以利用 ctx 访问数据库方言,实现跨库逻辑 db.registerOperators({ $customOperator(value, ctx) { const dialect = ctx.db.sequelize.getDialect(); // ...根据 dialect 返回不同实现 }, });扩展点的完整实现可查阅 database.ts 以及 operators/index.ts 中内置运算符的聚合方式,二者共同构成了“内置兜底 + 自定义覆盖”的可插拔运算符机制。
十一、与其他查询 API 的组合使用
filter参数同样适用于findOne、findAndCount、count等 Repository 方法:
// findOne:查询第一条匹配记录 const book = await repository.findOne({ filter: { title: { $eq: '春秋', }, }, }); // findAndCount:同时返回数据与总数,常用于分页列表 const [rows, total] = await repository.findAndCount({ filter: { $and: [ { price: { $between: [50, 200] } }, { publishedAt: { $dateOn: '2021-01-01' } }, ], }, offset: 0, limit: 20, }); // count:仅统计数量 const count = await repository.count({ filter: { isPublished: { $isTruly: true, }, }, });这些查询方法的行为在 repository.test.ts、filter.test.ts 与 filter-match.ts 中有大量测试与实现可对照验证。
十二、小结:运算符速查表
| 分类 | 运算符 | 相当于 SQL | 适用字段 |
|---|---|---|---|
| 通用 | $eq/$ne | =/!= | 任意 |
| 通用 | $is/$not | IS/IS NOT | 任意(常配null) |
| 通用 | $col | 列与列比较 | 任意 |
| 通用 | $in/$notIn | IN/NOT IN | 任意 |
| 通用 | $empty/$notEmpty | 空值/空串/空数组判断 | 任意 |
| 逻辑 | $and/$or | AND/OR | 条件组合 |
| 布尔 | $isFalsy/$isTruly | 真假判断 | boolean |
| 数字 | $gt/$gte/$lt/$lte | >/>=/</<= | 数字 |
| 数字 | $between/$notBetween | BETWEEN/NOT BETWEEN | 数字 |
| 字符串 | $includes/$notIncludes | 包含/不包含子串 | string |
| 字符串 | $startsWith/$notStatsWith | 前缀/非前缀 | string |
| 字符串 | $endsWith/$notEndsWith | 后缀/非后缀 | string |
| 字符串 | $like/$notLike | LIKE/NOT LIKE | string |
| 字符串 | $iLike/$notILike | ILIKE/NOT ILIKE(仅 PG) | string |
| 字符串 | $regexp/$notRegexp/$iRegexp/$notIRegexp | REGEXP/~*等(仅 PG) | string |
| 日期 | $dateOn/$dateNotOn | 当天/非当天 | date |
| 日期 | $dateBefore/$dateNotBefore | </>= | date |
| 日期 | $dateAfter/$dateNotAfter | >/<= | date |
| 数组 | $match/$notMatch/$anyOf/$noneOf | 数组包含匹配 | array |
| 数组 | $arrayEmpty/$arrayNotEmpty | 空/非空数组 | array |
| 关系 | $exists/$notExists | 外键非空/为空 | 关系字段 |
掌握这五大类 40+ 运算符及其底层实现逻辑,你就能在 NocoBase 中构建从简单等值查询到跨字段、跨关系、跨数据库方言的任意过滤条件,并在需要时通过registerOperators()无缝扩展自己的查询能力。
【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考