如何匹配MongoDB中的多个子文档?

时间:2014-07-31 12:47:13

标签: mongodb

假设我的图书集中有以下数据:

[
    {
        name: "Animal Farm",
        readers: [
            {
                name: "Johny"
            },
            {
                name: "Lisa"
            }
        ],
        likes: [
            {
                name: "Johny"
            }
        ]
    },

    {
        name: "1984",
        readers: [
            {
                name: "Fred"
            },
            {
                name: "Johny"
            },
            {
                name: "Johny",
                type: "bot"
            }
        ],
        likes: [
            {
                name: "Fred"
            }
        ]
    }
]

如何检索所有符合名称" Johny"的读者和喜欢,最终结果如下:

[
    {
        name: "Animal Farm",
        readers: [
            {
                name: "Johny"
            }
        ],
        likes: [
            {
                name: "Johny"
            }
        ]
    },

    {
        name: "1984",
        readers: [
            {
                name: "Johny"
            },
            {
                name: "Johny",
                type: "bot"
            }
        ],
        likes: []
    }
]

无法进行以下查询:

db.books.find(
    { $or: [{ "readers.name": "Johny" }, { "likes.name": "Johny" }] },
    { name: 1, "readers.$": 1, "likes.$": 1 })

MongoDB抱怨以下错误:Cannot specify more than one positional array element per query (currently unsupported).

我曾尝试使用聚合框架,但没有成功。那么MongoDB可以实现这一点,还是必须运行两个查询来检索所需的结果?

2 个答案:

答案 0 :(得分:5)

正如Sammaye已指出的那样,目前不支持指定多个位置数组元素。

但是,您可以使用$elemMatch投影运算符来获得所需的结果。 $ elemMatch投影运算符限制数组的内容以包含与$ elemMatch条件匹配的元素:

db.books.find(
    { $or: [{ "readers.name": "Johny" }, { "likes.name": "Johny" }] }, 
    { 
        readers : { $elemMatch : { name : "Johny" }}, 
        likes : { $elemMatch : { name : "Johny" }}
    }
);

修改

Altough MongoDB没有内置运营商来做你想做的事情,使用现有的运营商,你可以实现你想要的。但是,拥抱自己,这将是一个漫长的过程:

db.books.aggregate([
    // find only documents that have correct "name"
    { $match: { $or: [{ "readers.name": "Johny" }, { "likes.name": "Johny" }]}},
    // unwind the documents so we can push them to a array
    { $unwind: '$likes' },
    // do a group to conditionally push the values into the array
    { $group : { 
        _id : '$_id', 
        likes : { 
            $push : { 
                $cond : [
                    { $eq : ["$likes.name", "Johny"]}, 
                    "$likes", 
                    null
                ]
            }
        },
        readers : { $first : "$readers" }, 
        name : { $first : "$name" }
    }},
    // the process is repeated for the readers array
    { $unwind: '$readers' },
    { $group : { 
        _id : '$_id', 
        readers : { 
            $push : { 
                $cond : [
                    { $eq : ["$readers.name", "Johny"]}, 
                    "$readers", 
                    null
                ]
            }
        },
        likes : { $first : "$likes" }, 
        name : { $first : "$name" }
    }},
    // final step: remove the null values from the arrays
    { $project : {
        name : 1,
        readers : { $setDifference : [ "$readers", [null] ] },
        likes : { $setDifference : [ "$likes", [null] ] },
    }}
]);

如您所见,您可以在$push内使用$cond运算符执行“有条件”$push。但在小组阶段之后,您的数组将包含null个值。您必须使用setDifference过滤掉它们。

另请注意,您需要为正在构建的每个阵列执行展开/分组阶段,否则双重展开将复制文档,并且最终会在阵列中显示重复值。

答案 1 :(得分:3)

来自@ ChristianP的答案:

db.books.aggregate(

    // So we don't have to random do this to docs we don't need to
    {$match: { $or: [{ "readers.name": "Johny" }, { "likes.name": "Johny" }] }},

    {$unwind: '$readers'},
    {$match: { "readers.name": "Johny" }},

    {$unwind: '$likes'},
    {$match: { "likes.name": "Johny" }},

    {$group: {_id: '$_id', likes: {$push: '$likes'}, readers: {$push: '$readers'}}}
)

这样的东西应该能够做你想要的,在查询中执行此操作的功能被避开,以支持这样做。