MongoDB里做表间关联

MongoDB 与关系型数据库的建模还是有许多不同,因为 MongoDB 支持内嵌对象和数组类型。MongoDB 建模有两种方式,一种是内嵌 (Embed),另一种是连接 (Link)。那么何时 Embed 何时 Link 呢?那得看两个实体之间的关系是什么类型。

一对一的关系:Embed,比如用户信息集合有 Address 字段,Address 字段有省、市、县三个字段。

在关系型数据库中,通过连接运算符可以实现多个表联合查询。而非关系型数据库的特点是表之间属于弱关联,Mongodb 作为 Nosql 代表,其本身特性不建议对多 Collection 关联处理,不过对于有些需要对多表关联处理的需求,Mongodb 也可以实现。主要分为几种方式:简单手工关联和 DBRef 方式关联、esProc

 

1. 简单手工关联

下图表示帖子和用户两个 Collection 的 ER 图:

首先将 authors 集合中的用户对象查询出来,放在一个变量 author 中,代码如下:

> author=db.authors.findOne({name:"chenzhou"})
{"_id" : ObjectId("5030ba7621bdee44765b2147"),
	"name" : "chenzhou",
	"email" : "chenzhou1025@126.com"
}

通过用户对象 author 来获取帖子列表,代码如下:

> for(var post=db.posts.find({"author_name":author.name}); post.hasNext();){... printjson(post.next().title);
... }
"Hello Mongodb"
"Hello World"
"Hello My Friend"

2.DBRef 方式关联

{$ref : <value>, $id : <value>, $db : <value>}
$ref:集合名称;$id:引用的 id;$db: 数据库名称,可选参数。
可以看到 DBRef 的结构比 Manual References 的复杂,占用的空间大,但是功能也强大,如果要跨数据库连接,上面讲的评论集合的例子,都得需要使用 DBRef,MongoDB 提供了函数来解析 DBRef,不用像 Manual References 需要自己手动写两次查询。

DBRef 就是在两个 Collection 之间定义的一个关联关系,比如,把 CollectionB "_id" 列的值存在 CollectionA 的一个列中,然后通过 CollectionA 这个列中所存的值在 CollectionB 中找到相应的记录。

示例:模拟用户发帖的过程,看一看如何将帖子表和用户表建立关联。

步骤 1:取得当前用户信息,代码如下:

> author=db.authors.find({name:"chenzhou"})[0]
{"_id" : ObjectId("5030ba7621bdee44765b2147"),
	"name" : "chenzhou",
	"email" : "chenzhou1025@126.com"
}

步骤 2:发帖子并做关联,代码如下: 

> db.posts.insert({"title":"Hello Mongodb DBRef1",
... authors:[new DBRef('authors',author._id)]})
> db.posts.insert({"title":"Hello Mongodb DBRef2",
... authors:[new DBRef('authors',author._id)]})
>

步骤 3:通知帖子查找用户信息,代码如下:

>  db.posts.find({"title":"Hello Mongodb DBRef1"})[0].authors[0].fetch()
{"_id" : ObjectId("5030ba7621bdee44765b2147"),
	"name" : "chenzhou",
	"email" : "chenzhou1025@126.com"
}

通过这个例子可以看出,DBRef 就是从文档的一个属性指向另一个文档的指针。

关于 DBRef 详细信息,可以参见官网说明:http://docs.mongodb.org/manual/applications/database-references/ 

 

$lookup

 我们来看 mongodb 另一个非常有意思的东西,那就是 $lookup,我们知道 mongodb 是一个文档型的数据库,而且它也是最像关系型数据库的

一种 nosql,但是呢,既然 mongodb 是无模式的,自然就很难在关系型数据库中非常擅长的多表关联上发挥作用,在这之前,我们可以使用 DbRef,但

是呢,在 mongodb 3.2 中给你增加了一个相当牛逼的手段,那就是 $lookup,而且放到了 aggreation 这种重量级的 pipeline 分析框架上,自然就是一等

公民了,牛逼哈 ~。

$lookup:

db.product.insert({"_id":1,"productname":"商品 1","price":15})
db.product.insert({"_id":2,"productname":"商品 2","price":36})

db.orders.insert({"_id":1,"pid":1,"ordername":"订单 1"})
db.orders.insert({"_id":2,"pid":2,"ordername":"订单 2"})
db.orders.insert({"_id":3,"pid":2,"ordername":"订单 3"})
db.orders.insert({"_id":4,"pid":1,"ordername":"订单 4"})

db.product.find()
db.orders.find()


语法:
db.product.aggregate([ { $lookup: { from: "orders", localField: "_id", foreignField: "pid", as: "inventory_docs" } } ])
然后展示的结果如下:
复制代码
 1 /* 1 */
 2 {
 3     "_id" : 1.0,
 4     "productname" : "商品 1",
 5     "price" : 15.0,
 6     "inventory_docs" : [ 
 7         {
 8             "_id" : 1.0,
 9             "pid" : 1.0,
10             "ordername" : "订单 1"
11         }, 
12         {
13             "_id" : 4.0,
14             "pid" : 1.0,
15             "ordername" : "订单 4"
16         }
17     ]
18 }
19 
20 /* 2 */
21 {
22     "_id" : 2.0,
23     "productname" : "商品 2",
24     "price" : 36.0,
25     "inventory_docs" : [ 
26         {
27             "_id" : 2.0,
28             "pid" : 2.0,
29             "ordername" : "订单 2"
30         }, 
31         {
32             "_id" : 3.0,
33             "pid" : 2.0,
34             "ordername" : "订单 3"
35         }
36     ]
37 }
复制代码

下面我简单介绍一些 $lookup 中的参数:

from:需要关联的表【orders】

localField: 【product】表需要关联的键。

