MongoDB query subset of array of DBRefs

qum*_*uma 1 mongodb mongodb-query aggregation-framework

My main object looks like this:

{
"_id" : ObjectId("56eb0a06560fd7047318465a"),
 ...
"intervalAbsenceDates" : [
    DBRef("ScheduleIntervalContainer", ObjectId("56eb0a06560fd7047318463b")),
    DBRef("ScheduleIntervalContainer", ObjectId("56eb0a05560fd70473184467")),
    DBRef("ScheduleIntervalContainer", ObjectId("56eb0a05560fd70473184468")),
    DBRef("ScheduleIntervalContainer", ObjectId("56eb0a05560fd70473184469")),
Run Code Online (Sandbox Code Playgroud)

An embedded ScheduleIntervalContainer object looks like this:

{
"_id" : ObjectId("56eb0a06560fd7047318463b"),
"end" : ISODate("2022-08-23T07:06:00Z"),
"available" : true,
"confirmation" : true,
"start" : ISODate("2022-08-19T09:33:00Z")
}
Run Code Online (Sandbox Code Playgroud)

Now I will query all ScheduleIntervalContainers where start and end is in a range. I have tried a lot but I not even can query one ScheduleIntervalContainer by id. This is my approach:

db.InstitutionUserConnection.find( { 
"intervalAbsenceDates" : { 
    "$ref" : "ScheduleIntervalContainer", 
    "$id" : ObjectId("56eb0a05560fd7047318446d")
}
})
Run Code Online (Sandbox Code Playgroud)

Could anyone give me a hint how to query all ScheduleIntervalContainers which have start and end in a time range.

Bla*_*ven 5

使用DBRef充满了问题,而且用法有点“过时”,因为它更像是一种“鞋拔式”解决方案,以提供一种机制来提供对外部集合数据的引用,而不是一个真正经过深思熟虑的解决方案。

一般建议是不要使用DBRef,而是简单地使用普通ObjectId或其他唯一标识符,并使用其他信息解析实际的“集合”甚至“数据库容器”,作为存储在文档中的另一个标准属性或只是一个普通的外部引用您标识目标集合的代码。

基本的 BSON 问题

这样做的一个很好的原因是人们经常犯下常见的错误(就像您一样)将包含 a 的存储对象的“序列化”输出解释DBRef为实际存在于对象上的“属性”。这不是真的,因为实际上对象有它自己的 BSON 类型,就像Date和一样ObjectId。序列化形式唯一有效的时间是与“严格模式”JSON 解析器一起使用时,这将产生实际DBRef对象。

该手册本身不符合这一事实特别有帮助,并说:“有DBREFS以下字段”。这是令人误解,因为这些特性是不实际可用作为查询字段,并且不会暴露比可用来的JavaScript的处理对象的检查其它$where或mapReduce。并且不是将其中任何一个用于“查询”目的的好方法。

因此这些属性不可用于查询,但您当然可以直接从您的 API 指定 BSON 表单。如:

db.InstitutionUserConnection.find({ 
    "intervalAbsenceDates": DBRef(
        "ScheduleIntervalContainer",  
        ObjectId("56eb0a05560fd70473184469")
    ) 
})
Run Code Online (Sandbox Code Playgroud)

由于在查询中发送了正确的 BSON 表单并且元素实际上将匹配,因此可以正确解析和匹配。

根据这个原则,我们可以继续讨论问题中提出的“范围”的概念。

MongoDB 并没有“真正”做连接

直到最近的版本(MongoDB的3.2.x中系列)的声明实际上是“MongoDB的确实不是做连接”,这一直是设计理念的一般区别远离关系数据库。

一般的口头禅“一直”是“联接成本高昂”,因此在分布式数据系统(例如 MongoDB 的主要设计目的)中不能很好地扩展。

因此,如果您要求在引用的文档中引用属性DBRef,那么您基本上不走运。在这种情况下,基于外部属性过滤结果的唯一可能操作是:

  1. 查看主集合中的所有数据,要么整体加载查询,要么单独处理

  2. 对于每个检索到的文档,查找DBRef值并将其扩展到其目标集合数据。

  3. 过滤掉外部引用的扩展数据实际上不符合条件的文档。

这意味着所有的“扩展”和“过滤”都必须在数据库的“客户端”上进行,而不是在服务器本身上进行。根本没有机制可以做到这一点,因此您最终会通过网络连接提取大量数据。

有了DBRef适当的地方,即使使用现代版本,这也完全不可能在服务器上执行。同样是 BSON 类型问题,因为“源”包含DBRef类型而“目标”包含ObjectId类型。

但是,如果您可以简单地接受这样一个事实,即您的“范围”正在查看any 中 固有的“创建日期”数据ObjectId,那么当然还有另一种不涉及“加入”的方法。

过滤 ObjectId“范围”

每个ObjectId以 4 字节开头,表示创建时的当前时间戳值(不包括毫秒)ObjectId。这通常是有关文档在何处使用的“插入”时间的良好指标。

由此您可以确定所引用文档的“创建日期”DBRef大约等于ObjectId目标集合中该文档使用的那部分值。这允许您基本上为ObjectId落在给定范围之间的值构建一个“范围”值。因此,您可以构造可DBRef与范围运算符一起使用的 BSON 对象:

// Define start and end dates to query
var dateStart = new Date("2016-03-17T19:48:21Z"), // equal to 56eb0a05
    dateEnd = new Date("2016-03-17T19:48:25Z");   // equal to 56eb0a09

// Convert to hex and pad to ObjectId length
var startRange = new ObjectId(
    ( dateStart.valueOf() / 1000 ).toString(16) + "0000000000000000"
),
// Yields ObjectId("56eb0a050000000000000000")
endRange = new ObjectId(
    ( dateEnd.valueOf() / 1000 ).toString(16) + "ffffffffffffffff"
);
// Yields ObjectId("56eb0a09ffffffffffffffff")

// Now query with contructed DBRef values

db.InstitutionUserConnection.find({ 
    "intervalAbsenceDates": {
        "$elemMatch": {
            "$gte": DBRef("ScheduleIntervalContainer",startRange),
            "$lt": DBRef("ScheduleIntervalContainer",endRange),
        }
    }
})
Run Code Online (Sandbox Code Playgroud)

因此,只要“创建”是您要查找的内容,那么该方法就足以选择匹配的父项,而无需先扩展DBRef数组中的值以进行进一步检查。

反向案例查找

当然,这里的另一种情况是简单地首先查询“加盟”集合,然后寻找在“大师”集合包含文档ObjectId中的值DBRef。这当然意味着要发出多个查询,但它确实解决了扩展 eachDBRef以匹配相关属性的情况:

// Create array of matching DBRef values
var refs = db.ScheduleIntervalContainer.find({
    "start" { "$lte": targetDate },
    "end": { "$gte": targetDate }
}).map(function(doc) {
    return DBRef("ScheduleIntervalContainer",doc._id)
});

// Find documents that match the DBRef's within the array
db.InstitutionUserConnection.find({ 
    "intervalAbsenceDates": { "$in": refs } 
})
Run Code Online (Sandbox Code Playgroud)

其实用性因相关集合中匹配的数量而异,从而导致将传递给 的数组$in,但它实际上确实产生了所需的结果。

实际上做连接

我之前提到过,“现代”MongoDB 版本现在有一种方法可以“连接”来自不同集合的数据。这是$lookup聚合管道操作符。

但是虽然这可以用来“加入”数据,但当前的用法在DBRef这里不起作用。我之前也提到过,基本问题是数组中的数据是 a DBRef,但引用集合中的数据是 a ObjectId。

因此,如果您想使用一种$lookup方法,那么您首先需要使用普通ObjectId值代替现有DBRef值:

{
  "_id" : ObjectId("56eb0a06560fd7047318465a"),
  "intervalAbsenceDates" : [
    ObjectId("56eb0a06560fd7047318463b"),
    ObjectId("56eb0a05560fd70473184467"),
    ObjectId("56eb0a05560fd70473184468"),
    ObjectId("56eb0a05560fd70473184469")
  ]
}
Run Code Online (Sandbox Code Playgroud)

有了该结构中的数据,您就可以使用$lookup和其他聚合管道方法来返回实际匹配相关属性值的文档。即"end"在相关对象中:

 db.InstitutionUserConnection.aggregate([
     // Presently you need to unwind the array first
     { "$unwind": "$intervalAbsenceDates" },

     // Then $lookup to get a resulting array of matches for each member
     { "$lookup": {
         "from": "ScheduleIntervalContainer",
         "localField": "intervalAbsenceDates",
         "foreignField": "_id",
         "as": "absenceDates"
     }},

     // unwind the array result field as well
     { "$unwind": "$absenceDates" },

     // Now reform the documents
     { "$group": { 
         "_id": "$_id",
         "intervalAbsenceDates": { "$push": "$absenceDates" }
     }},

     // Then query on the "end" property for the range
     { "$match": { 
         "intervalAbsenceDates": {
             "$elemMatch": {
                 "end": {
                     "$gte": new Date("2016-03-23"),
                     "$lt": new Date("2016-03-24")
                 }
             }
         }
     }}
 ])
Run Code Online (Sandbox Code Playgroud)

的当前行为$lookup是您无法直接处理文档中的数组属性,因此使用“数组中 ObjectId 的 $lookup”中所示的过程将当前数组替换为其他集合中的扩展对象。

一旦此处的操作实际生成了一个现在嵌入了相关数据的文档,那么查看数组中文档的属性以查看它们是否与查询条件匹配就是一个简单的过程。

结论

这一切都应该表明这DBRef不是存储引用的好主意。正如所演示的那样,虽然可以使用这些ObjectId值解决问题,但您通常希望将普通ObjectId或其他键值作为参考,并通过其他方式解决它们。即使解决方法足够了,它对于普通ObjectId值或任何其他呈现自然范围的东西也一样。

当谈到在这样的“连接”中使用引用属性的“值”时,当然不管使用DBRef或其他值,$lookup在服务器上的查询条件中不使用它是不可能的。首先需要将所有数据加载到客户端,然后通过对数据库的其他查询进行解析,然后才能检查这些属性以进行过滤。

由于$lookupwill的机制实际上会导致一种形式,如果您首先“嵌入”数据,那么“嵌入”通常是正确的方法,因为数据已经存在于源集合中并且可供查询。

关于 16MB 的 BSON 限制,有很多“恐慌媒体”,并说这就是将数据保存在另一个集合中的原因。有时这确实适用,但大多数时候并不适用。毕竟,16MB 确实是相当大的数据量,而且比大多数在一般应用程序中实际使用的要多。

来自MongoDB 的引用:权威指南

为了让您了解 16MB 的大小,战争与和平的整个文本只有 3.14MB。

要检查查询到需要去“嵌入形式”无论如何,这是值得商榷的,如果你可以存储阵列DBRef或ObjectId或任何嵌入的数据,然后存储所有他们实际上指向的内容是不是真的那么更多的是一个延伸。

一般的教训是,您应该根据您的应用程序应用于数据的实际使用模式进行设计。如果您一直在查询“相关数据”,那么将这些数据全部保存在一个集合中是最有意义的。当然,其他因素也适用,但请始终牢记您正在执行的操作对性能考虑的权衡。