将查询构建器条件转换为 MongoDB 操作,包括嵌套的子文档数组
Posted
技术标签:
【中文标题】将查询构建器条件转换为 MongoDB 操作,包括嵌套的子文档数组【英文标题】:Convert query builder conditions to MongoDB operations including nested array of subdocuments 【发布时间】:2019-07-29 18:13:48 【问题描述】:我正在客户端使用 Angular 8 和服务器端使用 MongoDB 4 / Mongoose 5 的 NodeJS 12 构建应用程序。我有一个由Angular2 query builder 模块生成的查询。 Angular 查询构建器对象被发送到服务器。
我有一个服务器端控制器函数converts the Angular query object to MongoDB operations。这非常适合为***属性生成查询,例如RecordID
和RecordType
。这也适用于构建嵌套和/或条件。
不过,我还需要支持查询子文档数组(示例架构中的“Items”数组)。
架构
这是我尝试查询的示例架构:
RecordID: 123,
RecordType: "Item",
Items: [
Title: "Example Title 1",
Description: "A description 1"
,
Title: "Example 2",
Description: "A description 2"
,
Title: "A title 3",
Description: "A description 3"
,
]
工作示例
仅限***属性
以下是查询生成器输出的示例,其中和/或仅针对***属性的条件:
"condition": "or", "rules": [ "field": "RecordID", "operator": "=", "value": 1 , "condition": "and", "rules": [ "field": "RecordType", "operator": "=", "value": "Item" ] ]
这是仅在***属性上转换为 MongoDB 操作后的查询生成器输出:
'$expr': '$or': [ '$eq': [ '$RecordID', 1 ] , '$and': [ '$eq': [ '$RecordType', 'Item' ] ] ]
将角度查询对象转换为 mongodb 运算符。
这是现有的查询转换函数
const conditions = "and": "$and", "or": "$or" ;
const operators = "=": "$eq", "!=": "$ne", "<": "$lt", "<=": "$lte", ">": "$gt", ">=": "$gte" ;
const mapRule = rule => (
[operators[rule.operator]]: [ "$"+rule.field, rule.value ]
);
const mapRuleSet = ruleSet =>
return
[conditions[ruleSet.condition]]: ruleSet.rules.map(
rule => rule.operator ? mapRule(rule) : mapRuleSet(rule)
)
;
let mongoDbQuery = $expr: mapRuleSet(q) ;
console.log(mongoDbQuery);
问题
该函数仅适用于***属性,例如 RecordID 和 RecordType,但我需要扩展它以支持子文档的 Items 数组。
显然,要查询嵌套子文档数组中的属性,必须使用$elemMatch
运算符,基于this related question。但是,就我而言,$expr 是构建嵌套和/或条件所必需的,因此我不能简单地切换到 $elemMatch
。
问题
如何扩展查询转换功能,使其也支持 $elemMatch 来查询子文档数组?有没有办法让 $expr 工作?
UI 查询生成器
这里是 UI 查询构建器,其中包含嵌套的“Items”子文档数组。在此示例中,结果应匹配 RecordType 等于 "Item" AND Items.Title 等于 "Example Title 1" OR Items.Title contains "Example"。
这是 UI 查询生成器生成的输出。注意:field
和 operator
属性值是可配置的。
"condition":"and","rules":["field":"RecordType","operator":"=","value":"Item","condition":"or","rules":["field":"Items.Title","operator":"=","value":"Example Title 1","field":"Items.Title","operator":"contains","value":"Example"]]
更新:我可能已经找到了一种查询格式,它也适用于 $elemMatch
的嵌套和/或条件。我不得不删除 $expr
运算符,因为 $elemMatch
在表达式中不起作用。我的灵感来自answer to this similar question。
这是有效的查询。下一步是让我弄清楚如何调整查询生成器转换函数来创建查询。
"$and": [
"RecordType":
"$eq": "Item"
,
"$or": [
"RecordID":
"$eq": 1
,
"Items":
"$elemMatch":
"Title": "$eq": "Example Title 1"
]
]
【问题讨论】:
澄清一下:在 MongoDB 中,您有两种方法可以查询这样的数组:1) $elemMatch 可用于查找至少一个匹配所有条件的元素 2) 像“Items.Title”这样的点表示法将尝试找到任何Example Title 1
,所有其他条件将单独应用,因此不必应用于相同的数组元素。有什么方法可以确定您尝试应用哪种过滤方式?
你能在const conditions = "and": "$and", "or": "$or" ;
中定义其他属性吗,比如添加elemMatch
例如:conditions = "and": "$and", "or": "$or", "elemMatch": " $elemMatch" ;`
@pengz 我明白你的意思。不幸的是,这里没有好的解决方案,因为Item
的两个条件都显示为单独的rules
,因此以前的解决方案在这里可以按预期工作。请记住,您也可以在数据库中存储这样的文档: Items: Title: "Example Title 1"
,因此库本身应该有一种方法来区分数组和嵌套文档。
@pengz 所以在以前的解决方案中,您可以忽略使用点符号指定路径的所有字段,例如Items.Title
,然后您需要遍历您的结构并将它们累积到单个 $elemMatch
只是为了澄清:为什么您的最终查询语法类似于 '$eq': [ '$RecordID', 1 ]
而不是 'RecordID': '$eq': 1
或 'RecordID': 1
?
【参考方案1】:
经过更多研究,我有了一个可行的解决方案。感谢所有提供见解的乐于助人的响应者。
该函数从 Angular 查询构建器模块获取查询并将其转换为 MongoDB 查询。
角度查询生成器
"condition": "and",
"rules": [
"field": "RecordType",
"operator": "=",
"value": "Item"
,
"condition": "or",
"rules": [
"field": "Items.Title",
"operator": "contains",
"value": "book"
,
"field": "Project",
"operator": "in",
"value": ["5d0699380a2958e44503acfb", "5d0699380a2958e44503ad2a", "5d0699380a2958e44503ad18"]
]
]
MongoDB查询结果
"$and": [
"RecordType":
"$eq": "Item"
,
"$or": [
"Items.Title":
"$regex": "book",
"$options": "i"
,
"Project":
"$in": ["5d0699380a2958e44503acfb", "5d0699380a2958e44503ad2a", "5d0699380a2958e44503ad18"]
]
]
代码
/**
* Convert a query object generated by UI to MongoDB query
* @param query a query builder object generated by Angular2QueryBuilder module
* @param model the model for the schema to query
* return a MongoDB query
*
*/
apiCtrl.convertQuery = async (query, model) =>
if (!query || !model)
return ;
const conditions = "and": "$and", "or": "$or" ;
const operators =
"=": "$eq",
"!=": "$ne",
"<": "$lt",
"<=": "$lte",
">": "$gt",
">=": "gte",
"in": "$in",
"not in": "$nin",
"contains": "$regex"
;
// Get Mongoose schema type instance of a field
const getSchemaType = (field) =>
return model.schema.paths[field] ? model.schema.paths[field].instance : false;
// Map each rule to a MongoDB query
const mapRule = (rule) =>
let field = rule.field;
let value = rule.value;
if (!value)
value = null;
// Get schema type of current field
const schemaType = getSchemaType(rule.field);
// Check if schema type of current field is ObjectId
if (schemaType === 'ObjectID' && value)
// Convert string value to MongoDB ObjectId
if (Array.isArray(value))
value.map(val => new ObjectId(val));
else
value = new ObjectId(value);
// Check if schema type of current field is Date
else if (schemaType === 'Date' && value)
// Convert string value to ISO date
console.log(value);
value = new Date(value);
console.log(schemaType);
console.log(value);
// Set operator
const operator = operators[rule.operator] ? operators[rule.operator] : '$eq';
// Create a MongoDB query
let mongoDBQuery;
// Check if operator is $regex
if (operator === '$regex')
// Set case insensitive option
mongoDBQuery =
[field]:
[operator]: value,
'$options': 'i'
;
else
mongoDBQuery = [field]: [operator]: value ;
return mongoDBQuery;
const mapRuleSet = (ruleSet) =>
if (ruleSet.rules.length < 1)
return;
// Iterate Rule Set conditions recursively to build database query
return
[conditions[ruleSet.condition]]: ruleSet.rules.map(
rule => rule.operator ? mapRule(rule) : mapRuleSet(rule)
)
;
let mongoDbQuery = mapRuleSet(query);
return mongoDbQuery;
【讨论】:
以上是关于将查询构建器条件转换为 MongoDB 操作,包括嵌套的子文档数组的主要内容,如果未能解决你的问题,请参考以下文章