在MongoDB中搜索多个集合

50

我了解MongoDB的理论和它不支持联接(join)的事实,也知道应该尽可能使用嵌入式文档或去规范化(denormalize),但是我还是有这样的需求:

我有多个文档,比如:

  • 用户(Users),其中包含了居住区(Suburbs),但也包括:名(first name)、姓(last name)
  • 居住区(Suburbs),其中包含了州(State)
  • 孩子(Child),其中包含学校(School),属于一个用户(User),同时也包括:名(first name)、姓(last name)

例如:

Users:
{ _id: 1, first_name: 'Bill', last_name: 'Gates', suburb: 1 }
{ _id: 2, first_name: 'Steve', last_name: 'Jobs', suburb: 3 }

Suburb:
{ _id: 1, name: 'Suburb A', state: 1 }
{ _id: 2, name: 'Suburb B', state: 1 }
{ _id: 3, name: 'Suburb C', state: 3 }

State:
{ _id: 1, name: 'LA' }
{ _id: 3, name: 'NY' }

Child:
{ _id: 1, _user_id: 1, first_name: 'Little Billy', last_name: 'Gates' }
{ _id: 2, _user_id: 2, first_name: 'Little Stevie', last_name: 'Jobs' }

我需要实现的搜索内容包括:

  • 用户和子用户的名字和姓氏
  • 用户所在的州

我知道我需要执行多个查询来完成它,但如何实现呢?使用MapReduce还是Aggregate?

能否指出一种解决方案?

我尝试使用MapReduce,但无法获得包含state_id的用户文档,这就是我在这里提出的原因。


我认为两者都不是。聚合框架和MapReduce的目标是在单个集合内聚合/汇总数据。类似于连接的操作应该由应用程序而不是数据库处理。顺便说一下 - 您不希望“尽可能去规范化”。 - zero323
您能否编辑问题,以包括您已尝试解决问题的步骤?我不确定您被卡在哪里了。 - WiredPrairie
我其实没有尝试过太多。当我发现mapReduce没有将我的用户文档填充到state_id时,我放弃了继续前进并寻求建议。 - Adrian Istrate
7个回答

44

此答案已过时。自3.2版本以来,MongoDB使用$lookup聚合操作符有限支持左连接查询。

MongoDB不支持跨多个集合进行查询 - 完全不支持。当您需要联接多个集合中的数据时,必须在应用程序级别上执行多个查询。

  1. 查询集合A。
  2. 从结果中获取辅助键,并将其放入数组中。
  3. 通过将该数组作为$in运算符的值来查询集合B。
  4. 在应用程序层面上编程地联接两个查询的结果。

必须这样做通常应该是例外而非常规情况。如果您经常需要模拟JOIN操作,那么可能意味着在设计数据库模式时仍然过于关注关系型数据库,或者您的数据根本不适合MongoDB的文档存储概念。


你好,Philipp。我完全理解了你的逐步解释,但我想知道是否有任何方法可以通过不同的JavaScript函数或存储过程的等效方式直接在MongoDB中执行上述查询。 - Adrian Istrate
1
@AdrianIstrate 你可以使用MongoDB服务器端JavaScript完成所有这些操作。但是,不建议这样做 - Philipp
我能否使用类似Rockmongo的GUI来组织多个查询? - DBWeinstein
这是一个非常有帮助的答案。然而,基于这个逻辑,为什么会有多个集合呢? - DBWeinstein
1
对于我的情况,我们的数据根本不适合文档式存储。但是,无模式设计的便利性以及像扩展、分布和后台索引等操作的便捷性,以及像背景索引等实用工具,都让我们选择了 MongoDB。因此,即使我们需要处理多个查询,我也不可能将同一文档重复写入 2 亿次,而不是仅编写一个 ID 作为辅助键。 - kommradHomer
显示剩余2条评论

25

所以现在在mongodb中可以使用$lookup$facet聚合来实现连接,这可能是在多个集合中查找的最佳方法。

db.collection.aggregate([
  { "$limit": 1 },
  { "$facet": {
    "c1": [
      { "$lookup": {
        "from": Users.collection.name,
        "pipeline": [
          { "$match": { "first_name": "your_search_data" } }
        ],
        "as": "collection1"
      }}
    ],
    "c2": [
      { "$lookup": {
        "from": State.collection.name,
        "pipeline": [
          { "$match": { "name": "your_search_data" } }
        ],
        "as": "collection2"
      }}
    ],
    "c3": [
      { "$lookup": {
        "from": State.collection.name,
        "pipeline": [
          { "$match": { "name": "your_search_data" } }
        ],
        "as": "collection3"
      }}
    ]
  }},
  { "$project": {
    "data": {
      "$concatArrays": [ "$c1", "$c2", "$c3" ]
    }
  }},
  { "$unwind": "$data" },
  { "$replaceRoot": { "newRoot": "$data" } }
])

19
如果您采用反规范化的模式设计架构,那么理解MongoDB会更加容易。也就是说,您需要按照请求客户端应用程序所理解的方式来构建文档结构。本质上,您将文档建模为domain objects,并与应用程序进行交互。当您以这种方式建模数据时,连接变得不那么重要。请考虑如何将您的数据反规范化成单个集合:
{  
    _id: 1, 
    first_name: 'Bill', 
    last_name: 'Gates', 
    suburb: 'Suburb A',
    state: 'LA',
    child : [ 3 ]
}

