I code it

Code and Life

第15章 服务器端的JavaScript(下)

15.2 CouchDB

15.2.1 CouchDB简介

CouchDB是一个Apache的开源项目,它是一个面向文档的数据库系统。CouchDB提供REST形式的API的访问,并以JSON作为数据交换格式。这一点使得CouchDB很容易被使用。事实上,支持HTTP的编程语言都可以作为CouchDB的客户端。它使用MapReduce方式来进行数据的访问和索引,在这一点上,函数式编程无疑更加合适:因为map/reduce本身就是函数式编程中的概念。

CouchDB并无意取代关系数据库系统,而是作为一种支持方案呈现。使用关系型数据库,可以进行规整而复杂的关系运算。而在CouchDB中,并不存在模式(schema)的概念。比如一些消息系统,内容管理等,如果使用关系型数据库,当表结构变动时,可能会非常复杂,而且容易出错。但是采用CouchDB的松散的key/value对来存放,则处理起来会容易的多。一般来说,在CouchDB中,文档之间是不应该存在关系的,每个文档都是独立的实体,但是可能具有某些共性,这些共性可以通过map/reduce来抽取。

CouchDB的工作方式大致如下:

clip_image002

云朵表示一个数据库,其中蓝色的块表示文档,这些文档之间并无直接关联。通过map函数,将其中的某些项抽取为一些key/value对,然后根据query找到用户需要的结果集。由于key/value都可以是复杂的JSON,因此它的表现力十分强大。

CouchDB的文档采用JSON来表示,一个典型的文档示例如下;

 1: {
 2:    "_id": "46efa16cc220f6548696a6e6fc004463",
 3:    "_rev": "1-37802625d560be4af0dc18155422fd2b",
 4:    "language": [
 5:        "Chinese",
 6:        "English"
 7:    ],
 8:    "name": "Juntao"
 9: }
 

每个文档都会带有两个隐式的属性”_id”和”_rev”。”_id”如果在创建文档时不显式的指定,则系统会分配一个GUID作为其值。”_rev”是一个内部的版本号,CouchDB中包含一个版本控制系统,关于版本控制系统的细节,此处不做讨论。

15.2.1 CouchDB使用

为了使用JavaScript来访问CouchDB提供的REST形式的API,我们使用Rhino引擎,实现了一个简单的xmlhttprequest来与CouchDB的服务器进行通信,当然使用其他语言如C/Python等也可以做到,此处我们使用JavaScript本身。

另一个常用的工具是curl,curl是基于命令行的,功能非常强大,支持多种网络协议。CouchDB的官方示例中,很多次的用到了curl,但是我们只是在简单的测试时才会使用它。

这个小节中,我们主要讨论如何通过HTTP请求完成下列动作:

  • 创建数据库
  • 创建文档
  • 创建设计文档
  • 过滤数据
创建数据库非常容易,仅需要发送PUT请求到下列形式的URL即可:

http://localhost:5984/database_name

即可创建一个数据库,名称为database_name,后续的操作即以此作为基准。

创建文档时,需要指定数据库,然后将文档(JSON字符串)通过HTTP的POST方式发送到下列形式的URL上:

http://localhost:5984/database_name

需要发送的文档的形式如下:

 1: {
 2: "name" : "Juntao",
 3: "language" : ["Chinese", "English"]
 4: }
 

请求成功之后,服务器会为此文档创建id及版本信息。可以通过GET下列形式的URL来获取文档:

http://localhost:5984/contacts/46efa16cc220f6548696a6e6fc004463

此处的46efa16cc220f6548696a6e6fc004463即为CouchDB为此文档生成的GUID,结果如下:

 1: {
 2:    "_id": "46efa16cc220f6548696a6e6fc004463",
 3:    "_rev": "1-37802625d560be4af0dc18155422fd2b",
 4:    "language": [
 5:        "Chinese",
 6:        "English"
 7:    ],
 8:    "name": "Juntao"
 9: }
 

设计文档是一种特别的文档,用以表现应用程序的逻辑部分,其中包含视图(map/reduce对),附件定义等。CouchDB是基于文档的,因此将应用程序作为文档也就不足为奇了。下面是一个设计文档的示例:

 1: {
 2:    "_id": "_design/filters",
 3:    "_rev": "2-85428a15c29f7dc5b31be1a0a3a217c9",
 4:    "views": {
 5:        "view_gt": {
 6:            "map": "function(doc){...}",
 7:            "reduce" : "function(keys, values, rereduce){}"
 8:        },
 9:        "view_lt": {
 10:            "map": "function(doc){...}",
 11:            "reduce" : "function(keys, values, rereduce){}"
 12:        },
 13:        "view_company": {
 14:            "map": "function(doc){...}",
 15:            "reduce" : "function(keys, values, rereduce){}"
 16:        }
 17:    },
 18:    "shows" : {
 19:         "show_sth" : "function(doc, req){}"
 20:    },
 21:
 22:    "_attachments" : {
 23:
 24:    }
 25: }
 

