原文地址

接上一篇

四、模型树结构

父引用的模型树结构

这个数据模型描述了一个树形结构,在子节点中存储父节点的引用。

模式

父引用模式存储每个树节点到文档中,除了树节点外,文档还存储了父节点的id。

考虑以下目录的层级关系。

以下为应用实例

db.categories.insert( { _id: "MongoDB", parent: "Databases" } )
db.categories.insert( { _id: "dbm", parent: "Databases" } )
db.categories.insert( { _id: "Databases", parent: "Programming" } )
db.categories.insert( { _id: "Languages", parent: "Programming" } )
db.categories.insert( { _id: "Programming", parent: "Books" } )
db.categories.insert( { _id: "Books", parent: null } )

查询一个节点的父节点变得快速且直观

db.categories.findOne( { _id: "MongoDB" } ).parent

还可以对parent字段创建索引以提高查询速度

db.categories.createIndex( { parent: 1 } )

通过parent字段查询其直接子节点

db.categories.find( { parent: "Databases" } )

这种父引用模式是实现树存储的简单方法,缺点是获取子树时则需要多个查询。

子引用的模型树结构

这个数据模型描述了树形结构,存储子节点的引用到父节点中。

模式

子引用模式存储每个树节点到一个文档中,除了树节点,文档还存储了包含其子节点id的数组。

考虑与上一个图中相同的目录层级关系,其子引用模式的实现如下

db.categories.insert( { _id: "MongoDB", children: [] } )
db.categories.insert( { _id: "dbm", children: [] } )
db.categories.insert( { _id: "Databases", children: [ "MongoDB", "dbm" ] } )
db.categories.insert( { _id: "Languages", children: [] } )
db.categories.insert( { _id: "Programming", children: [ "Databases", "Languages" ] } )
db.categories.insert( { _id: "Books", children: [ "Programming" ] } )

查询获取一个节点的直接子节点则变得快速且直观

db.categories.findOne( { _id: "Databases" } ).children

创建children字段上的索引以实现快速查询

db.categories.createIndex( { children: 1 } )

通过children信息查询其父节点以及兄弟节点信息

db.categories.find( { children: "MongoDB" } )

子引用模式提供了一种树存储的合适方案,只要不需要对子树操作就行(单个查询无法保证获取一个节点的所有子节点)。对于存储图像,其中一个节点可能有多个父节点,那这个模式也是很好的方案。

祖先数组的模型树结构

这个数据模型描述了树形结构,使用对父节点的引用和一个数组来存储所有的祖先节点。

模式

文档中除了存储每个树节点,还存储了一个数组,其他暴露了所有祖先节点的id或者路径(祖先节点的id按顺序排序组成路径)。

考虑跟上面图中相同的目录层级关系

以下实际实例中,文档除了ancestors字段,还有parent字段,parent字段存储了直接父节点的引用。

db.categories.insert( { _id: "MongoDB", ancestors: [ "Books", "Programming", "Databases" ], parent: "Databases" } )
db.categories.insert( { _id: "dbm", ancestors: [ "Books", "Programming", "Databases" ], parent: "Databases" } )
db.categories.insert( { _id: "Databases", ancestors: [ "Books", "Programming" ], parent: "Programming" } )
db.categories.insert( { _id: "Languages", ancestors: [ "Books", "Programming" ], parent: "Programming" } )
db.categories.insert( { _id: "Programming", ancestors: [ "Books" ], parent: "Books" } )
db.categories.insert( { _id: "Books", ancestors: [ ], parent: null } )

查询获取一个节点的祖先节点或者路径则变得快速而直观

db.categories.findOne( { _id: "MongoDB" } ).ancestors

对ancestors字段创建索引提高查询速度

db.categories.createIndex( { ancestors: 1 } )

查询ancestors字段以查找它的所有后代节点

db.categories.find( { ancestors: "Programming" } )

祖先数组模式提供了查询某节点的后代节点以及某节点的祖先节点的有效方案,所以当需要对子树节点进行操作时,这个模式是一个很好的选择。

祖先数组模式比具体化路径(Materialized Paths)模式稍慢,但是使用更直观。

具体化路径(Materialized Paths)的模型树结构

这个数据模型描述了树形结构,存储文档间的全关系路径。

模式

具体化路径中,文档除了存储树节点之外,还存储了节点的祖先的id或者路径。尽管具体化路径模式要求额外的步骤处理字符串和正则表达式,这个模式也提供了处理路径的灵活性,如通过部分路径查找节点。

考虑与上面的图中相同的目录层级关系

具体化路径模式中,存储路径到path字段中,路径使用逗号作为分隔符

db.categories.insert( { _id: "Books", path: null } )
db.categories.insert( { _id: "Programming", path: ",Books," } )
db.categories.insert( { _id: "Databases", path: ",Books,Programming," } )
db.categories.insert( { _id: "Languages", path: ",Books,Programming," } )
db.categories.insert( { _id: "MongoDB", path: ",Books,Programming,Databases," } )
db.categories.insert( { _id: "dbm", path: ",Books,Programming,Databases," } )

通过path字段查询所有的节点

db.categories.find().sort( { path: 1 } )  // 按path的升序

对path字段使用正则表达式查找Programming的后代

db.categories.find( { path: /,Programming,/ } ) // 路径包含",Programming,"

查找Books的后代,因为Books是最高节点,故正则表达式以",Books,"开头

db.categories.find( { path: /^,Books,/ } )

对path字段创建索引

db.categories.createIndex( { path: 1 } )

