在CouchDB中执行"连接"时,可以使用视图排序规则将记录分组在一起。 例如,具有两种文档类型的客户和订单。这样您就可以返回客户,然后是该客户的所有订单,然后是下一个客户和订单。
问题是,如何合并行,以便如果您有 10 个客户和 40 个订单,您的输出仍然是 10 行而不是 50 行。您实质上是在客户行中添加更多信息。
我相信使用_list
或reduce
可以解决这个问题。问题是究竟如何做到这一点?
我第二个jhs回答,但我认为他的"选项2"太危险了。我以艰难的方式学会了它。您可以将reduce功能用于许多好事,例如获取博客每个用户的最后一篇文章,但您不能将其用于任何不会减少返回数据量的事情。
为了用事实来支持它,我制作了这个小脚本来生成 200 个客户,每个客户有 20 个订单。
#!/bin/bash
echo '{"docs":['
for i in $(seq 1 200); do
id=customer/$i
echo '{"_id":"'$id'","type":"customer","name":"Customer '$i'"},'
for o in $(seq 1 20); do
echo '{"type":"order","_id":"order/'$i'/'$o'", "for":"'$id'", "desc":"Desc '$i$o'"},'
done
done
echo ']}'
这是一个非常可能的情况,足以抛出Error: reduce_overflow_error
.
恕我直言,您有两个选择是:
选项1:优化列表功能
只需稍加努力,您就可以手动构建 JSON 响应,这样您就不需要在数组中累积订单。
我编辑了jhs的列表功能以避免使用任何数组,因此您可以拥有任意数量的订单的客户。
function(head, req) {
start({'headers':{'Content-Type':'application/json'}});
var first_customer = true
, first_order = true
, row
;
send('{"rows":[');
while(row = getRow()) {
if(row.key[1] === 2) {
// Order for customer
if (first_order) {
first_order = false;
} else {
send(',');
}
send(JSON.stringify(row.value));
}
else if (row.key[1] === 1) {
// New customer
if (first_customer) {
first_customer = false;
} else {
send(']},');
}
send('{"customer":');
send(JSON.stringify(row.key[0]));
send(',"orders":[');
first_order = true;
}
}
if (!first_customer)
send(']}');
send('n]}');
}
选项 2:针对您的用例优化文档
如果您确实需要将订单放在同一个文档中,请问问自己是否可以以这种方式存储它并避免在查询时进行任何处理。
换句话说:尝试充分利用文档数据库提供的可能性。设计最适合您的用例的文档,并减少使用它们所需的后处理。
CouchDB 的主要"意见"之一是它只做在分布式、集群环境中也可能做的事情。实际上,这意味着一开始会有一些不便,以后在不更改代码的情况下获得大可扩展性的回报。
换句话说,"加入"问题没有完美的答案。但我认为有两个不错的选择。
我正在使用这个数据集:
$ curl localhost:5984/so/_bulk_docs -XPOST -Hcontent-type:application/json -d @-
{"docs":[
{"type":"customer","name":"Jason"},
{"type":"customer","name":"Hunter"},
{"type":"customer","name":"Smith"},
{"type":"order", "for":"Jason", "desc":"Hat"},
{"type":"order", "for":"Jason", "desc":"Shoes"},
{"type":"order", "for":"Smith", "desc":"Pan"}
]}
^D
[{"id":"4cb766ebafda06d8a3a7382f74000b46","rev":"1-8769ac2fffb869e795c347e7b8c653bf"},
{"id":"4cb766ebafda06d8a3a7382f74000b7d","rev":"1-094eff3e3a5967d974fcd7b3cfd7e454"},
{"id":"4cb766ebafda06d8a3a7382f740019cb","rev":"1-5cda0b61da4c045ff503b57f614454d5"},
{"id":"4cb766ebafda06d8a3a7382f7400239d","rev":"1-50642a9809f15283a9d938c8fe28ef27"},
{"id":"4cb766ebafda06d8a3a7382f74002778","rev":"1-d03d883fb14a424e3db022350b38c510"},
{"id":"4cb766ebafda06d8a3a7382f74002c5c","rev":"1-e9612f5d267a8442d3fc2ae09e8c800d"}]
我的地图功能是
function(doc) {
if(doc.type == 'customer')
emit([doc.name, 1], "");
if(doc.type == 'order')
emit([doc.for, 2], doc.desc);
}
查询完整视图显示:
{"total_rows":6,"offset":0,"rows":[
{"id":"4cb766ebafda06d8a3a7382f74000b7d","key":["Hunter",1],"value":""},
{"id":"4cb766ebafda06d8a3a7382f74000b46","key":["Jason",1],"value":""},
{"id":"4cb766ebafda06d8a3a7382f7400239d","key":["Jason",2],"value":"Hat"},
{"id":"4cb766ebafda06d8a3a7382f74002778","key":["Jason",2],"value":"Shoes"},
{"id":"4cb766ebafda06d8a3a7382f740019cb","key":["Smith",1],"value":""},
{"id":"4cb766ebafda06d8a3a7382f74002c5c","key":["Smith",2],"value":"Pan"}
]}
选项 1:用于收集结果的列表功能
好处是,如果你要求 10 行,你肯定会得到 10 行(当然,除非没有足够的数据)。
但代价是您必须对每个查询进行服务器端处理。您想要的数据只是放在磁盘上,准备流式传输给您,但现在您已经通过这个瓶颈。
但是,我个人认为,除非您有明显的性能问题,否则_list
很好。
function(head, req) {
start({'headers':{'Content-Type':'application/json'}});
send('{"rows":');
var customer = null, orders = [], count = 0;
var prefix = 'n[ ';
function show_orders() {
if(customer && orders.length > 0) {
count += 1;
send(prefix);
prefix = 'n, ';
send(JSON.stringify({'customer':customer, 'orders':orders}));
}
}
function done() {
send('n]}');
}
var row;
while(row = getRow()) {
if(row.key[1] == 2) {
// Order for customer
orders.push(row.value);
}
if(row.key[1] == 1) {
// New customer
show_orders();
if(req.query.lim && count >= parseInt(req.query.lim)) {
// Reached the limit
done();
return;
} else {
// Prepare for this customer.
customer = row.key[0];
orders = [];
}
}
}
// Show the last order set seen and finish.
show_orders();
done();
}
此函数只是循环遍历map
行,并且仅在收集所有信息后输出完整的客户+订单行。显然,您可以更改输出的 JSON 格式。此外,还有一个?lim=X
参数,因为使用参数limit
会干扰地图查询。
危险在于此函数在内存中构建无限响应。如果客户下了 10,000 个订单怎么办?还是10万?最终构建orders
阵列将失败。这就是为什么CouchDB将它们放在一个"高"列表中。如果您永远不会为每个客户获得 10,000 个订单,那么这不是问题。
$ curl 'http://localhost:5984/so/_design/ex/_list/ex/so?reduce=false&lim=2'
{"rows":
[ {"customer":"Jason","orders":["Hat","Shoes"]}
, {"customer":"Smith","orders":["Pan"]}
]}
选项 2:偷偷摸摸地减少
您可以使用reduce
函数执行类似的操作。在这里,我会警告你,这在技术上是不可扩展的,因为你在磁盘上积累了一个响应,但是我个人更喜欢它而不是_list因为代码更简单,我知道我直接从磁盘读取数据,没有后处理。
function(keys, vals, re) {
// If all keys are the same, then these are all
// orders for the same customer, so accumulate
// them. Otherwise, return something meaningless.
var a;
var first_customer = keys[0][0][0];
for(a = 0; a < keys.length; a++)
if(keys[a][0][0] !== first_customer)
return null;
var result = [];
for(a = 0; a < vals.length; a++)
if(vals[a]) {
// Accumulate an order.
result.push(vals[a]);
}
return result;
}
始终使用?group_level=1
查询此视图,这将按客户对结果进行细分(因为客户名称是map
键中的第一项)。
这是违法的,因为您不应该在减少阶段积累数据。这就是为什么他们称之为减少。
但是,CouchDB 是放松的,只要你不构建巨大的列表,它应该可以工作,而且它更优雅。
$ curl 'localhost:5984/so/_design/ex/_view/so?group_level=1&limit=3'
{"rows":[
{"key":["Hunter"],"value":[]},
{"key":["Jason"],"value":["Shoes","Hat"]},
{"key":["Smith"],"value":["Pan"]}
]}
祝你好运!