千家信息网

简化 MongoDB 关联运算

发表于:2024-11-22 作者:千家信息网编辑
千家信息网最后更新 2024年11月22日,MongoDB属于 NoSql 中的基于分布式文件存储的文档型数据库,这种bson格式的文档结构,更加贴近我们对物体各方面的属性描述。而在使用 MongoDB 存储数据的过程中,有时候难免需要进行关联
千家信息网最后更新 2024年11月22日简化 MongoDB 关联运算

MongoDB属于 NoSql 中的基于分布式文件存储的文档型数据库,这种bson格式的文档结构,更加贴近我们对物体各方面的属性描述。而在使用 MongoDB 存储数据的过程中,有时候难免需要进行关联表查询。自从 MongoDB 3.2 版本后,它提供了 $lookup 进行关联表查询,让查询功能改进了不少。但在实现应用场景中,所遇到的环境错综复杂,问题解决也非易事,脚本书写起来也并不简单。好在有了集算器 SPL 语言的协助,处理起来就相对容易多了。
本文我们将针对 MongoDB 在关联运算方面的问题进行讨论分析,并通过集算器 SPL 语言加以改进,方便用户使用 MongoDB。讨论将分为以下几个部分:
1. 关联嵌套结构情况 1…………………………………………….. 1
2. 关联嵌套结构情况 2…………………………………………….. 3
3. 关联嵌套结构情况 3…………………………………………….. 4
4. 两表关联查询………………………………………………………. 6
5. 多表关联查询………………………………………………………. 8
6. 关联表中的数组查找…………………………………………… 10
Java 应用程序调用 DFX 脚本…………………………………… 12

1.关联嵌套结构情况1

两个关联表,表 A 与表 B 中的内嵌文档信息关联, 且返回的信息在内嵌文档中。表 childsgroup 字段 childs 是嵌套数组结构,需要合并的信息 name 在其下。

测试数据:

history:

_ididHistorychild_id
1001today workedch001
2002Workingch004
3003now workingch009

childsgroup:

_idgidnamechilds
1g001group1{"id":"ch001","info":{"name":"a",mobile:1111}},{"id":"ch002","info":{"name":"b",mobile:2222}}
2g002group1{"id":"ch004","info":{"name":"c",mobile:3333}},{"id":"ch009","info":{"name":"d",mobile:4444}}

表History中的child_id与表childsgroup中的childs.id关联,希望得到下面结果:

{
"_id" : ObjectId("5bab2ae8ab2f1bdb4f434bc3"),
"id" : "001",
"history" : "today worked",
"child_id" : "ch001",
"childInfo" :
{
"name" : "a",
" mobile" : 1111
}
………………
}


Mongo 脚本

db.history.aggregate([
{$lookup: {
from: "childsgroup",
let: {child_id: "$child_id"},
pipeline: [
{$match: { $expr: { $in: [ "$$child_id", "$childs.id"] } } },
{$unwind: "$childs"},
{$match: { $expr: { $eq: [ "$childs.id", "$$child_id"] } } },
{$replaceRoot: { newRoot: "$childs.info"} }
],
as: "childInfo"
}},
{"$unwind": "$childInfo"}
])

这个脚本用了几个函数lookup、pipeline、match、unwind、replaceRoot处理,一般 mongodb 用户不容易写出这样复杂脚本;那么我们再看看 spl 脚本是如何实现的:


SPL脚本 ( 文件名:childsgroup.dfx)


AB
1=mongo_open("mongodb://127.0.0.1:27017/raqdb")
2=mongo_shell(A1,"history.find()").fetch()
3=mongo_shell(A1,"childsgroup.find()").fetch()
4=A3.conj(childs)
5=A2.join(child_id,A4:id,info)
6>A1.close()

关联查询结果:

_ididhistorychild_idinfo
1001today workedch001[a,1111]
2002workingch004[c,3333]
3003now workingch009[d,4444]

脚本说明:
A1:连接 mongodb 数据库。
A2:获取 history 表中的数据。
A3:获取 childsgroup 表中的数据。
A4:将 childsgroup 中的 childs 数据提取出来合并成序表。
A5:表 history 中的 child_id 与表 childs 中的 id 关联查询,追加 info 字段, 返回序表。
A6:关闭数据库连接。


相对 mongodb 脚本写法,SPL 脚本的难度降低了不少,思路也更加清晰,也不需要再去熟悉有关 mongo 函数的用法,以及如何去组合处理数据等,节约了不少时间。

2.关联嵌套结构情况 2

两个关联表,表 A 与表 B 中的内嵌文档信息关联, 将信息合并到内嵌文档中。表 txtPost 字段 comment 是嵌套数组结构,需要把 comment_content 合并到其下。