foreignField:【orders】的 matching key。

as:           对应的外键集合的数据,【因为可能是一对多的,对吧】

 

MongoDB 不支持 join,其官网上推荐的 unity jdbc 可以把数据取出来进行二次计算实现 join 运算,但收费版才有这个功能。其他免费的 jdbc drive 只能支持最基本的 SQL 语句,不支持 join。如果用 Java 等编程语言将数据取出后实现 join 计算,也比较复杂。
3、esProc
   用免费的 esProc 配合 MongoDB,可以实现 join 计算。这里通过一个例子来说明一下具体作法。
   MongoDB 中的文档 orders 保存了订单数据,employee 保存了员工数据。如下:
   MongoDB shell version: 2.6.4
   connecting to: test
    > db.orders.find();
    {“_id” : ObjectId(“5434f88dd00ab5276493e270″), “ORDERID” : 1, “CLIENT” : “UJRNP
    ”, “SELLERID” : 17, “AMOUNT” : 392, “ORDERDATE” : “2008/11/2 15:28″ }
    {“_id” : ObjectId(“5434f88dd00ab5276493e271″), “ORDERID” : 2, “CLIENT” : “SJCH”
    , “SELLERID” : 6, “AMOUNT” : 4802, “ORDERDATE” : “2008/11/9 15:28″ }
    {“_id” : ObjectId(“5434f88dd00ab5276493e272″), “ORDERID” : 3, “CLIENT” : “UJRNP
    ”, “SELLERID” : 16, “AMOUNT” : 13500, “ORDERDATE” : “2008/11/5 15:28″ }
    {“_id” : ObjectId(“5434f88dd00ab5276493e273″), “ORDERID” : 4, “CLIENT” : “PWQ”,
    ”SELLERID” : 9, “AMOUNT” : 26100, “ORDERDATE” : “2008/11/8 15:28″ }
    …
    > db.employee.find();
    {“_id” : ObjectId(“5437413513bdf2a4048f3480″), “EID” : 1, “NAME” : “Rebecca”, ”
    SURNAME” : “Moore”, “GENDER” : “F”, “STATE” : “California”, “BIRTHDAY” : “1974-1
    1-20″, “HIREDATE” : “2005-03-11″, “DEPT” : “R&D”, “SALARY” : 7000 }
    {“_id” : ObjectId(“5437413513bdf2a4048f3481″), “EID” : 2, “NAME” : “Ashley”, “S
    URNAME” : “Wilson”, “GENDER” : “F”, “STATE” : “New York”, “BIRTHDAY” : “1980-07-
    19″, “HIREDATE” : “2008-03-16″, “DEPT” : “Finance”, “SALARY” : 11000 }
    {“_id” : ObjectId(“5437413513bdf2a4048f3482″), “EID” : 3, “NAME” : “Rachel”, “S
    URNAME” : “Johnson”, “GENDER” : “F”, “STATE” : “New Mexico”, “BIRTHDAY” : “1970-
    12-17″, “HIREDATE” : “2010-12-01″, “DEPT” : “Sales”, “SALARY” : 9000 }
    …
   Orders 中的 sellerid 对应 employee 中的 eid。需要查询出 employee 的 state 属性等于 California 的所有订单信息。其中 orders 数据量较大,不能一次装入内存。Employee 数据量较小,Orders 过滤之后的结果数据量也比较小。

   查询条件表达式可以作为参数传递给 esProc,如下图:

   A1: 连接 MongoDB 数据库,ip 和端口号是 localhost:27017,数据库是 test,用户名和密码都是 test。
   A2: 使用 find 函数从 MongoDB 中取数,形成游标。集合是 orders,过滤条件是空,指定键 _id 不取出。esProc 在 find 函数中采用了和 mongdb 的 find 语句一样的参数格式。esProc 的游标支持分批读取和处理数据,可以避免数据量过大,内存出现溢出的情况。
   A3: 取得 employee 中的数据。因为数据量不大,所以用 fetch 函数一次取出。
   A4: 使用 switch 函数,将游标 A2 中 SELLERID 字段的值,转换为 A3(employee) 中的记录引用。
   A5: 按照条件过滤。这里使用宏来实现动态解析表达式,其中的 where 就是传入参数。集算器将先计算 ${…} 里的表达式,将计算结果作为宏字符串值替换 ${…} 之后解释执行。这个例子中最终执行的是:=A4.select(SELLERID.STATE==”California”)。由于 SELLERID 已经转化为 employee 的对应记录的引用,所以可以直接写 SELLERID.STATE。过滤之后的结果数据量较小,所以一次取出。如果结果数据量仍然比较大的话,可以分批取出,比如每次取出 10000 条:fetch(10000)。
   A6:将过滤结果中的 SELLERID 重新切换为普通值。
   A6 的计算结果是:

   过滤条件发生变化时不用改变程序,只需改变 where 参数即可。例如,条件变为:state 等于 California 的订单,或者 CLIENT 等于 PWQ 的订单。Where 的参数值可以写为:CLIENT==”PWQ”|| SELLERID.STATE==”California”。
   esProc 并不包含 MongoDB 的 java 驱动包。用 esProc 来访问 MongoDB,必须提前将 MongoDB 的 java 驱动包(esProc 要求 2.12.2 或以上版本的驱动,mongo-java-driver-2.12.2.jar)放到 [esProc 安装目录]\common\jdbc 中。
   esProc 协助 MongoDB 计算的脚本很容易集成到 java 中,只要增加一行 A7,写成 result A6 即可向 java 输出 resultset 形式的结果,具体的代码请参考 esProc 教程。同样,用 java 调用 esProc 访问 MongoDB 也必须将 mongdb 的 java 驱动包放到 java 程序的 classpath 中。