【问题标题】:Node.js mongoose return all non-empty fields on 2nd levelNode.js mongoose 返回第二级的所有非空字段
【发布时间】:2018-04-19 09:58:01
【问题描述】:

想再次向您学习 Node.js 和 mongoose。

我定义了一个猫鼬模式,findOne() 返回一个文档,如下所示。实际文档中的“资源”下还有很多元素。

{
    "metadata": {"isActive": true, "isDeleted": false },
    "test": "123",
    "resource": {
        "id": "59e94f3f6d5789611ce9926f",
        "resourceType": "Patient",
        "active": true,
        "gender": "male",
        "birthDate": "2000-01-01T00:00:00.000Z",
        "extension": [
            {
                "url": "hxxp://example.com/fhir/StructureDefinition/patient-default-bundle",
                "valueCodeableConcept": {
                    "code": "sys",
                    "display": ""
                }
            }
        ],
        "link": [],
        "careProvider": [],
        "communication": [],
        "animal": {
            "genderStatus": {
                "coding": []
            },
            "breed": {
                "coding": []
            },
            "species": {
                "coding": []
            }
        },
        "contact": []
    }
}

问题:如何选择“资源”下的所有非空字段?

我的预期结果如下,即“资源”元素下的所有非空字段。

{
  "id": "59e94f3f6d5789611ce9926f",
  "resourceType": "Patient",
  "active": true,
  "gender": "male",
  "birthDate": "2000-01-01T00:00:00.000Z",
  "extension": [
      {
          "url": "hxxp://example.com/fhir/StructureDefinition/patient-default-bundle",
          "valueCodeableConcept": {
              "code": "sys",
              "display": ""
          }
      }
  ]
}

我目前的编码:

module.exports.findById = function (req, res, next) {
    var resourceId = req.params.resourceId;
    var resourceType = req.params.resourceType;
    var thisModel = require('mongoose').model(resourceType);

    console.log("findById is being called by the API [" + resourceType + "][" + resourceId + "]");
    thisModel.findOne(
        {'resource.id': resourceId, 'metadata.isActive': true, 'metadata.isDeleted': false},
        'resource -_id',
        function(err, doc) {
            if (err) {
                globalsvc.sendOperationOutcome(res, resourceId, "Error", "findOne() Not Found", err, 404);
            }
            else {
                if (doc) {
                    sendJsonResponse(res, 200, doc);
                }  else {
                    delete doc._id;
                    globalsvc.sendOperationOutcome(res, resourceId, "Error", "Id: [" + resourceId + "] Not Found", err, 404);
                }
            }
        }
    );
}

【问题讨论】:

  • 你的意思是所有没有空数组属性的东西?如“返回文档但如果为空则不显示这些属性”?如果这是您的要求,那么它实际上根本不是很简单。最好的情况是根本不存储该属性,除非您有一些数据要放入其中。这比剥离服务器返回的属性要容易得多。
  • 谢谢尼尔,我想要“资源”下的所有内容,这些内容不为空。此外,资源: { } 也需要删除。请看我的预期结果。我同意你的观点,首先不应该存储那些空字段。例如,文档是 { 'resource': { 'id': '123', 'gender': ""}},我的预期结果是 {'id': '123'} 因为 'gender' 为空。
  • 这就是我以为你的意思。这不是一件简单的事情。作为一个“无模式”的面向文档的存储,一般的意图是如果你没有属性的数据,那么你根本不存储它。存储空字符串或空数组实际上是“某事”。并且它需要使用聚合框架进行非常先进的计算密集型投影,以便在返回结果之前“删除”这些投影。所以这里的一般建议是“不要存储空属性”,如果您不希望它们返回。
  • 嗨,尼尔,再次感谢。你说的对。我不应该保存那些空字段。是否有任何好的示例代码可以检查空字段并以通用递归方式删除它们? ;) 我有几个非常复杂和深刻的模式。
  • 嗨,尼尔,不管是否为空,都返回“资源”下的所有字段如何?如何以简单的递归方式提取“资源”下的所有字段?

标签: javascript node.js mongodb mongoose


【解决方案1】:

如前所述,实际上不将空数组存储在 MongoDB 集合中要比在返回数据时尝试处理它们要好得多。您实际上只能通过使用最新版本中的聚合框架功能(然后仍然不递归)从返回的结果中忽略它们,或者允许服务器返回整个对象,然后在传递它们之前从文档中删除这些属性.

所以我真的认为这是修复数据的两步过程。

更改架构以省略空数组

当然,您说架构中有更多字段,但据我所知,我可以给您举几个例子。基本上,您需要在带有undefined 数组的任何东西上放置一个default 值。仅列出一些作为您架构的一部分:

"resource": {
  "extension": {
    "type": [{
      "url": String,
      "valueCodeableConcept": {
        "code": String,
        "display": String
      }
    ],
    "default": undefined
  },
  "link": { "type": [String], "default": undefined },
  "animal": {
    "genderStatus": { 
      "coding": { "type": [String], "default": undefined }
    },
    "breed": {
      "coding": { "type": [String], "default": undefined }
    }
  }
}

这应该会给你一个大致的想法。有了这些"default" 值,猫鼬不会在没有提供其他数据时尝试写入空数组。一旦你通过这样标注每个数组定义来修复你的架构,那么就不会再创建空数组了。

修剪数据

这应该是一个“一次性”操作,以删除仅托管空数组的所有属性。这意味着您还真的想摆脱在每个内部键下只有一个空数组的属性,例如 "animals" 属性。

所以我只是做一个简单的清单来遍历数据重写它:

const MongoClient = require('mongodb').MongoClient;

const uri = 'mongodb://localhost/test',
      collectionName = 'junk';

function returnEmpty(obj) {
  var result = {};

  Object.keys(obj).forEach(k => {
    if ( typeof(obj[k]) === "object" && obj[k].constructor === Object ) {
      let temp = returnEmpty(obj[k]);
      if (Object.keys(temp).length !== 0)
        result[k] = temp;
    } else if ( !((Array.isArray(obj[k]) && obj[k].length > 0)
      || !Array.isArray(obj[k]) ) )
    {
      result[k] = obj[k];
    }
  });

  return result;
}

function stripPaths(obj,cmp) {
  var result = {};

  Object.keys(obj).forEach( k => {
    if ( Object.keys(obj[k]).length !== Object.keys(cmp[k]).length ) {
      result[k] = stripPaths(obj[k], cmp[k]);
    } else {
      result[k] = "";
    }
  });

  return result;
}

function dotNotate(obj,target,prefix) {
  target = target || {};
  prefix = prefix || "";

  Object.keys(obj).forEach( key => {
    if ( typeof(obj[key]) === 'object' ) {
      dotNotate(obj[key], target, prefix + key + '.');
    } else {
      target[prefix + key] = obj[key];
    }
  });

  return target;
}

function log(data) {
  console.log(JSON.stringify(data, undefined, 2))
}

(async function() {

  let db;

  try {

    db = await MongoClient.connect(uri);

    let collection = db.collection(collectionName);

    let ops = [];
    let cursor = collection.find();

    while ( await cursor.hasNext() ) {
      let doc = await cursor.next();
      let stripped = returnEmpty(doc);
      let res = stripPaths(stripped, doc);
      let $unset = dotNotate(res);

      ops.push({
        updateOne: {
          filter: { _id: doc._id },
          update: { $unset }
        }
      });

      if ( ops.length > 1000 ) {
        await collection.bulkWrite(ops);
        ops = [];
      }
    }

    if ( ops.length > 0 ) {
      await collection.bulkWrite(ops);
      log(ops);
      ops = [];
    }


  } catch(e) {
    console.error(e);
  } finally {
    db.close();
  }

})();

这基本上会为集合中的每个文档生成一个操作,将其馈送到bulkWrite()$unset 具有空属性的路径。

对于您提供的文档,更新如下所示:

[
  {
    "updateOne": {
      "filter": {
        "_id": "5a0151108204f6bce9baf86f"
      },
      "update": {
        "$unset": {
          "resource.link": "",
          "resource.careProvider": "",
          "resource.communication": "",
          "resource.animal": "",
          "resource.contact": ""
        }
      }
    }
  }
]

这基本上标识了所有具有空数组的属性,甚至删除了"animal" 下的所有键,因为每个键都有一个空数组,如果我们只删除子,该键将只是一个空对象-钥匙。因此,我们删除了整个密钥及其子密钥。

一旦运行,所有那些不需要的键将从存储的文档中删除,然后任何查询将只返回实际定义的数据。因此,为了长期收益,这只是短期内的一点工作。

处理结果

当然对于懒惰的人,您可以简单地应用用于返回要删除的路径的基本函数,用相反的逻辑从返回的对象中删除路径:

function returnStripped(obj) {
  var result = {};

  Object.keys(obj).forEach(k => {
   if ( typeof(obj[k]) === "object" && obj[k].constructor === Object ) {
     var temp = returnStripped(obj[k]);
     if (Object.keys(temp).length !== 0)
       result[k] = temp;
   } else if ( ((Array.isArray(obj[k]) && obj[k].length > 0) || !Array.isArray(obj[k])) ) {
     result[k] = obj[k];
   }
  });

  return result;
}


db.collection.find().map(returnStripped)

这只是从结果中删除不需要的键。

它可以完成这项工作,但这里更大的收获是实际修复架构和永久更新数据。

【讨论】:

  • 非常感谢!我会实施并测试它!
  • 谢谢尼尔。我使用了“操纵结果”方法,然后将其用于我的用例。效果很好!
  • @Autorun 你可以为所欲为。请注意,那里的“长”描述确实表明您通过保留空键“仍然”返回和“存储”大量不必要的数据。就我个人而言,我喜欢用我的“臀部口袋”,并选择尽可能少地支付数据传输费用。因此,虽然您“可以”在数据返回后对其进行剥离,但您“应该”考虑使您存储的数据符合“仅您需要的数据”。这就是所含建议的重点。
【解决方案2】:

我认为你可以这样做。

thisModel.findOne({ extension: { $gt: [] } })

【讨论】:

  • 谢谢斯里卡。我的查找条件必须是 {'resource.id': resourceId, 'metadata.isActive': true, 'metadata.isDeleted': false}。获得返回的文档后,我想返回文档的子集,例如“resource.*”
猜你喜欢
  • 2021-02-05
  • 2018-06-17
  • 2017-08-18
  • 2023-04-06
  • 1970-01-01
  • 2019-03-12
  • 2014-04-04
  • 1970-01-01
  • 2021-10-19
相关资源
最近更新 更多