根据具体查询,这个索引可能会提高性能:

  • 对于根节点Books的子树上的查询(例如,/^,Books,/ 或者 /^,Books,Programming,/),path字段的索引会明显提高查询性能。
  • 对于未提供到根节点的路径的子树上的查询(例如, /,Databases,/),或者类似的子树查询,节点在加索引的字符串的中间,这时查询必须扫描所有索引。对这些查询来说,如果索引比整个集合小很多,则索引会提高查询性能。

内嵌集(Nested Sets)的模型树结构

这个数据模型描述了树形结构,优化了查找子树的性能,但是也会导致树的易变性的增加。

模式

内嵌集模式对树作一个往返遍历并标示树中每个节点为停留点。应用程序对每个节点访问两次,第一次为去往遍历,第二次为返回遍历。内嵌集模式中文档除了存储树节点,还存储了其父节点的id,存储其去往停留点到left字段,以及存储返回停留点到right字段。

考虑如下目录的层级关系

下面代码演示了内嵌集的实现

db.categories.insert( { _id: "Books", parent: 0, left: 1, right: 12 } )
db.categories.insert( { _id: "Programming", parent: "Books", left: 2, right: 11 } )
db.categories.insert( { _id: "Languages", parent: "Programming", left: 3, right: 4 } )
db.categories.insert( { _id: "Databases", parent: "Programming", left: 5, right: 10 } )
db.categories.insert( { _id: "MongoDB", parent: "Databases", left: 6, right: 7 } )
db.categories.insert( { _id: "dbm", parent: "Databases", left: 8, right: 9 } )

查询获取一个节点的后代

var databaseCategory = db.categories.findOne( { _id: "Databases" } );
db.categories.find( { left: { $gt: databaseCategory.left }, right: { $lt: databaseCategory.right } } );

内嵌集模式提供了为查找子树的一个快速并有效率的方案,但是在修改树结构时较为麻烦。故这种模式适合不会改变结构的静态树。

五、模型特定的程序上下文

原子操作的模型数据

MongoDB中的写操作,例如db.collection.update(),db.collection.findAndModify(),db.collection.remove()在单个文档级别是原子的。对于那些需要一起被更新的字段,将字段嵌入到文档中可以确保对这些字段的更新是原子的。

例如,考虑这样一种情况,当需要维护书的信息时,包括可以检出的书数量以及当前检出信息,如何确定这两个操作整体的原子性?

可获得的书和检出信息必须同步。这样,将available字段和checkout字段嵌入相同的文档以确保更新文档是原子性的。

{
    _id: 123456789,
    title: "MongoDB: The Definitive Guide",
    author: [ "Kristina Chodorow", "Mike Dirolf" ],
    published_date: ISODate("2010-09-24"),
    pages: 216,
    language: "English",
    publisher_id: "oreilly",
    available: 3,
    checkout: [ { by: "joe", date: ISODate("2012-10-15") } ]
}

那么,根据新的检出信息,可以使用db.collection.update()进行更新,并且对available字段和checkout字段的更新是原子性的。

db.books.update (
   { _id: 123456789, available: { $gt: 0 } },  //查询条件
   {
     $inc: { available: -1 },    //available字段减1
     $push: { checkout: { by: "abc", date: new Date() } }  // checkout字段数组增加一个元素
   }
)

以上操作返回一个WriteResult()对象,包含了操作的有关信息

WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

nMatched字段表明1个文档匹配这个更新条件,nModified表明这个操作更新了一个文档。

支持关键字查询的模型数据

关键字搜索与文本搜索或者全文搜索不同,它不提供词干提取或者其他文本处理特性。

此模式描述了支持关键字搜索的方法,以支持程序搜索功能,这个搜索方法使用存储在相同文档中的数组中的关键字,而这个数组作为这个文档的文本字段。与多键索引结合后,这个模式可以支持程序的关键字搜索操作。

模式

为了向文档中增加结构以支持基于关键字的查询,首先向文档中创建一个数组字段,并将关键字以字符串形式添加到数组中。然后可以对这个数组创建多键索引,并创建查询从这个数组中选择数值。

实例:

给定一个图书卷的集合,并想提供一个机遇主题的搜索。对每个卷而言,我们增加topics数组,并尽可能的对这个卷添加关键字到数组中。对Moby-Dick卷可以由如下文档表示,

{ title : "Moby-Dick" ,
  author : "Herman Melville" ,
  published : 1851 ,
  ISBN : 0451526996 ,
  topics : [ "whaling" , "allegory" , "revenge" , "American" ,
    "novel" , "nautical" , "voyage" , "Cape Cod" ]
}

然后对topics数组创建多键索引

db.volumes.createIndex( { topics: 1 } )

多键索引对topics数组中的每个关键字创建索引项。例如,索引中有一个是关于"whaling"的索引项,另一个是关于"allegory"的索引项。

然后基于关键字的查询示例如下

db.volumes.findOne( { topics : "voyage" }, { title: 1 } )

提示:数组中的元素数量很大时,如有几百或上千的关键字,那插入操作会导致加索引花费很大。

关键字索引的限制

使用特定的数据模型和多键索引,MongoDB可以支持关键字搜索。然而,这些关键字索引在以下方面与全文搜索比较则显得不足或者无法相比:

  • 词干提取。关键字查询无法解析关键字为根或者有关词语。
  • 同义。关键字搜索特性必须在应用层提供同义支持或相关查询。
  • 排序。关键字查询不提供判断结果权重的方式。
  • 异步索引。MongoDB同步创建索引,这意味着为关键字使用的索引总是处于当前的并可实时操作。然而,异步创建的索引在某种内容和工作负荷下效率更高。