txtComment:

_IDcomment_nocomment_content
1143test test
2140math

txtPost

_IDpost_noComment
148[{"comment_no" : 143, "comment_group" : 1} ]
247[{"comment_no" : 140, "comment_group" : 2},
{"comment_no" : 143, "comment_group" : 3} ]


期望结果:

_IDpost_noComment
148[{"comment_no" : 143, "comment_group" : 1,"comment_content" : "test test"} ]
247[{"comment_no" : 140, "comment_group" : 2,"comment_content" : "math"},
{"comment_no" : 143, "comment_group" : 3,"comment_content" : "test test"} ]


Mongo 脚本

db.getCollection("txtPost").aggregate([
{ "$unwind": "$comment"},
{ "$lookup": {

"from": "txtComment",
"localField": "comment.comment_no",
"foreignField": "comment_no",
"as": "comment.comment_content"
}},
{ "$unwind": "$comment.comment_content"},
{ "$addFields": { "comment.comment_content":
"$comment.comment_content.comment_content" }},
{ "$group": {
"_id": "$_id",
'post_no':{"$first": "$post_no"},
"comment": {"$push": "$comment"}
}},
]).pretty()

表txtPost 按 comment 拆解成记录,然后与表 txtComment 关联查询,将其结果放到数组中,再将数组拆解成记录,将comment_content 值移到 comment 下,最后分组合并。


SPL 脚本:


AB
1=mongo_open("mongodb://127.0.0.1:27017/raqdb")
2=mongo_shell(A1,"txtPost.find()").fetch()
3=mongo_shell(A1,"txtComment.find()").fetch()
4=A2.conj(comment.derive(A2.post_no:pno))
5=A4.join(comment_no,A3:comment_no,comment_content:Content)
6=A5.group(pno;~:comment)
7>A1.close()

关联查询结果:

pnoComment
47[[ 140, 2,47, …],[143, 3,47, …] ]
48[[143, 1,48, …]]


脚本说明:
A1:连接 mongodb 数据库。
A2:获取 txtPost 表中的数据。
A3:获取 txtComment 表中的数据。
A4:将序表 A2 下的 comment 与 post_no 组合成序表,其中 post_no 改名为 pno。
A5:序表 A4 通过 comment_no 与序表 A3 关联,追加字段 comment_content,将其改名为 Content。
A6:按 pno 分组返回序表,~ 表示当前记录。
A7:关闭数据库连接。

Mongo、SPL 脚本实现方式类似,都是把嵌套结构的数据转换成行列结构的数据,再分组合并。但 SPL 脚本的实现更简单明了。

3.关联嵌套结构情况 3

两个关联表,表 A 与表 B 中的内嵌文档信息关联, 且返回的信息在记录上。表 collection2 字段 product 是嵌套数组结构,返回的信息是 isCompleted 等字段 。


测试数据:
collection1:
{
_id: '5bc2e44a106342152cd83e97',
description
{
status: 'Good',
machine: 'X'
},
order: 'A',
lot: '1'
};

collection2:
{
_id: '5bc2e44a106342152cd83e80',
isCompleted: false,
serialNo: '1',
batchNo: '2',
product: [ // note the subdocuments here
{order: 'A', lot: '1'},
{order: 'A', lot: '2'}
]
}


期待结果
{
_id: 5bc2e44a106342152cd83e97,
description:
{
status: 'Good',
machine: 'X',
},
order: 'A',
lot: '1' ,
isCompleted: false,
serialNo: '1',
batchNo: '2'
}


Mongo 脚本

db.collection1.aggregate([{
$lookup: {

from: "collection2",
let: {order: "$order", lot: "$lot"},
pipeline: [{
$match: {
$expr:{ $in: [ { order: "$$order", lot: "$$lot"}, "$product"] }
}
}],
as: "isCompleted"
}
}, {
$addFields: {
"isCompleted": {$arrayElemAt: [ "$isCompleted", 0] }
}
}, {
$addFields: { // add the required fields to the top level structure
"isCompleted": "$isCompleted.isCompleted",
"serialNo": "$isCompleted.serialNo",
"batchNo": "$isCompleted.batchNo"
}
}])

lookup 两表关联查询,首个 addFields获取isCompleted数组的第一个记录,后一个addFields 转换成所需要的几个字段信息


SPL脚本:


AB
1=mongo_open("mongodb://127.0.0.1:27017/raqdb")
2=mongo_shell(A1,"collection1.find()").fetch()
3=mongo_shell(A1,"collection2.find()").fetch()
4=A3.conj(A2.select(order:A3.product.order,lot:A3.product.lot).derive(A3.serialNo:sno,A3.batchNo:bno))
5>A1.close()

