查询对象:MongoDB过滤的第一入口
在MongoDB中,find()并不是一个只能"查全部"的方法。它接受一个查询文档(querydocument)作为第一个参数,这个文档的结构与中存储的文档结构一致,只是字段值表达的是"匹配条件"而非"目标值"。理解这一点,是写好所有过滤查询的基础。
以员工employees为例,假设文档结构大致如下:
{
_id: ObjectId("..."),
name: "张伟",
address: "Delhi",
department: "研发",
joinDate: ISODate("2025-04-12")
}
如果只关心地址为Delhi的员工,查询条件写成{address:"Delhi"}即可。这里的address是字段名,"Delhi"是等值匹配条件。驱动会把这个对象翻译成MongoDB的BSON查询指令,服务端据此过滤。
一个容易忽略的细节是:查询对象的键名区分大小写,值也区分大小写。"Delhi"与"delhi"在默认的二进制比较下不会互相匹配。如果业务中地址录入不规范,等值查询会漏掉数据,这种问题在项目里往往比语法错误更难排查。
一个可运行的过滤示例
下面这段代码演示了按地址精确过滤的完整流程。文件名用code01_query_filter.js这种带编号的命名方式,便于在教程或项目中按顺序管理示例。
const MongoClient = require('mongodb').MongoClient;
const url = 'mongodb://localhost:27017/MongoDatabase';
MongoClient.connect(url, function (err, db) {
if (err) throw err;
const query = { address: 'Delhi' };
db.collection('employees')
.find(query)
.toArray(function (err, result) {
if (err) throw err;
console.log(result);
db.close();
});
});
运行:
node code01_query_filter.js
这段代码里,find(query)返回的是一个游标(Cursor),toArray()把游标中的文档一次性读入数组。控制台会打印出所有address为Delhi的员工文档。
关于连接与回调的取舍
上面的写法沿用了较早版本驱动的回调风格,MongoClient.connect成功后的db对象直接用于获取。这里有一个实际项目中值得注意的点:db.close()放在toArray回调内部,确保读取完成后才断开连接。如果放在find之后立即调用,游标可能尚未消费完,连接就被关闭,导致读取失败。这个顺序问题在并发场景下更容易暴露。
现在版本的Node.js驱动(4.x及以上)已经推荐使用Promise或async/await,并且connect返回的是MongoClient实例,获取方式变为client.db('MongoDatabase').collection('employees')。旧教程中的db.collection写法在新驱动里需要调整。如果你的项目还在用旧写法,升级驱动时要留意这一变化。
用async/await重写后大致是这样:
const { MongoClient } = require('mongodb');
const url = 'mongodb://localhost:27017';
const client = new MongoClient(url);
async function findEmployees() {
try {
await client.connect();
const db = client.db('MongoDatabase');
const result = await db.collection('employees')
.find({ address: 'Delhi' })
.toArray();
console.log(result);
} finally {
await client.close();
}
}
findEmployees();
连接只建立一次、在finally中统一释放,这种做法比每次查询都connect/close更适合服务端常驻进程。回调嵌套层级也随之减少,错误传递更清晰。
正则表达式:字符串字段的灵活匹配
等值匹配只能命中一致的字符串。当需求变成"地址以L开头"、"姓名包含某个字"这类模式匹配时,就需要正则表达式。MongoDB支持在查询对象中直接写入JavaScript正则字面量,驱动会将其作为BSON正则类型传给服务端。
匹配地址以字母L开头的员工:
const query = { address: /^L/ };
^表示字符串起始位置。这个查询会命中London、Lucknow、Lagos等所有以L开头的地址。相比$where或客户端过滤,正则查询在服务端执行,只把匹配结果传回,网络开销小得多。
完整示例文件命名为code02_query_regex.js:
const MongoClient = require('mongodb').MongoClient;
const url = 'mongodb://localhost:27017/MongoDatabase';
MongoClient.connect(url, function (err, db) {
if (err) throw err;
const query = { address: /^L/ };
db.collection('employees')
.find(query)
.toArray(function (err, result) {
if (err) throw err;
console.log(result);
db.close();
});
});
运行:
node code02_query_regex.js
正则的使用边界与性能考量
正则只能作用于字符串类型的字段。如果对数值字段或日期字段使用正则,MongoDB不会报错,但也不会匹配到任何文档——这种"静默失败"在排查时容易让人困惑。
另一个关键点是索引。普通等值查询可以走B树索引,而正则查询只有在模式以^开头且不区分大小写时,才有可能利用索引做前缀扫描。像/Delhi/这种非锚定正则,会退化为全扫描。数据量达到几十万条以上时,这种查询的延迟会明显上升。
在项目里,我通常会把频繁用到的前缀查询单独抽出来,并在对应字段上建立索引。例如地址前缀查询多,就为address建索引;如果业务允许,把地址规范化成枚举值再做等值匹配,往往比正则更稳定。正则更适合后台检索、运营筛选这类对延迟不敏感的场景。
查询条件的组合方式
实际业务很少只有一个条件。MongoDB的查询对象支持在同一层级写多个字段,它们之间是逻辑与的关系:
const query = {
address: /^L/,
department: '研发'
};
这表示"地址以L开头,并且部门是研发"。如果需要逻辑或,用$or:
const query = {
$or: [
{ address: /^L/ },
{ address: /^D/ }
]
};
$or的每个元素都是一个独立查询对象,理解这一点后,复杂条件可以层层嵌套。$or中的每个分支如果都能走索引,整体效率才理想;否则可能出现一个分支全表扫描拖慢整个查询的情况。
现在框架中的惯用写法
在Express、Koa、NestJS这类框架里,查询条件通常来自请求参数,需要先做校验再拼装查询对象。直接把req.query透传给find()是危险的,因为客户端可能传入$where、$ne等操作符,造成注入风险。
比较稳妥的做法是显式提取字段并做类型检查:
function buildEmployeeQuery(params) {
const query = {};
if (typeof params.address === 'string' && params.address.trim()) {
query.address = new RegExp('^' + escapeRegExp(params.address.trim()));
}
if (typeof params.department === 'string' && params.department.trim()) {
query.department = params.department.trim();
}
return query;
}
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
escapeRegExp这一步很有必要。用户输入的字符串可能包含正则元字符,如果不转义,轻则查询结果异常,重则构造出消耗大量CPU的复杂正则。这是很多教程示例里不会提到、但线上项目必须处理的问题。
本节课程知识要点
-
find()的第一个参数是查询对象,字段名与文档结构对应,值表达匹配条件。 -
等值查询区分大小写,数据录入不规范时需要额外处理。
-
正则只能用于字符串字段,锚定
^开头的正则有利用索引的可能,非锚定正则会全扫描。 -
旧版回调写法中
db.close()的位置影响读取结果,现在驱动推荐async/await与连接复用。 -
组合条件默认是逻辑与,逻辑或用
$or,每个分支的索引情况决定整体性能。 -
来自外部的查询参数需要显式校验和转义,避免操作符注入与正则拒绝服务。
练习与思考
-
把
code01_query_filter.js改写为async/await版本,并加入try/finally确保连接释放。 -
写一个查询,检索地址以
L或D开头、且部门为研发的员工。 -
思考:如果
address字段上已有索引,查询条件写成/^L/i(忽略大小写)时,索引还能被有效利用吗?为什么? -
在
escapeRegExp的基础上,给buildEmployeeQuery增加一个按姓名模糊匹配的参数,并说明为什么推荐前缀匹配而非任意位置匹配。
find()的过滤能力看着简单,真正拉开差距的是对查询对象语义、索引行为和输入安全的把握。把等值查询和正则匹配用对场景,再配合连接复用与参数校验,这套数据访问层在中小规模项目里已经足够稳。后续如果要处理聚合、分页或事务,也可以在这个基础上平滑扩展。