其中,views中包含map/reduce的定义,用以对数据进行抽取,转换。_attachments中包含文档的附件的定义。设计文档的路径均以”/database_name/_design”开头。我们这里着重讨论设计文档的views节:

每个view中包含一个map函数和一个可选的reduce函数。map负责在所有的文档中收集信息,并生成key/value对,这个动作是并行的,可能同时有多个map函数在工作。所有的map函数都是用同一种hash算法,从而使得具有相同hash值的结果会被存储到一起。而reduce则负责将相同hash值的结果进行处理,并最终产生结果。map函数相当于关系数据库中聚合查询的group-by子句,而reduce函数则相当于聚合函数,如统计,求平均值等。

比如,我们可以将gt10这个view的map/reduce定义如下:

 1: function(doc){
 2:     if(doc.name && doc.age){
 3:         if(doc.age > 10){
 4:             emit(doc.name, doc.age);
 5:         }
 6:     }
 7: }
 8:
 9: function(keys, values, rereduce){
 10:     return sum(values);
 11: }
 

这个map将产生key为联系人名字,value为联系人年龄的key/value对,而reduce将符合map的所有联系人的年龄加在一起。

15.2.3 CouchDB实例

在这个小节中,我们将开发一个简单的CouchDB的应用,这个应用用以保存个人的联系人名单,然后创建一个设计文档,其中包含3个view,分别用以查询联系人中年龄大于10岁的,小于5岁的,以及联系人所在的公司为”Infonet”的。

我们通过JavaScript来进行所有的这些操作,因此我们需要封装一些常用的操作:

  • 创建数据库
  • 添加联系人(文档)
  • 添加视图
  • 查询数据
创建数据库非常容易,以PUT方式访问需要建立的数据库的URL即可:
 1: //PUT http://localhost:5984/contacts
 2: function create_couch_db(host, port, dbname, handler){
 3:     host = host || "localhost";
 4:     port = port || "5984";
 5:
 6:     if(!dbname){
 7:         return false;
 8:     }
 9:
 10:     var xhr = new XMLHttpRequest();
 11:     var url = "http://"+host+":"+port+"/"+dbname;
 12:
 13:     println(url);
 14:
 15:     xhr.open("PUT", url, false);
 16:     xhr.send(null);
 17:
 18:     function complete(){
 19:         if(xhr.readyState == 4){
 20:             handler(xhr);
 21:         }
 22:     }
 23:
 24:     xhr.onreadystatechange = complete;
 25: }
 

此处的XMLHttpRequest是一个用Java做的简单实现,不支持POST数据。对于POST的请求,我们在随后再做介绍。create_couch_db函数的最后一个参数是一个函数,当回调发生时调用此函数。

比如我们要在本机上部署的CouchDB创建一个名为”contacts”的数据库,当完成时我们将服务端返回的JSON解析并打印出其中包含的信息:

 1: function complete(xhr){
 2:     var obj = JSON.parse(xhr.responseText);
 3:     for(var item in obj){
 4:         if(typeof obj[item] == "object"){
 5:             println(JSON.stringify(obj[item]));
 6:         }
 7:         println("key = "+item+", value="+obj[item]);
 8:     }
 9: }
 10:
 11: create_couch_db("localhost", "5984", "contacts", complete);
 

调用之后,我们得到以下结果:

 

 

 1: key = id, value=3e1a1b09536ee010352d79fd89000961
 2: key = ok, value=true
 3: key = rev, value=1-122ac58db939abb4aaf227accf5405b7
 

我们再来添加文档,首先定义几个联系人的信息:

 1: var contacts = [
 2: {
 3:     name : "John",
 4:     age : 28,
 5:     address : "Somewhere in Kunming",
 6:     interests : ["trevaling", "reading"],
 7:     phone : "(871)-1234567"
 8: },
 9:
 10: {
 11:     name : "Nelson",
 12:     age : 35,
 13:     company : "Infonet",
 14:     address : "Eest Black-bridge"
 15: },
 16:
 17: {
 18:     name : "Boycott",
 19:     company : "Infonet"
 20: },
 21:
 22: {
 23:     name : "Smith",
 24:     age : 27,
 25:     company : "Infonet",
 26: },
 27:
 28: {
 29:     name : "Jim",
 30:     address : "West Mountain",
 31:     phone : "(871)-7654321"
 32: },
 33:
 34: {
 35:     name : "SiJing",
 36:     address : "HeBei",
 37:     age : 3
 38: },
 39: ];
 

然后在一个循环中调用添加文档的接口,前面的那个XMLHttpRequest对象不支持POST,我们只好自行开发一个可以发送HTTP请求的JavaScript脚本:

 1: for(var i = 0; i < contacts.length; i++){
 2:     add_couch_doc_raw("http://localhost:5984", "contacts", \
 3:             contacts[i], complete_raw);
 4: }
 

