如何在MongoDB中存储有序的文档集合,而不使用固定集合。

10

如何在MongoDB中存储有序的文档集合?我需要轻松地在任意位置插入文档,并可能在以后重新排序它们。

我可以给每个项分配一个递增的数字并按照该数字排序,或者可以按照_id排序,但是我不知道如何在其他文档之间插入另一个文档。比如说,我想在具有sequence5的元素和sequence6的元素之间插入一些内容?

我的第一个想法是递增所有接下来元素的sequence,从而留出新元素的空间,使用类似于db.items.update({"sequence":{$gte:6}},{$inc:{" sequence ":1}})的查询。 我对数据库管理的了解有限告诉我,这样的查询会很慢,通常是一个坏主意,但我很乐意纠正。

我想我可以将新元素的sequence设置为5.5,但我认为这可能会很快变得混乱。(如果我错了,请纠正我。)

我可以使用带有保证顺序的固定大小集合,但如果需要扩大集合,则会遇到问题。(我对此可能也是错的。)

我可以使每个文档包含对下一个文档的引用,但这将需要为列表中的每个项进行查询。 (您会获取一个项,将其推送到结果数组中,并根据当前项的next字段获取另一个项。)除了明显的性能问题外,我还将无法将排序后的mongo游标传递给我的{#each} spacebars块表达式,并允许它在数据库更改时进行实时更新。(我正在使用全栈javascript框架Meteor。)

我知道每种选择都有它的优势和劣势,也许我只能使用上面列出的其中一种选项,但我想知道是否有更好的方法来做事。

4个回答

8
根据您的需求,其中一种方法是设计模式,使每个文档具有容纳多个文档的能力,并且本身充当一个封顶容器。
{
  "_id":Number,
  "doc":Array
}

每个文档集合将充当一个有限容器,文档将以数组形式存储在“doc”字段中。由于“doc”字段是一个数组,因此它将保持插入顺序。您可以将文档数量限制为“n”。因此每个容器文档的“_id”字段将按“n”递增,表示容器文档可以容纳的文档数。
通过这样做,您可以避免向文档添加额外的字段、额外的索引和不必要的排序。
插入第一条记录时(即文档集合为空时)。
var record = {"name" : "first"};
db.col.insert({"_id":0,"doc":[record]});

插入后续记录

  • 确定最后一个容器文档的_id和它所包含的文档数量number
  • 如果它包含的文档数量小于n,则更新容器文档,并添加新文档;否则,创建一个新的容器文档。

例如,每个容器文档最多可以包含5个文档,我们想要插入一个新的文档。

var record = {"name" : "newlyAdded"};

// using aggregation, get the _id of the last inserted container, and the 
// number of record it currently holds.
db.col.aggregate( [ {
    $group : {
        "_id" : null,
        "max" : {
            $max : "$_id"
        },
        "lastDocSize" : {
            $last : "$doc"
        }
    }
}, {
    $project : {
        "currentMaxId" : "$max",
        "capSize" : {
            $size : "$lastDocSize"
        },
        "_id" : 0
    }
// once obtained, check if you need to update the last container or 
// create a new container and insert the document in it.
} ]).forEach( function(check) {
    if (check.capSize < 5) {
        print("updating");
        // UPDATE
        db.col.update( {
            "_id" : check.currentMaxId
        }, {
            $push : {
                "doc" : record
            }
        });
    } else {
        print("inserting");
        //insert
        db.col.insert( {
            "_id" : check.currentMaxId + 5,
            "doc" : [ record ]
        });
    }
})

请注意,聚合是在服务器端运行的非常高效的操作,还要注意,在版本2.6之前,聚合将返回一个文档而不是一个游标。因此,您需要修改上面的代码,仅从单个文档中进行选择而不是迭代游标。

在文档之间插入新文档

现在,如果您想在文档12之间插入一个新文档,我们知道该文档应该位于具有_id=0的容器内,并且应该放置在该容器的doc数组的second位置。
因此,我们利用$each$position操作符来插入到特定位置。
var record = {"name" : "insertInMiddle"};

db.col.update(
{
    "_id" : 0
}, {
    $push : {
        "doc" : {
            $each : [record],
            $position : 1
        }
    }
}
);

处理溢出

现在,我们需要处理每个容器中的文档溢出问题,比如在_id=0的容器中插入一个新文档。如果该容器已经有5个文档,我们需要将最后一个文档移动到下一个容器,并一直这样做,直到所有容器都容纳了不超过其容量的文档,如果必要,我们需要创建一个容器来容纳溢出的文档。

这个复杂的操作应该在服务器端完成。为了处理这个问题,我们可以创建一个脚本,例如下面的脚本,并将其注册到mongodb中。

db.system.js.save( {
    "_id" : "handleOverFlow",
    "value" : function handleOverFlow(id) {
        var currDocArr = db.col.find( {
            "_id" : id
        })[0].doc;
        print(currDocArr);
        var count = currDocArr.length;
        var nextColId = id + 5;
        // check if the collection size has exceeded
    if (count <= 5)
        return;
    else {
        // need to take the last doc and push it to the next capped 
    // container's array
    print("updating collection: " + id);
    var record = currDocArr.splice(currDocArr.length - 1, 1);
    // update the next collection
    db.col.update( {
        "_id" : nextColId
    }, {
        $push : {
            "doc" : {
                $each : record,
                $position : 0
            }
        }
    });
    // remove from original collection
    db.col.update( {
        "_id" : id
    }, {
        "doc" : currDocArr
    });
    // check overflow for the subsequent containers, recursively.
    handleOverFlow(nextColId);
}
}

为了使得每次在中间插入时,我们可以通过传递容器 ID handleOverFlow(containerId) 调用该函数。

按顺序获取所有记录

只需在聚合管道中使用 $unwind 运算符即可。

db.col.aggregate([{$unwind:"$doc"},{$project:{"_id":0,"doc":1}}]);

重新排序文档

您可以将每个文档存储在带有"_id"字段的封顶容器中:

.."doc":[{"_id":0,","name":"xyz",...}..]..

获取你想要重新排序的封闭容器的“doc”数组。
var docArray = db.col.find({"_id":0})[0];

更新它们的id,以便在排序后,项目的顺序将会改变。

根据它们的_id对数组进行排序。

docArray.sort( function(a, b) {
    return a._id - b._id;
});

使用新的文档数组更新带有容量限制的容器。

但最终,一切都取决于哪种方法最可行并最符合您的要求。

回答您的问题:

在MongoDB中存储一组文档的好方法是什么,在此过程中顺序很重要?我需要轻松地在任意位置插入文档,并可能在以后重新排序它们。

使用文档数组。

比如说,我想在序列为5的元素和序列为6的元素之间插入一些内容怎么办?

db.collection.update()函数中使用$each$position操作符,就像我的回答中所示。

我对数据库管理的理解有限告诉我,这样的查询会变慢,通常不是一个好主意,但我很乐意接受纠正。

是的。除非集合数据非常少,否则会影响性能。

我可以使用带有保证顺序的固定大小的集合,但如果需要扩展集合,则会遇到问题。(再一次,我可能也是错的。)

是的。使用固定大小的集合可能会丢失数据。


哇,感谢您抽出时间撰写如此详细的描述。有一个问题:为什么我需要限制容器文档可以容纳的文档数量?为什么不能将所有文档放在一个父文档的数组中呢? - BonsaiOak
4
MongoDB限制文档的大小为16 MB。此外,将所有文档都倾泻进一个容器会导致在运行某些聚合操作时出现内存溢出问题。通过修复和限制容器中文档的数量,可以根据它们的索引来识别文档。例如,当容器大小为5时,始终保证第4个文档位于第1个容器中,第6个文档位于第2个容器中。在应用层面提前知道这一点可以减少大量编码工作。 - BatScream
太棒了,感谢您详细的回答。此外,“创建一个包含许多文档的文档”的方法非常有趣。 - Abhishek Pathak
值得注意的是,Mongodb不支持对数组内的数组进行原子交互。无论这种风格会带来多大的速度、维护和开发者瓶颈,当您的子文档和子子文档变得更加复杂并且失去插入新数据或原子地获取、放置或删除数据的能力时,您将遇到一些重大障碍。 - Quest

2
在MongoDB中,_id字段是一个唯一且索引的关键字,类似于关系数据库中的主键。如果文档中存在内在的顺序,最好能将一个唯一的关键字与每个文档相关联,并使关键字值反映顺序。因此,在插入文档之前,要明确地添加一个_id字段作为此关键字(如果没有这样做,Mongo会自动创建一个BSON对象ID)。
至于检索结果方面,MongoDB不保证返回文档的顺序,除非您明确使用.sort()。如果不使用.sort(),通常会按照自然顺序(插入顺序)返回结果。同样,这种行为也不能保证。
我建议您在插入时覆盖_id的顺序,并在检索时使用排序。由于_id是必需和自动索引的实体,因此定义排序键并存储其索引不会浪费任何空间。

我该如何使用这个系统在列表中间插入一个项目? - BonsaiOak
我猜你的意思是插入一个新项目并保持顺序不变。这个逻辑最好由你的应用程序处理。如果你使用数字作为键,可以在两个数字之间使用一个新数字。如果使用字母如a、b等,则可以使用a0、a1等。正如我所说,你的应用程序知道固有的顺序,应该由应用程序提供保持顺序的逻辑。 - Abhishek Pathak

1

对于任何集合的任意排序,您需要一个字段来进行排序。我称之为“sequence”。

schema:
{
 _id: ObjectID,
 sequence: Number,
 ...
}

db.items.ensureIndex({sequence:1});

db.items.find().sort({sequence:1})

2
那么我该如何重新排序项目呢?我正在编辑我的问题以澄清这一点。 - BonsaiOak
我会使用经典的BASIC代码排序技巧,将你的序列号设为100的倍数。这样,你就可以在两个现有条目之间插入99个条目,而无需重新排列它们。虽然不完美,但根据你的集合大小,可能会起到作用。 - Will Shaver
1
将您的序列按100的倍数放置,以便在它们之间插入99个项目是不可扩展的,绝对不应该在生产环境中使用。这是技术债务的典型例子。 - Quest

0

这里是一些通用排序数据库答案的链接,可能与您相关:

https://softwareengineering.stackexchange.com/questions/195308/storing-a-re-orderable-list-in-a-database/369754

我建议采用浮点数解决方案 - 添加一个position列:

使用浮点数作为位置列。 然后,您可以仅更改“移动”行中的位置列来重新排序列表。 如果用户想要将“红色”放在“蓝色”之后但在“黄色”之前,则只需计算

red.position = ((yellow.position - blue.position) / 2) + blue.position

在同一位置进行几次重新定位(每次都切成两半)后 - 您可能会遇到障碍 - 最好是达到一定阈值时重新排序列表。

检索时,您只需说col.sort()即可对其进行排序,无需任何客户端代码(例如链表解决方案的情况)


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