脚本说明:
A1:连接 mongodb 数据库。
A2:获取 collection1 表中的数据。
A3:获取 collection2 表中的数据。
A4:根据条件 order, lot 从序表 A2 中查询记录,然后追加序表 A3 中的字段 serialNo, batchNo,返回合并后的序表。
A5:关闭数据库连接。

Mongo、SPL 脚本都实现了预期的结果。SPL 很清晰地实现了从数据记录中的内嵌结构中筛选,将符合条件的数据合并成新序表。

4.两表关联查询

从关联表中选择所需要的字段组合成新表。

Collection1:

user1user2income
120.56
130.26

collection2:

user1user2output
120.3
130.4
230.5

期望结果:

user1user2incomeoutput
120.560.3
130.260.4


Mongo 脚本

db.c1.aggregate([
{ "$lookup": {
"from": "c2",
"localField": "user1",
"foreignField": "user1",
"as": "collection2_doc"
}},
{ "$unwind": "$collection2_doc"},
{ "$redact": {
"$cond": [
{"$eq": [ "$user2", "$collection2_doc.user2"] },
"$$KEEP",
"$$PRUNE"
]
}},
{ "$project": {
"user1": 1,
"user2": 1,
"income": "$income",
"output": "$collection2_doc. output"
}}
]).pretty()

lookup 两表进行关联查询,redact 对记录根据条件进行遍历处理,project 选择要显示的字段。


SPL脚本:


AB
1=mongo_open("mongodb://127.0.0.1:27017/raqdb")
2=mongo_shell(A1,"c1.find()").fetch()
3=mongo_shell(A1,"c2.find()").fetch()
4=A2.join(user1:user2,A3:user1:user2,output)
5>A1.close()

脚本说明:
A1:连接 mongodb 数据库。
A2:获取c1表中的数据。
A3:获取c2表中的数据。
A4:两表按字段 user1,user2 关联,追加序表 A3 中的 output 字段,返回序表。
A5:关闭数据库连接。


Mongo、SPL 脚本都实现了预期的结果。SPL 通过 join 把两个关联表不同的字段合并成新表,与关系数据库用法类似。


5.多表关联查询

多于两个表的关联查询,结合成一张大表。

Doc1:

_idfirstNamelastName
U001shubhamverma

Doc2:

_iduserIdaddressmob
2U001Gurgaon9876543200

Doc3:

_iduserIdfbURLstwitterURLs
3U001http://www.facebook.comhttp://www.twitter.com


合并后的结果:
{
"_id" : ObjectId("5901a4c63541b7d5d3293766"),
"firstName" : "shubham",
"lastName" : "verma",
"address" : {
"address" : "Gurgaon"
},
"social" : {
"fbURLs" : "http://www.facebook.com",
"twitterURLs" : "http://www.twitter.com"
}
}


Mongo 脚本

db.doc1.aggregate([
{$match: { _id: ObjectId("5901a4c63541b7d5d3293766") } },
{
$lookup:
{
from: "doc2",
localField: "_id",
foreignField: "userId",
as: "address"
}
},
{
$unwind: "$address"
},
{
$project: {
"address._id": 0,
"address.userId": 0,
"address.mob": 0
}
},
{
$lookup:
{
from: "doc3",
localField: "_id",
foreignField: "userId",
as: "social"
}
},
{
$unwind: "$social"
},

{
$project: {
"social._id": 0,
"social.userId": 0
}
}
]).pretty();

由于 Mongodb 数据结构原因,写法也多样化,展示也各不相同。


SPL脚本:


AB
1=mongo_open("mongodb://127.0.0.1:27017/raqdb")
2=mongo_shell(A1,"doc1.find()").fetch()
3=mongo_shell(A1,"doc2.find()").fetch()
4=mongo_shell(A1,"doc3.find()").fetch()
5=A2.join(_id,A3:userId,address,mob)
6=A5.join(_id,A4:userId,fbURLs,twitterURLs)
7>A1.close()

Mongo、SPL 脚本都实现了预期的结果。此 SPL 脚本与上面例子类似,只是多了一个关联表,每次 join 就新增加字段,最后叠加构成一张大表。

SPL 脚本的简洁性、统一性非常明显。

6.关联表中的数组查找

从关联表记录数据组中查找符合条件的记录, 用给定的字段组合成新表。

测试数据:

users:

_idNameworkouts
1000xxx[2,4,6]
1002yyy[1,3,5]

workouts:

_idDateBook
11/1/2001Othello
22/2/2001A Midsummer Night's Dream
33/3/2001The Old Man and the Sea
44/4/2001GULLIVER'S TRAVELS
55/5/2001Pickwick Papers
66/6/2001The Red and the Black