{ 
    _id: 2, 
    first_name: 'Steve', 
    last_name: 'Jobs', 
    suburb: 'Suburb C',
    state 'NY',
    child: [ 4 ] 
}
{ 
    _id: 3, 
    first_name: 'Little Billy', 
    last_name: 'Gates',
    suburb: 'Suburb A',
    state: 'LA',
    parent : [ 1 ]
}

{
    _id: 4, 
    first_name: 'Little Stevie', 
    last_name: 'Jobs'
    suburb: 'Suburb C',
    state 'NY',
    parent: [ 2 ]
}

第一个优点是这个架构更易于查询。此外,由于地址字段嵌入单个文档中,因此对地址字段的更新现在与单个Person实体一致。还注意到父母和子女之间的双向关系了吗?这使得这个集合不仅仅是个人的集合,而是一个社交图。以下是一些有关MongoDB模式设计的资源,它们可能对您有所帮助。


16

这里是一个JavaScript函数,它将返回符合指定条件的所有记录的数组,在当前数据库中搜索所有集合:

function searchAll(query,fields,sort) {
    var all = db.getCollectionNames();
    var results = [];
    for (var i in all) {
        var coll = all[i];
        if (coll == "system.indexes") continue;
        db[coll].find(query,fields).sort(sort).forEach(
            function (rec) {results.push(rec);} );
    }
    return results;
}

你可以从Mongo shell中复制/粘贴该函数,然后像这样调用:

> var recs = searchAll( {filename: {$regex:'.pdf$'} }, {moddate:1,filename:1,_id:0}, {filename:1} ) > recs


2

基于 @brian-moquin 等人的工作,我编写了一组函数,用于通过简单的关键字搜索整个集合中的整个键(字段)。

这些函数在我的代码片段中,链接为:https://gist.github.com/fkiller/005dc8a07eaa3321110b3e5753dda71b

更详细地说,我首先编写了一个函数来收集所有的键。

function keys(collectionName) {
    mr = db.runCommand({
        'mapreduce': collectionName,
        'map': function () {
            for (var key in this) { emit(key, null); }
        },
        'reduce': function (key, stuff) { return null; },
        'out': 'my_collection' + '_keys'
    });
    return db[mr.result].distinct('_id');
}

然后再根据键数组生成$or查询。
function createOR(fieldNames, keyword) {
    var query = [];
    fieldNames.forEach(function (item) {
        var temp = {};
        temp[item] = { $regex: '.*' + keyword + '.*' };
        query.push(temp);
    });
    if (query.length == 0) return false;
    return { $or: query };
}

以下是一个用于搜索单个集合的函数。
function findany(collection, keyword) {
    var query = createOR(keys(collection.getName()));
    if (query) {
        return collection.findOne(query, keyword);
    } else {
        return false;
    }
}

最后,每个集合都有一个搜索功能。
function searchAll(keyword) {
    var all = db.getCollectionNames();
    var results = [];
    all.forEach(function (collectionName) {
        print(collectionName);
        if (db[collectionName]) results.push(findany(db[collectionName], keyword));
    });
    return results;
}

您可以在Mongo控制台中加载所有函数,然后执行searchAll('任何关键字')

2
您可以使用MongoDB驱动程序中的$mergeObjects来实现这一点。 示例 创建一个具有以下文档的集合订单:
db.orders.insert([
  { "_id" : 1, "item" : "abc", "price" : 12, "ordered" : 2 },
  { "_id" : 2, "item" : "jkl", "price" : 20, "ordered" : 1 }
])

使用以下文档创建另一个集合项:
db.items.insert([
  { "_id" : 1, "item" : "abc", description: "product 1", "instock" : 120 },
  { "_id" : 2, "item" : "def", description: "product 2", "instock" : 80 },
  { "_id" : 3, "item" : "jkl", description: "product 3", "instock" : 60 }
])

以下操作首先使用$lookup阶段通过item字段连接两个集合,然后在$replaceRoot中使用$mergeObjects合并来自items和orders的连接文档:
db.orders.aggregate([
   {
      $lookup: {
         from: "items",
         localField: "item",    // field in the orders collection
         foreignField: "item",  // field in the items collection
         as: "fromItems"
      }
   },
   {
      $replaceRoot: { newRoot: { $mergeObjects: [ { $arrayElemAt: [ "$fromItems", 0 ] }, "$$ROOT" ] } }
   },
   { $project: { fromItems: 0 } }
])

该操作返回以下文档:
{ "_id" : 1, "item" : "abc", "description" : "product 1", "instock" : 120, "price" : 12, "ordered" : 2 }
{ "_id" : 2, "item" : "jkl", "description" : "product 3", "instock" : 60, "price" : 20, "ordered" : 1 }

这种技术将对象合并并返回结果。

0

Minime的解决方案有效,但需要进行修复: var query = createOR(keys(collection.getName()), keyword); 在此处createOR调用中需要添加关键字作为第二个参数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接