【问题标题】:Convert query builder conditions to MongoDB operations including nested array of subdocuments将查询构建器条件转换为 MongoDB 操作,包括嵌套的子文档数组
【发布时间】: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 }

标签: node.js mongodb nosql


【解决方案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;

}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-09
    • 1970-01-01
    • 1970-01-01
    • 2020-11-28
    • 2019-02-20
    • 2021-02-25
    相关资源
    最近更新 更多