MongoDB屬于 NoSql 中的基于分布式文件存儲(chǔ)的文檔型數(shù)據(jù)庫(kù),這種bson格式的文檔結(jié)構(gòu),更加貼近我們對(duì)物體各方面的屬性描述。而在使用 MongoDB 存儲(chǔ)數(shù)據(jù)的過(guò)程中,有時(shí)候難免需要進(jìn)行關(guān)聯(lián)表查詢(xún)。自從 MongoDB 3.2 版本后,它提供了 $lookup 進(jìn)行關(guān)聯(lián)表查詢(xún),讓查詢(xún)功能改進(jìn)了不少。但在實(shí)現(xiàn)應(yīng)用場(chǎng)景中,所遇到的環(huán)境錯(cuò)綜復(fù)雜,問(wèn)題解決也非易事,腳本書(shū)寫(xiě)起來(lái)也并不簡(jiǎn)單。好在有了集算器 SPL 語(yǔ)言的協(xié)助,處理起來(lái)就相對(duì)容易多了。
本文我們將針對(duì) MongoDB 在關(guān)聯(lián)運(yùn)算方面的問(wèn)題進(jìn)行討論分析,并通過(guò)集算器 SPL 語(yǔ)言加以改進(jìn),方便用戶(hù)使用 MongoDB。討論將分為以下幾個(gè)部分:
1. 關(guān)聯(lián)嵌套結(jié)構(gòu)情況 1…………………………………………….. 1
2. 關(guān)聯(lián)嵌套結(jié)構(gòu)情況 2…………………………………………….. 3
3. 關(guān)聯(lián)嵌套結(jié)構(gòu)情況 3…………………………………………….. 4
4. 兩表關(guān)聯(lián)查詢(xún)………………………………………………………. 6
5. 多表關(guān)聯(lián)查詢(xún)………………………………………………………. 8
6. 關(guān)聯(lián)表中的數(shù)組查找…………………………………………… 10
Java 應(yīng)用程序調(diào)用 DFX 腳本…………………………………… 12
兩個(gè)關(guān)聯(lián)表,表 A 與表 B 中的內(nèi)嵌文檔信息關(guān)聯(lián), 且返回的信息在內(nèi)嵌文檔中。表 childsgroup 字段 childs 是嵌套數(shù)組結(jié)構(gòu),需要合并的信息 name 在其下。
測(cè)試數(shù)據(jù):
history:
_id | id | History | child_id |
1 | 001 | today worked | ch001 |
2 | 002 | Working | ch004 |
3 | 003 | now working | ch009 |
childsgroup:
_id | gid | name | childs |
1 | g001 | group1 | {"id":"ch001","info":{"name":"a",mobile:1111}},{"id":"ch002","info":{"name":"b",mobile:2222}} |
2 | g002 | group1 | {"id":"ch004","info":{"name":"c",mobile:3333}},{"id":"ch009","info":{"name":"d",mobile:4444}} |
表History中的child_id與表childsgroup中的childs.id關(guān)聯(lián),希望得到下面結(jié)果:
{
"_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"} ]) |
這個(gè)腳本用了幾個(gè)函數(shù)lookup、pipeline、match、unwind、replaceRoot處理,一般 mongodb 用戶(hù)不容易寫(xiě)出這樣復(fù)雜腳本;那么我們?cè)倏纯?spl 腳本是如何實(shí)現(xiàn)的:
SPL腳本 ( 文件名:childsgroup.dfx)
A | B | |
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() |
關(guān)聯(lián)查詢(xún)結(jié)果:
_id | id | history | child_id | info |
1 | 001 | today worked | ch001 | [a,1111] |
2 | 002 | working | ch004 | [c,3333] |
3 | 003 | now working | ch009 | [d,4444] |
腳本說(shuō)明:
A1:連接 mongodb 數(shù)據(jù)庫(kù)。
A2:獲取 history 表中的數(shù)據(jù)。
A3:獲取 childsgroup 表中的數(shù)據(jù)。
A4:將 childsgroup 中的 childs 數(shù)據(jù)提取出來(lái)合并成序表。
A5:表 history 中的 child_id 與表 childs 中的 id 關(guān)聯(lián)查詢(xún),追加 info 字段, 返回序表。
A6:關(guān)閉數(shù)據(jù)庫(kù)連接。
相對(duì) mongodb 腳本寫(xiě)法,SPL 腳本的難度降低了不少,思路也更加清晰,也不需要再去熟悉有關(guān) mongo 函數(shù)的用法,以及如何去組合處理數(shù)據(jù)等,節(jié)約了不少時(shí)間。
兩個(gè)關(guān)聯(lián)表,表 A 與表 B 中的內(nèi)嵌文檔信息關(guān)聯(lián), 將信息合并到內(nèi)嵌文檔中。表 txtPost 字段 comment 是嵌套數(shù)組結(jié)構(gòu),需要把 comment_content 合并到其下。
txtComment:
_ID | comment_no | comment_content |
1 | 143 | test test |
2 | 140 | math |
txtPost
_ID | post_no | Comment |
1 | 48 | [{"comment_no" : 143, "comment_group" : 1} ] |
2 | 47 | [{"comment_no" : 140, "comment_group" : 2}, {"comment_no" : 143, "comment_group" : 3} ] |
期望結(jié)果:
_ID | post_no | Comment |
1 | 48 | [{"comment_no" : 143, "comment_group" : 1,"comment_content" : "test test"} ] |
2 | 47 | [{"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", |
表txtPost 按 comment 拆解成記錄,然后與表 txtComment 關(guān)聯(lián)查詢(xún),將其結(jié)果放到數(shù)組中,再將數(shù)組拆解成記錄,將comment_content 值移到 comment 下,最后分組合并。
SPL 腳本:
A | B | |
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() |
關(guān)聯(lián)查詢(xún)結(jié)果:
pno | Comment |
47 | [[ 140, 2,47, …],[143, 3,47, …] ] |
48 | [[143, 1,48, …]] |
腳本說(shuō)明:
A1:連接 mongodb 數(shù)據(jù)庫(kù)。
A2:獲取 txtPost 表中的數(shù)據(jù)。
A3:獲取 txtComment 表中的數(shù)據(jù)。
A4:將序表 A2 下的 comment 與 post_no 組合成序表,其中 post_no 改名為 pno。
A5:序表 A4 通過(guò) comment_no 與序表 A3 關(guān)聯(lián),追加字段 comment_content,將其改名為 Content。
A6:按 pno 分組返回序表,~ 表示當(dāng)前記錄。
A7:關(guān)閉數(shù)據(jù)庫(kù)連接。
Mongo、SPL 腳本實(shí)現(xiàn)方式類(lèi)似,都是把嵌套結(jié)構(gòu)的數(shù)據(jù)轉(zhuǎn)換成行列結(jié)構(gòu)的數(shù)據(jù),再分組合并。但 SPL 腳本的實(shí)現(xiàn)更簡(jiǎn)單明了。
兩個(gè)關(guān)聯(lián)表,表 A 與表 B 中的內(nèi)嵌文檔信息關(guān)聯(lián), 且返回的信息在記錄上。表 collection2 字段 product 是嵌套數(shù)組結(jié)構(gòu),返回的信息是 isCompleted 等字段 。
測(cè)試數(shù)據(jù):
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'}
]
}
期待結(jié)果
{
_id: 5bc2e44a106342152cd83e97,
description:
{
status: 'Good',
machine: 'X',
},
order: 'A',
lot: '1' ,
isCompleted: false,
serialNo: '1',
batchNo: '2'
}
Mongo 腳本
db.collection1.aggregate([{ $lookup: { from: "collection2", |
lookup 兩表關(guān)聯(lián)查詢(xún),首個(gè) addFields獲取isCompleted數(shù)組的第一個(gè)記錄,后一個(gè)addFields 轉(zhuǎn)換成所需要的幾個(gè)字段信息
SPL腳本:
A | B | |
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() |
腳本說(shuō)明:
A1:連接 mongodb 數(shù)據(jù)庫(kù)。
A2:獲取 collection1 表中的數(shù)據(jù)。
A3:獲取 collection2 表中的數(shù)據(jù)。
A4:根據(jù)條件 order, lot 從序表 A2 中查詢(xún)記錄,然后追加序表 A3 中的字段 serialNo, batchNo,返回合并后的序表。
A5:關(guān)閉數(shù)據(jù)庫(kù)連接。
Mongo、SPL 腳本都實(shí)現(xiàn)了預(yù)期的結(jié)果。SPL 很清晰地實(shí)現(xiàn)了從數(shù)據(jù)記錄中的內(nèi)嵌結(jié)構(gòu)中篩選,將符合條件的數(shù)據(jù)合并成新序表。
從關(guān)聯(lián)表中選擇所需要的字段組合成新表。
Collection1:
user1 | user2 | income |
1 | 2 | 0.56 |
1 | 3 | 0.26 |
collection2:
user1 | user2 | output |
1 | 2 | 0.3 |
1 | 3 | 0.4 |
2 | 3 | 0.5 |
期望結(jié)果:
user1 | user2 | income | output |
1 | 2 | 0.56 | 0.3 |
1 | 3 | 0.26 | 0.4 |
Mongo 腳本
db.c1.aggregate([ |
lookup 兩表進(jìn)行關(guān)聯(lián)查詢(xún),redact 對(duì)記錄根據(jù)條件進(jìn)行遍歷處理,project 選擇要顯示的字段。
SPL腳本:
A | B | |
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() |
腳本說(shuō)明:
A1:連接 mongodb 數(shù)據(jù)庫(kù)。
A2:獲取c1表中的數(shù)據(jù)。
A3:獲取c2表中的數(shù)據(jù)。
A4:兩表按字段 user1,user2 關(guān)聯(lián),追加序表 A3 中的 output 字段,返回序表。
A5:關(guān)閉數(shù)據(jù)庫(kù)連接。
Mongo、SPL 腳本都實(shí)現(xiàn)了預(yù)期的結(jié)果。SPL 通過(guò) join 把兩個(gè)關(guān)聯(lián)表不同的字段合并成新表,與關(guān)系數(shù)據(jù)庫(kù)用法類(lèi)似。
多于兩個(gè)表的關(guān)聯(lián)查詢(xún),結(jié)合成一張大表。
Doc1:
_id | firstName | lastName |
U001 | shubham | verma |
Doc2:
_id | userId | address | mob |
2 | U001 | Gurgaon | 9876543200 |
Doc3:
_id | userId | fbURLs | twitterURLs |
3 | U001 | http://www.facebook.com | http://www.twitter.com |
合并后的結(jié)果:
{
"_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 數(shù)據(jù)結(jié)構(gòu)原因,寫(xiě)法也多樣化,展示也各不相同。
SPL腳本:
A | B | |
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 腳本都實(shí)現(xiàn)了預(yù)期的結(jié)果。此 SPL 腳本與上面例子類(lèi)似,只是多了一個(gè)關(guān)聯(lián)表,每次 join 就新增加字段,最后疊加構(gòu)成一張大表。
SPL 腳本的簡(jiǎn)潔性、統(tǒng)一性非常明顯。
從關(guān)聯(lián)表記錄數(shù)據(jù)組中查找符合條件的記錄, 用給定的字段組合成新表。
測(cè)試數(shù)據(jù):
users:
_id | Name | workouts |
1000 | xxx | [2,4,6] |
1002 | yyy | [1,3,5] |
workouts:
_id | Date | Book |
1 | 1/1/2001 | Othello |
2 | 2/2/2001 | A Midsummer Night's Dream |
3 | 3/3/2001 | The Old Man and the Sea |
4 | 4/4/2001 | GULLIVER’S TRAVELS |
5 | 5/5/2001 | Pickwick Papers |
6 | 6/6/2001 | The Red and the Black |
期望結(jié)果:
Name | _id | Date | Book |
xxx | 2 | 2/2/2001 | A Midsummer Night's Dream |
xxx | 4 | 4/4/2001 | GULLIVER’S TRAVELS |
xxx | 6 | 6/6/2001 | The Red and the Black |
yyy | 1 | 1/1/2001 | Othello |
yyy | 3 | 3/3/2001 | The Old Man and the Sea |
yyy | 5 | 5/5/2001 | Pickwick Papers |
Mongo 腳本
db.users.aggregate([ { "$lookup": { "from" : "workouts", |
把關(guān)聯(lián)表 users,workouts 查詢(xún)結(jié)果放到數(shù)組中,再將數(shù)組拆解,提升子記錄的位置,去掉不需要的字段。
SPL腳本 (users.dfx):
A | B | |
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() |
腳本說(shuō)明:
A1:連接 mongodb 數(shù)據(jù)庫(kù)。
A2:獲取users表中的數(shù)據(jù)。
A3:獲取workouts表中的數(shù)據(jù)。
A4:查詢(xún)序表 A3 的 _id 值存在于序表A2中 workouts 數(shù)組的記錄, 并追加 name 字段。返回合并的序表。
A5:關(guān)閉數(shù)據(jù)庫(kù)連接。
由于需要獲取序列的交集不為空為條件,故將 _id 轉(zhuǎn)換成序列。
Mongo、SPL 腳本都實(shí)現(xiàn)了預(yù)期的結(jié)果。從腳本實(shí)現(xiàn)過(guò)程來(lái)看,SPL 集成度高而又不失靈活性,讓程序簡(jiǎn)化了不少。
在通過(guò) SPL 腳本對(duì) MongoDB 數(shù)據(jù)進(jìn)行了關(guān)聯(lián)計(jì)算后,其結(jié)果可以被 java 應(yīng)用程序很容易地使用。集算器提供了 JDBC 驅(qū)動(dòng)程序,用 JDBC 存儲(chǔ)過(guò)程方式訪(fǎng)問(wèn),與調(diào)用存儲(chǔ)過(guò)程相同。(JDBC 具體配置參考《集算器教程》中的“ JDBC 基本使用”章節(jié) )
Java 調(diào)用主要過(guò)程如下:
public void testUsers(){
Connection con = null;
com.esproc.jdbc.InternalCStatement st;
try{
// 建立連接
Class.forName("com.esproc.jdbc.InternalDriver");
con= DriverManager.getConnection("jdbc:esproc:local://");
// 調(diào)用存儲(chǔ)過(guò)程,其中 users 是 dfx 的文件名
st =(com. esproc.jdbc.InternalCStatement)con.prepareCall("call users> ()");
// 執(zhí)行存儲(chǔ)過(guò)程
st.execute();
// 獲取結(jié)果集
ResultSet rs = st.getResultSet();
。。。。。。。
catch(Exception e){
System.out.println(e);
}
可以看到,使用時(shí)按標(biāo)準(zhǔn)的 JDBC 方法操作,集算器很方便嵌入到 Java 應(yīng)用程序中。同時(shí),集算器也支持 ODBC 驅(qū)動(dòng),因此集成到其它支持 ODBC 的語(yǔ)言也非常容易。
Mongo 存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)相對(duì)關(guān)系數(shù)據(jù)庫(kù)更復(fù)雜、更靈活,其提供的查詢(xún)語(yǔ)言也非常強(qiáng)、適應(yīng)面廣,同時(shí)需要了解函數(shù)也不少,函數(shù)之間的結(jié)合更是變化無(wú)窮,因此要熟練掌握并應(yīng)用也并非易事。集算器的離散性、易用性恰好能彌補(bǔ) Mongo 這方面的不足,在降低 mongo 學(xué)習(xí)成本及使用復(fù)雜度、難度的同時(shí),讓 mongo 的功能得到更充分的展現(xiàn)。
另外有需要云服務(wù)器可以了解下創(chuàng)新互聯(lián)scvps.cn,海內(nèi)外云服務(wù)器15元起步,三天無(wú)理由+7*72小時(shí)售后在線(xiàn),公司持有idc許可證,提供“云服務(wù)器、裸金屬服務(wù)器、高防服務(wù)器、香港服務(wù)器、美國(guó)服務(wù)器、虛擬主機(jī)、免備案服務(wù)器”等云主機(jī)租用服務(wù)以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡(jiǎn)單易用、服務(wù)可用性高、性?xún)r(jià)比高”等特點(diǎn)與優(yōu)勢(shì),專(zhuān)為企業(yè)上云打造定制,能夠滿(mǎn)足用戶(hù)豐富、多元化的應(yīng)用場(chǎng)景需求。