将查询构建器条件转换为 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。这非常适合为***属性生成查询,例如RecordIDRecordType。这也适用于构建嵌套和/或条件。

不过,我还需要支持查询子文档数组(示例架构中的“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 查询生成器生成的输出。注意:fieldoperator 属性值是可配置的。

"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 操作,包括嵌套的子文档数组的主要内容,如果未能解决你的问题,请参考以下文章

将原始 SQL 转换为 Laravel 查询构建器

求教mongodb大神,在java中怎么以时间为条件查询

如何将数据库查询构建器转换为 Eloquent 模型

将 MySQL 查询转换为 Laravel 查询构建器代码

如何将原生 PHP 转换为查询构建器 Laravel?

mongodb中Criteria转换为es条件