add_couch_doc_raw函数将用以创建文档,并通过complete_raw函数将结果呈现出来:

 1: function add_couch_doc_raw(uri, dbname, doc, handler){
 2:     var url = new java.net.URL(uri+"/"+dbname);
 3:     var data = JSON.stringify(doc);
 4:
 5:     var con = url.openConnection();
 6:     con.setDoOutput(true);
 7:     con.addRequestProperty("Content-Type", "application/json");
 8:     var writer = new java.io.OutputStreamWriter(con.getOutputStream());
 9:     writer.write(data);
 10:     writer.flush();
 11:     writer.close();
 12:
 13:     var reader = new java.io.BufferedReader(
 14:     new java.io.InputStreamReader(con.getInputStream()));
 15:
 16:     var line, text='';
 17:     while((line = reader.readLine()) != null){
 18:         text += line;
 19:     }
 20:
 21:     reader.close();
 22:
 23:     if(handler){
 24:         handler(text);
 25:     }
 26: }
 

完成之后的complete接收服务端的JSON字符串:

 1: function complete_raw(raw){
 2:     var obj = JSON.parse(raw);
 3:     for(var item in obj){
 4:         println("key = "+item+", value="+obj[item]);
 5:     }
 6: }
 

运行之后,服务器将返回结果:

 

 1: key = id, value=46efa16cc220f6548696a6e6fc0044f4
 2: key = ok, value=true
 3: key = rev, value=1-485b14c13e90cfad44d539a3860dc1f3
 4: key = id, value=46efa16cc220f6548696a6e6fc004f83
 5: key = ok, value=true
 6: key = rev, value=1-a22f80d27046f1b46ae218788747ffdb
 7: key = id, value=46efa16cc220f6548696a6e6fc005855
 8: key = ok, value=true
 9: key = rev, value=1-fc222c18f52f463c44ac48328b10385c
 10: ...
 

好了,现在我们可以通过CouchDB的web界面来查看刚才创建的数据库以及数据库中的文档了:

clip_image004

下面我们来尝试创建一个设计文档,并在设计文档中添加一些view:

 1: {
 2:     "_id" : "_design/filters",
 3:     "views" : {
 4:         "gt10" : {
 5:             "map" : "function(doc){ if(doc.name && doc.age){ if(doc.age > 10){emit(doc.name, doc.age);}}}"
 6:         },
 7:         "lt5" : {
 8:             "map" : "function(doc){ if(doc.name && doc.age){ if(doc.age < 5 ){emit(doc.name, doc.age);}}}"
 9:         },
 10:         "infonet" : {
 11:             "map" : "function(doc){ if(doc.name && doc.company){if(doc.company == 'Infonet'){ emit(doc.name, doc.company)}}}"
 12:         }
 13:     }
 14: }
我们为设计文档添加了三个view,其中的map将用作过滤器,它将对所有的文档进行抽取转换。将这个设计文档保存为文本,并通过curl上传该文档:
 1: $ curl -X PUT http://localhost:5984/contacts/_design/filters \
 2:   -d @contacts-filter.js
 

这个设计文档的名称叫做filters,因此完整的路径为/contacts/_design/filters。

现在我们就可以使用view来进行数据的过滤了,比如我们要找出所有联系人中,在Infonet公司工作的人:

clip_image006

当然,我们可以通过自己编写JavaScript来访问这些数据:

 1: function filter_couch_data(host, port, dbname, doc, view, handler){
 2:     host = host || "localhost";
 3:     port = port || "5984";
 4:
 5:     if(!dbname || !doc || !view){
 6:         return false;
 7:     }
 8:
 9:     var xhr = new XMLHttpRequest();
 10:     var url = "http://"+host+":"+port+"/"+dbname+"/_design/"+doc+"/_view/"+view;
 11:
 12:     xhr.open("GET", url, false);
 13:
 14:     function complete(){
 15:         if(xhr.readyState == 4){
 16:             handler(xhr);
 17:         }
 18:     }
 19:
 20:     xhr.onreadystatechange = complete;
 21:
 22:     xhr.send(null);
 23: }
 

访问http://localhost:5984/contacts/_design/filters/_view/infonet即可:

 1: filter_couch_data("localhost", "5984", "contacts",
 2:         "filters", "infonet", complete);
 

当插入新的数据时,map可以自动的将符合条件的key/value对放入view,比如:

 1: var newguy = {
 2:     age : 30,
 3:     name : "Fire"
 4: };
 5:
 6: add_couch_doc_raw("http://localhost:5984", "contacts", newguy, complete_raw);
 7:
 8: filter_couch_data("localhost", "5984", "contacts",
 9:         "filters", "gt10", complete);
 

我们新添加了一个newguy,他的年龄为30,然后我们访问gt10这个view,可以看到,符合这个条件的记录多了一条。这一点与关系数据库中的query的效果相同。

Comments