期望结果:

Name_idDateBook
xxx22/2/2001A Midsummer Night's Dream
xxx44/4/2001GULLIVER'S TRAVELS
xxx66/6/2001The Red and the Black
yyy11/1/2001Othello
yyy33/3/2001The Old Man and the Sea
yyy55/5/2001Pickwick Papers


Mongo 脚本

db.users.aggregate([
{ "$lookup": {

"from" : "workouts",
"localField" : "workouts",
"foreignField" : "_id",
"as" : "workoutDocumentsArray"
}},
{$project: { _id:0,workouts:0} } ,
{"$unwind": "$workoutDocumentsArray"},
{"$replaceRoot": { "newRoot": { $mergeObjects: [ "$$ROOT", "$workoutDocumentsArray"] } } },
{$project: { workoutDocumentsArray: 0} }
]).pretty()

把关联表 users,workouts 查询结果放到数组中,再将数组拆解,提升子记录的位置,去掉不需要的字段。

SPL脚本 (users.dfx):


AB
1=mongo_open("mongodb://127.0.0.1:27017/raqdb")
2=mongo_shell(A1,"users.find()").fetch()
3=mongo_shell(A1,"workouts.find()").fetch()
4=A2.conj(A3.select(A2.workouts^~.array(_id)!=[]).derive(A2.name))
5>A1.close()

脚本说明:
A1:连接 mongodb 数据库。
A2:获取users表中的数据。
A3:获取workouts表中的数据。
A4:查询序表 A3 的 _id 值存在于序表A2中 workouts 数组的记录, 并追加 name 字段。返回合并的序表。
A5:关闭数据库连接。
由于需要获取序列的交集不为空为条件,故将 _id 转换成序列。
Mongo、SPL 脚本都实现了预期的结果。从脚本实现过程来看,SPL 集成度高而又不失灵活性,让程序简化了不少。


7.Java 应用程序调用 DFX 脚本

在通过 SPL 脚本对 MongoDB 数据进行了关联计算后,其结果可以被 java 应用程序很容易地使用。集算器提供了 JDBC 驱动程序,用 JDBC 存储过程方式访问,与调用存储过程相同。(JDBC 具体配置参考《集算器教程》中的" JDBC 基本使用"章节 )
Java 调用主要过程如下:
public void testUsers(){
Connection con = null;
com.esproc.jdbc.InternalCStatement st;
try{
// 建立连接
Class.forName("com.esproc.jdbc.InternalDriver");
con= DriverManager.getConnection("jdbc:esproc:local://");
// 调用存储过程,其中 users 是 dfx 的文件名
st =(com. esproc.jdbc.InternalCStatement)con.prepareCall("call users> ()");
// 执行存储过程
st.execute();
// 获取结果集
ResultSet rs = st.getResultSet();
。。。。。。。
catch(Exception e){
System.out.println(e);
}
可以看到,使用时按标准的 JDBC 方法操作,集算器很方便嵌入到 Java 应用程序中。同时,集算器也支持 ODBC 驱动,因此集成到其它支持 ODBC 的语言也非常容易。

Mongo 存储的数据结构相对关系数据库更复杂、更灵活,其提供的查询语言也非常强、适应面广,同时需要了解函数也不少,函数之间的结合更是变化无穷,因此要熟练掌握并应用也并非易事。集算器的离散性、易用性恰好能弥补 Mongo 这方面的不足,在降低 mongo 学习成本及使用复杂度、难度的同时,让 mongo 的功能得到更充分的展现。


关联 数据 脚本 查询 字段 结果 结构 数据库 数组 信息 文档 过程 存储 情况 程序 应用 两个 条件 复杂 函数 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 1t内存的服务器 云南做网络安全的公司排名 平谷区什么是网络技术常见问题 客户端软件开发是做什么的 杭州农商银行软件开发部 软件开发时打的代码 2019年网络安全考试答案 数据库同步工具推荐 张家口软件开发定制 嵌入式web服务器设计 网络安全法第8条规定 北华大学图书馆各类数据库有多少 世界互联网科技创新 软件开发中文教程 技术创新管理数据库 文档服务器文件怎么设置共享 软件开发公司有哪些工作 三级网络技术突击 软件开发公司组织架构和部门职能 戴尔服务器更改管理端 无法连接系统所需数据库 我的世界手机基岩版服务器积分 2017软件开发方向分析 游戏服务器 erlang 网络盗窃罪触犯网络安全法吗 郧西专业软件开发学习 华为云软件开发平台部署注意事项 数据库int怎么设置为无符号 养殖业环境控制系统软件开发 水晶报表 数据库登陆失败
0