数据库指南-SQL与NoSQL

时间:2022-05-05 08:53:45

什么是数据库

我们的应用程序保存和执行业务逻辑,那么数据呢?这就需要数据库来保存了。数据库(database),就是我们保存数据的地方。一般来讲,数据比硬件和程序更宝贵,因此数据库是价值的核心。专业定义:数据库指的是以一定方式储存在一起、能为多个用户共享、具有尽可能小的冗余度、与应用程序彼此独立的数据集合。
可以浅显地理解为,数据库的存在起到了几个作用:
(1)数据的持久化:我们在内存中执行的各种运算产生的结果如果断电或系统崩溃就会消失,而存放在数据库中则“持久”保存在硬盘上了。
(2)便于查询:数据库对数据进行了结构化的存储,并提供各种查询工具,使得从中查找信息十分方便。
(3)便于数据的备份和转移:只要备份和转移数据库就够了。

(4)减少内存压力:相比内存,硬盘要大得多,可以把数据先放到硬盘上,用到哪些,就读出哪些。这样可以操作海量的数据。 

数据库是如何保存数据和支持查询的

基本有两大类方式,对应着两大数据库阵营。
第一,传统的关系型数据库(RDBMS)。
数据以“表”为存储单元,表由行-列组成,数据按“关系”保存在不同的表中,按主键-外键的方式进行查找。主要的查询工具是SQL语言。
其代表产品有Oracle、IBM公司的 DB2、微软公司的MS SQL Server以及Informix等等。开源的有MySQL,PostgresQL等。
特点:
(1)数据库内部可以支持复杂的操作(借助SQL),这就减少了程序应用层面的工作;
(2)ACID:A代表原子性,即在事务中执行多个操作是原子性的,要么事务中的操作全部执行,要么一个都不执行;C代表一致性,即保证进行事
务的过程中整个数据加的状态是一致的,不会出现数据冲突的情况;I代表隔离性,即两个事务不会相互影响,覆盖彼此数据等;D表示持久化,即事务一量完成,那么数据应该是被写到安全的,持久化存储的设备上(比如磁盘)。ACID的支持使得应用者能够很清楚他们当前的数据状态。即使如对于多个事务同时执行的情况下也能够保证数据状态的正常性。但是如果要保证数据的一致性,通常多个事务是不可能交叉执行的,这样就导致了可能一个很简单的操作需要等等一个复杂操作完成才能进行的情况。
(3)数据结构(scheme)严格。好的方面是减少出错,坏的方面是不灵活,不易扩展。

随着互联网web2.0网站的兴起,非关系型的数据库成了一个极其热门的新领域,非关系数据库产品的发展非常迅速。而传统的关系数据库在应付web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,例如:
⒈High performance ;– ;对数据库高并发读写的需求
web2.0网站要根据用户个性化信息来实时生成动态页面和提供动态信息,所以基本上无法使用动态页面静态化技术,因此数据库并发负载非常高,往往要达到每秒上万次读写请求。关系数据库应付上万次SQL查询还勉强顶得住,但是应付上万次SQL写数据请求,硬盘IO就已经无法承受了。其实对于普通的BBS网站,往往也存在对高并发写请求的需求,例如像实时统计在线用户状态,记录热门帖子的点击次数,投票计数等,因此这是一个相当普遍的需求。
⒉Huge Storage ;– ;对海量数据的高效率存储和访问的需求
类似Facebook,twitter,Friendfeed这样的SNS网站,每天用户产生海量的用户动态,以Friendfeed为例,一个月就达到了2.5亿条用户动态,对于关系数据库来说,在一张2.5亿条记录的表里面进行SQL查询,效率是极其低下乃至不可忍受的。再例如大型web网站的用户登录系统,例如腾讯,盛大,动辄数以亿计的帐号,关系数据库也很难应付。
⒊High Scalability && High Availability- ;对数据库的高可扩展性和高可用性的需求
在基于web的架构当中,数据库是最难进行横向扩展的,当一个应用系统的用户量和访问量与日俱增的时候,你的数据库却没有办法像web server和app server那样简单的通过添加更多的硬件和服务节点来扩展性能和负载能力。对于很多需要提供24小时不间断服务的网站来说,对数据库系统进行升级和扩展是非常痛苦的事情,往往需要停机维护和数据迁移,为什么数据库不能通过不断的添加服务器节点来实现扩展呢?
在上面提到的“三高”需求面前,关系数据库遇到了难以克服的障碍,而对于web2.0网站来说,关系数据库的很多主要特性却往往无用武之地,例如:
⒈数据库事务一致性需求
很多web实时系统并不要求严格的数据库事务,对读一致性的要求很低,有些场合对写一致性要求也不高。因此数据库事务管理成了数据库高负载下一个沉重的负担。
⒉数据库的写实时性和读实时性需求
对关系数据库来说,插入一条数据之后立刻查询,是肯定可以读出来这条数据的,但是对于很多web应用来说,并不要求这么高的实时性,比方说我(JavaEye的robbin)发一条消息之后,过几秒乃至十几秒之后,我的订阅者才看到这条动态是完全可以接受的。
⒊对复杂的SQL查询,特别是多表关联查询的需求
任何大数据量的web系统,都非常忌讳多个大表的关联查询,以及复杂的数据分析类型的复杂SQL报表查询,特别是SNS类型的网站,从需求以及产品设计角度,就避免了这种情况的产生。往往更多的只是单表的主键查询,以及单表的简单条件分页查询,SQL的功能被极大的弱化了。
因此,关系数据库在这些越来越多的应用场景下显得不那么合适了,为了解决这类问题的非关系数据库(NoSQL)应运而生。

第二,新型的非关系型数据库(NoSQL)
有键-值(key-value)存储、key-结构化数据存储、key-文档存储、BigTable存储、图存储等多种存储模型。只提供基本的操作(增/改/删等),复杂的查询靠主程序在应用层实现。
代表性产品有:MongoDB、Cassandra、CouchDB、Redis、Memcache等。
特点:
(1)灵活的数据模型-Schemeless
NoSQL数据库种类繁多,但是一个共同的特点都是去掉关系数据库的关系型特性。数据之间无关系,这样就非常容易扩展。也无形之间,在架构的层面上带来了可扩展的能力。NoSQL无需事先为要存储的数据建立字段,随时可以存储自定义的数据格式。这带来了极大的扩展灵活性,而这在敏捷开发中是最重要的。而在关系数据库里,增删字段是一件非常麻烦的事情。如果是非常大数据量的表,增加字段简直就是一个噩梦。这点在大数据量的web2.0时代尤其明显。
(2)大数据量,高性能
NoSQL数据库都具有非常高的读写性能,尤其在大数据量下,同样表现优秀。这得益于它的无关系性,数据库的结构简单。一般MySQL使用 Query Cache,每次表的更新Cache就失效,是一种大粒度的Cache,在针对web2.0的交互频繁的应用,Cache性能不高。而NoSQL的 Cache是记录级的,是一种细粒度的Cache,所以NoSQL在这个层面上来说就要性能高很多了。同样,由于不受“表”的约束,数据集合可以非常之大。
(3)高可用和扩展性
NoSQL在不太影响性能的情况,就可以方便的实现高可用的分布式架构。比如Cassandra,HBase模型,通过复制模型也能实现高可用。这很适于网站的扩充。


我的数据库选型

嵌入式平台:SQLITE,TaffyDB

嵌入式平台:SQLITE和TaffyDB
分为两个方向:工控板和移动终端。在工控板上,主要能支持嵌入式linux(如NetBSD或Ubuntu embedded),就可以运行web-nodekit,就和桌面端一样了。完全可以使用同样的数据库技术。当然,工控板上应用的数据库不会太大,但反应速度要快,而且工控板很稳定,轻易不死机。可以选用TaffyDB。
对于手机/平板电脑等Android/IOS/WindowsPhone 等移动终端,可以使用PhoneGap配合localStorage。后者其实是基于sqlite的。

TaffyDB是一个基于JSON的纯内存嵌入式数据库,可以把JSON文件读到内存中成为对象,并把整个库再导出为一个JSON。提供了一些数据库CRUD的API。仅支持Javascript,十分简单易用。
对于很小的数据应用,完全可以用TaffyDB作为内嵌数据库。主要特点:
很小,只有10K左右
简单,JavaScript的语法
快速
易于集成到任何Web应用
兼容主流的Ajax库,例如:YUI, JQuery, Dojo, Prototype, EXT, etc
CRUD 接口 (Create, Read, Update, Delete)
排序
循环
高级查询
这就是浏览器上的SQL数据库:)  官网http://www.taffydb.com


Sqlite就不用说了,短小精悍,功能强大,是嵌入式数据库的经典。各种语言接口见其官网。


桌面平台:EJDB

为什么使用NoSQL
我们对非结构化数据的存储和处理需求日增,在这个变化的世界,互联网领域的应用可能越来越难像软件开发一样,去预先写义各种数据结构。
我们的应用场景对一致性,隔离性以及其它一些事务特性的需求可能越来越低,相反的,我们对性能,对扩展性的需求可能越来越高。再加之NoSQL中数据天然是“对象”类型的,无需ORM转换,因此,我决定选用NoSQL数据库。

EJDB 是一个嵌入式的 JSON 数据库引擎,旨在提供快速的类 MongoDB 的嵌入式数据库,可用于 C/C++ 应用程序中。主要特性包括:集合级别写锁、集合级别的事务、字符串匹配查询以及 Node.js 绑定。EJDB 修改自 Tokyo Cabinet. 基于 C BSON API 实现的 JSON 处理和查询。
选择EJDB的理由:
-功能完整的键-值NoSQL数据库;
-语法酷似MongoDB;
-嵌入式(不用单独安装和启动);
-支持Node.js
-多语言API;
官网https://ejdb.org

网站后台:MongoDB+PostgreSQL

网站后台需要承受海量访问和复杂的事务。对于非事务性业务使用MongoDB,对于事务性强的部分使用PostgreSQL
MongoDB是一个基于分布式文件存储的数据库,介于关系数据库和非关系数据库之间,是非关系数据库当中功能最丰富,最像关系数据库的。MongoDB最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。MongoDB支持RUBY,PYTHON,JAVA,C++,PHP,C#等多种语言。MongoDB是高性能开源文档数据库,也是目前最受关注的NoSQL技术之一,以敏捷、可扩展和对企业应用友好(支持事务,一致性和数据完整性保证,有大企业应用案例)而著称。有人甚至认为LAMP中的M应该用MongoDB取代MySQL,其火热程度可见一斑。使用MongoDB的公司包括Foursquare, Craiglist, 迪士尼,SAP,Intuit,EA等,国内淘宝、大众点评、视觉中国等公司有应用。
PostgreSQL是以加州大学伯克利分校计算机系开发的 POSTGRES,现在已经更名为POSTGRES,版本 4.2为基础的对象关系型数据库管理系统(ORDBMS)。PostgreSQL支持大部分 SQL标准并且提供了许多其他现代特性:复杂查询、外键、触发器、视图、事务完整性、MVCC。同样,PostgreSQL 可以用许多方法扩展,比如, 通过增加新的数据类型、函数、操作符、聚集函数、索引方法、过程语言。并且,因为许可证的灵活,任何人都可以以任何目的免费使用、修改、和分发 PostgreSQL,不管是私用、商用、还是学术研究使用。
事实上, PostgreSQL 的特性覆盖了 SQL-2/SQL-92 和 SQL-3/SQL-99,首先,它包括了可以说是目前世界上最丰富的数据类型的支持,其中有些数据类型可以说连商业数据库都不具备, 比如 IP 类型和几何类型等;其次,PostgreSQL 是全功能的*软件数据库,很长时间以来,PostgreSQL 是唯一支持事务、子查询、多版本并行控制系统(MVCC)、数据完整性检查等特性的唯一的一种*软件的数据库管理系统。 Inprise 的 InterBase 以及SAP等厂商将其原先专有软件开放为*软件之后才打破了这个唯一。最后,PostgreSQL拥有一支非常活跃的开发队伍,而且在许多黑客的努力下,PostgreSQL 的质量日益提高。
从技术角度来讲,PostgreSQL 采用的是比较经典的C/S(client/server)结构,也就是一个客户端对应一个服务器端守护进程的模式,这个守护进程分析客户端来的查询请求,生成规划树,进行数据检索并最终把结果格式化输出后返回给客户端。为了便于客户端的程序的编写,由数据库服务器提供了统一的客户端 C 接口。而不同的客户端接口都是源自这个 C 接口,比如ODBC,JDBC,Python,Perl,Tcl,C/C++,ESQL等, 同时也要指出的是,PostgreSQL 对接口的支持也是非常丰富的,几乎支持所有类型的数据库客户端接口。这一点也可以说是 PostgreSQL 一大优点。
缺点
从 Postgres 开始,PostgreSQL 就经受了多次变化。
首先,早期的 PostgreSQL 继承了几乎所有 Ingres, Postgres, Postgres95 的问题:过于学院味,因为首先它的目的是数据库研究,因此不论在稳定性, 性能还是使用方方面面,长期以来一直没有得到重视,直到 PostgreSQL 项目开始以后,情况才越来越好,PostgreSQL 已经完全可以胜任任何中上规模范围内的应用范围的业务。目前有报道的生产数据库的大小已经有 TB 级的数据量,已经逼近 32 位计算的极限。不过学院味也给 PostgreSQL 带来一个意想不到的好处:大概因为各大学的软硬件环境差异太大的缘故,它是目前支持平台最多的数据库管理系统的一种,所支持的平台多达十几种,包括不同的系统,不同的硬件体系。至今,它仍然保持着支持平台最多的数据库管理系统的称号。
其次,PostgreSQL 的确还欠缺一些比较高端的数据库管理系统需要的特性,比如数据库集群,更优良的管理工具和更加自动化的系统优化功能 等提高数据库性能的机制等。


SQL语言

SQL参考手册 http://www.w3school.com.cn/sql/sql_quickref.asp


TaffyDB的使用

利用json文件做数据库,用javascript封装了访问数据库的常见功能,所有操作在内存中进行,超快!完全面向对象。
TaffyDB其实是一个具有数据库功能的js库,可以打包在我的js应用中,在单用户使用情况下,不用另外装数据库了。
通过node使用TaffyDB


导入数据库模块
var TAFFY = require("taffy").taffy;
可以用console.log(TAFFY);打印出所有TaffyDB的方法。


读取数据库文件
数据库文件存储在JSON文件中,可用如下方法读取:
var fs = require('fs');
var TAFFY = require("taffy").taffy;


var data = fs.readFileSync('mydata.json')  //用nodejs的fs.readFileSync方法一次性把数据文件都都到内存中,用同步读以确保先执行完
var jsonObj = JSON.parse(data);             //将JSON字符串解析为对象
var db = TAFFY(jsonObj);                    //将此对象实例为一个taffyDB的数据库
                    


写数据库文件
要保存数据到磁盘文件(持久化),可用如下方法写:
var dbstream = db().stringify();                 //将taffyDB数据库转化为JSON数组
fs.writeFileSync('mydata2.json',dbstream);      //用nodejs的fs.writeFileSync方法将该数组写到一个文件中(每次都会覆盖原内容,重新写)


如果数据文件太大(5M+)应考虑用fs.readStream/writeStream方法,以免内存溢出。


但问题是:如果数据文件较大(1万条以上测试记录要150多M,全放到内存中太占用内存了,计算机的性能明显受影响,而且每写一次都要花很长时间。
所以,桌面一般还是用EJDB吧。只有确认很小的数据库采用TaffyDB。
    

EJDB的使用

EJDB是C语言写的可嵌入程序使用的动态查询库,脱胎于另一数据库TokyoCabinet,用法酷似MongoDB,API: C/C++ BSON, Python3, Node.js绑定。
支持读集合级写锁、事物、索引、集合级的联合(join)查询等。


安装 

进入$workspace/nodetest目录,运行npm install ejdb。本地安装!
Windows下可使用官网上的安装包。


以下介绍与nodejs结合使用(node API)
下面介绍的语句都是带回调的基本用法。如果不写回调,则变成同步的语法。写/更新/插入操作还是使用回调!否则大数据量时会影响响应。


数据库的打开和关闭

var EJDB = require("ejdb");
//Open zoo DB
var jb = EJDB.open("zoo", EJDB.DEFAULT_OPEN_MODE);
open 方法有两个参数,数据库名(字符串)和打开方式。打开方式有4种:
JBOREADER Open as a reader.
JBOWRITER Open as a writer.
JBOCREAT Create if db file not exists
JBOTRUNC Truncate db.
默认的是openMode=JBOWRITER | JBOCREAT。一般使用默认的就可以了。
数据库文件会默认生成在当前文件夹下(默认大小是520k),如果要设定数据库文件位置,可用"var/zoo"这样的路径。
//close the database
jb.close(); 
关闭数据库的命令要放在最后一个数据库操作的函数里(嵌套)。如果放在外面可能会出错。

这是阻塞命令(只能同步顺序执行的命令)。

注意:每次关系浏览器或用Ctrl-C中止WEB服务都 不会自动关闭EJDB,当每次重新加载var jb = EJDB.open("zoo", EJDB.DEFAULT_OPEN_MODE);命令时,都会导致重新打开数据库,致使数据库性能极具下降,体积变大,多开几次数据库,就瘫痪了。所以要在每次打开之前先关闭一下。如:

var databaseUrl = "./app/data/bca"; 
var EJDB = require("ejdb");
//Open DB
var db = EJDB.open(databaseUrl, EJDB.DEFAULT_OPEN_MODE);
db.close();                                                                                                         //first close possible opened db to prevent re-open.
var jb = EJDB.open(databaseUrl, EJDB.DEFAULT_OPEN_MODE);   //open the database as a new handler.
console.log("database bca is opened.");

这样的写法才稳妥。


集合的建立和删除

其实集合在存入数据时会自动建立,但事先建立并设定参数较好。这是阻塞命令(只能同步顺序执行的命令)
ensureCollection(cname, copts)
自动创建集合。cname是集合的名字(字符串),copts是可选参数。
Collection options (copts):
cachedrecords : 内存中缓存的记录数. Default: 0。我一般设1000条。
records : 预计的此集合中的记录数. Default: 65535.
large : 指定数据库尺寸可能大于2GB. Default: false
compressed : 如果选是则进行 DEFLATE压缩. Default: false. 如果不压缩,一个默认数据库是1.5M。压缩的才520K。
在实例要先设一个copts对象,再建集合。
//establish a collection   //标准参数
var copts = {
        cachedrecords : 1000,
        records : 65535,
        compressed : true,
        large : false
    };
jb.ensureCollection("collection1", copts);


删除集合用如下命令:
dropCollection(cname,prune,cb)  cname是集合的名字,prune是是否删除,cb是回调函数。
这种删除只是从内存中清除集合,如果要从硬盘上完整的删除要使用参数和回调:
jb.dropCollection("collection1", true, callback);
例子:
jb.dropCollection("collection1", true, function(err) {
if (err)
console.log(err);
else
console.log("collection1 removed.")
    });


数据的插入

save(cname, <json object>|<Array of json objects>, [cb])
使用save命令把json对象(一般都事先定义好)或对象的数组插入到集合中,如果不设回调就是同步运行。对象中不得包含$和.号。
cname是指定的集合。所有的EJDB记录都会包括自动生成的oid,这是类似MongoDB中的序号,在记录中表现为_id。如果同步运行,则返回所有插入的oid,如果异步执行,则返回undefined.
例子:
jb.save("parrots", [parrot1, parrot2], function(err, oids) {
    if (err) {
        console.log(err);
        return;
    }
    console.log("Grenny OID: " + parrot1["_id"]);
    console.log("Bounty OID: " + parrot2["_id"]);
});
那么,如何把整个JSON文件导入呢?可以把这个JSON文件解析到内存中作为一个对象,再把对象save到数据库中。JS/CS程序可以读取整个json文件到内存中,构成一个对象,对其进行各种对象操作。首先要做的是解析一个JSON文件为一个对象。使用JSON.parse方法: var data = JSON.parse('mydata.json'); 就可以把整个mydata.json解析为一个对象--data。方便地访问它的各种属性和方法。


数据的查询

1.查询全部内容。

查询条件处不写任何内容,利用cursor.next()循环
jb.find("parrots", function(err, cursor, count) {
    if (err) {
        console.error(err);
        return;
    }
    console.log("Found " + count + " parrots");
    while (cursor.next()) {
        console.log(cursor.field("name") + " " + cursor.field("_id"));
    } 
});
如果用cursor.object()代替具体的field就可以以对象的模式查询出所有记录。

只返回符合条件的头一条记录。使用$findOne语句。语法:findOne(cname, qobj, orarr, hints, cb)。

例如:
jb.findOne("parrots",
    {'name': "Grenny"},
    function(err,obj) {
        if (err) {
            console.log(err);
            return;
        }
        console.log("This is findOne result:")
        console.log(obj);
    });


2.简单条件查询

将单一的查询条件写在{}中,放在find里作为第一个参数。如:


jb.find("parrots", {"name":"Bounty"}, function(err, cursor, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Found " + count + " parrots");
        while (cursor.next()) {
            console.log(cursor.object());
        } 
 });
其中{"name":"Bounty"}就是查询name项的值是"Bounty"的记录。
大小写不敏感的查询用{'fpath' : {'$icase' : 'val1'}} 模式。
如果只想返回符合条件的第一条,用findOne语句。语法:findOne(cname, qobj, orarr, hints, cb)


3.组合条件查询

或查询  {$or : [{"item1" : "value1}, {"item2 : "value2"}]},
将各种条件放在一个数组中,前面放一个$or:
jb.find("parrots", {$or: [{"male":true}, {"age":15}]}, function(err, cursor, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Found " + count + " parrots");
        while (cursor.next()) {
            console.log(cursor.object());
        } 
 });
这是查"male"是true或"age"是15的记录。


与查询  {$and : [{"item1" : "value1}, {"item2 : "value2"}]},
将各种条件放在一个数组中,前面放一个$and:
jb.find("parrots", {$and: [{"male":true}, {"age":15}]}, function(err, cursor, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Found " + count + " parrots");
        while (cursor.next()) {
            console.log(cursor.object());
        } 
 });
这是查"male"是true且"age"是15的记录。


与或联合查询
查询条件状如{$and : [ {$or : [{a : 1}, {b : 2}]}, {$or : [{c : 5}, {d : 7}]} ] },
最好分行写,层次清晰些。例如:
jb.find("parrots", 
    {$and: [ {$or : [{"male" : true}, {"age" : 15}]}, 
             {$or : [{"type" : "Cockatoo"}, {"extral" : null}]} ]} ,
    function(err, cursor, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Found " + count + " parrots");
        while (cursor.next()) {
            console.log(cursor.object());
        } 
 });


4.否定查询

 {'fpath' : {'$not' : val}}                 //列值不等于val
如:
jb.find("parrots", 
     {"male": {'$not': false}}, 
    function(err, cursor, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Found " + count + " parrots");
        while (cursor.next()) {
            console.log(cursor.object());
        } 
 });


 {'fpath' : {'$not' : {'$begin' : prefix}}} //列值不以val为前缀
如:
jb.find("parrots", 
     {"name": {'$not': {'$begin': "G"}}}, 
    function(err, cursor, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Found " + count + " parrots");
        while (cursor.next()) {
            console.log(cursor.object());
        } 
 });


5.前缀查询

格式: {'fpath' : {'$begin' : prefix}}
如:
jb.find("parrots", 
     {"name": {'$begin': "G"}}, 
    function(err, cursor, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Found " + count + " parrots");
        while (cursor.next()) {
            console.log(cursor.object());
        } 
 });


6.范围查询

对于数字项,可以使用$gt, $gte (>, >=) 和 $lt, $lte (<, <=) 来查询
格式:{'fpath' : {'$gt' : number}, ...}
jb.find("parrots", 
     {"age": {'$gt': 10}}, 
    function(err, cursor, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Found " + count + " parrots");
        while (cursor.next()) {
            console.log(cursor.object());
        } 
 });
也可以用$bt(between)来查询某个范围内的记录
格式: {'fpath' : {'$bt' : [num1, num2]}}
如:
jb.find("parrots", 
     {"age": {'$bt': [0,10]}}, 
    function(err, cursor, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Found " + count + " parrots");
        while (cursor.next()) {
            console.log(cursor.object());
        } 
 });


对于字符串/数字/数组,均可用in关键字查询在其中的选项。
格式 {'fpath' : {'$in' : [val1, val2, val3]}} 
如:
jb.find("parrots", 
     {"age": {'$in': [0,10,15]}}, 
    function(err, cursor, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Found " + count + " parrots");
        while (cursor.next()) {
            console.log(cursor.object());
        } 
 });
如果用$nin则表示(not in)。


7.存在查询

根据某项是否存在查询。
语法: {'fpath' : {'$exists' : true|false}}
如:
jb.find("parrots", 
     {"extral": {'$exists': true}}, 
    function(err, cursor, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Found " + count + " parrots");
        while (cursor.next()) {
            console.log(cursor.object());
        } 
 });
注意:如果此字段存在,但值是null也是查不出来的。


8.查询中的hint参数

在查询中可以设置hint参数,写在回调的前面,对查询结果加以约束。包括:
  - $max 设置结果最多输出几项
  - $skip 跳过结果集的头几项
  - $orderby 根据某项的排序(1:正序;-1:倒序)输出结果
  - $onlycount true|false If `true` 只输出符合的记录数,而不输出具体记录。(写了这条后面就不能再写输出结果的打印语句了)。
  - $fields 设结果中只输出某几项
例如:
jb.find("parrots",
     {"name": "Grenny"},
     {"$orderby" : {"birthdate" : -1}},   //按出生日降序排列
    function(err, cursor, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Found " + count + " parrots");
        while (cursor.next()) {
            console.log(cursor.object());
        } 
    });
多个hint参数用{}括起来,里面用,分隔:
jb.find("parrots",
     {"name": "Grenny"},
     { '$orderby' : { "birthdate": - 1},  //按生日降序排列
       '$fields'  : { "_id": 1, "name": 1, "birthdate": 1}, //只显示id,name,birthdate三项
       '$max'     : 3                    //最多输出三条结果
     },
    function(err, cursor, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Found " + count + " parrots");
        while (cursor.next()) {
            console.log(cursor.object());
        } 
    });


数据的更新

使用jb.update语句。

1.条件更改

首先,使用查询语句将需要更新的记录查出来,然后,使用 '$set' : {'field1' : val1, 'fieldN' : valN}的语法,设置要更新的字段和更新的值。
如:
jb.update("parrots", 
     {'name': "Bounty", '$set': {"birthdate": "2013-08-05"}},  
    function(err, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Update " + count + " parrots");
 });
//将名字叫"Bounty“的鹦鹉的出生日期改为"2013-08-05"。
如果不写查询条件,就会把所有记录的"birthdate"项都改了。
下面这个例子是复杂查询的修改,将年龄大于10岁的母鹦鹉出生日期改为2013-08-05。
jb.update("parrots", 
     {$and : [{'age': {'$gt': 10}},{'male':false}], '$set': {"birthdate": "2013-08-05"}},  
    function(err, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Update " + count + " parrots");
 });


2.更改对象的结构(记录的字段)

使用$upsert, 有这个字段则更新值,没有这个字段则新建之。
语法:{查询条件, '$upsert' : {'field1' : val1, 'fieldN' : valN}}
如:
jb.update("parrots", 
     {"name": "Bounty", $upsert: {"birthdate": "2013-08-05", "sing": "jiujiujiu"}},  
    function(err, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Update " + count + " parrots");
 });


3.数字递增

对于值是数字的项,可以用$inc来增加某数量。语法:{查询条件, '$inc' : {'field1' : number, ..., 'field1' : {number}}
例如:给Bounty鹦鹉的年龄增加3岁。
jb.update("parrots", 
     {"name": "Bounty", $inc: {"age": 3}},  
    function(err, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Update " + count + " parrots");
 });


4.记录删除

可以在update中使用$dropall删除满足条件的记录
如:删除所有名字叫Bounty的鹦鹉的所有字段。
jb.update("parrots", 
     {"name": "Bounty", '$dropall' : true},  
    function(err, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Update " + count + " parrots");
 });

对于“与”关系的组合结果的删除,不要用$and,而要把各个字段的要求排列出来,用逗号分割。如:
jb.update("TestRecord", {"cid":cid, "tid":tid, '$dropall' : true});

5.数组内容的增加和减少

有些项的值是数组,要往里增加内容,可以用$addToSet命令。语法:{查询条件, '$addToSet' : {'fpath' : val1, 'fpathN' : valN, ...}}
如,给鹦鹉Bounty喜爱的东西里增加竹子一项:
jb.update("parrots", 
    {"name": "Bounty", '$addToSet' : {'likes': "bamboo"}},
    function(err, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Update " + count + " parrots");
 });
如果要增加多项,需要使用$addToSetAll命令:
jb.update("parrots", 
    {"name": "Bounty", '$addToSetAll' : {'likes': ["bamboo", "rice"]}},
    function(err, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Update " + count + " parrots");
 });
减少数组的内容,可以用$pull(删除一个值)和$pullAll(删除多个值)。语法:
{查询语句, '$pull' : {'fpath' : val1, 'fpathN' : valN, ...}}
{查询语句, '$pullAll' : {'fpath' : [val1, valN, ...]}}
例如:
jb.update("parrots", 
    {"name": "Bounty", '$pullAll' : {'likes': ["bamboo", "rice"]}},
    function(err, count) {
        if (err) {
            console.error(err);
            return;
        }
        console.log("Update " + count + " parrots");
 });
删除了'likes'项中的"bamboo", "rice"两个值。


记录数的统计

使用count语句统计符合条件的记录数。语法:count(cname, qobj, orarr, hints, cb)
jb.count("parrots",
    {"name": "Grenny"},
    function(err, count) {
        if (err) {
            console.log(err);
            return;
        }
        console.log("Parrot Grenny has: " + count);
    });


数据的删除

只能用oid来删除
remove(cname, oid, cb) 
如果不设回调就是同步运行。
怎么知道某条记录的oid呢? 一般要先用find语句查出来。如果删除某条记录就用findOne.
parrot = jb.findOne("parrots, {"name": "Bounty"};
parrotID = parrot._id;
jb.remove("parrots", parrotID, function(err) {
    if (err) {
        console.log(err);
        return;
        }
        console.log("parrot2 is removed.")   
});

通常,在不知道ID的情况下,可以用update语句中的$dropall命令来执行条件删除。


数据同步

可以用sync()语句来同步整个数据库到硬盘文件上。如:
jb.sync(function(err) {
       if (err) {
            console.log(err);
            return;
        }
        console.log("database synced");
    });


数据的导出

load(cname, oid, cb)
从集合中将OID标示的记录导出为一个JSON对象。这只能用在知道记录id的情况下。
{String} cname 集合的名字
{String} oid 记录的ID号
{Function} cb 回调函数 参数: (error, obj) obj: 取出的JSON对象或 NULL(如果没有找到的话.
返回:
同步模式:JSON对象或 NULL(如果没有找到的话.异步模式: {undefined}.
例子:
parrotObj = jb.load("parrots", parrot1["_id"]);
console.log("parrot1 loaded.see:\n ");
console.log(util.inspect(parrotObj, {showHidden: true, colors: true}));
这里*采用同步模式,因为对象还没有赋值,则无法用util.inspect显示出来。
这种基本语法只能导出一条数据,如果要导出多条数据,则需要使用循环。


JSON-内存对象-数据库集合

不同系统之间交互数据经常要通过JSON,而且有可能是很大的JSON。这需要频繁地把JSON文件导入数据库;同时,也需要把数据库的集合(表)中的内容导出为JSON文件,发送给其他系统。
首先,需要明确,不论MongoDB还是EJDB,存储数据用的都是bason格式,而不是JSON,所以不能直接把数据库文件拷贝传递。
把JSON文件导入数据库要分两步:首先,把JSON文件读入内存(使用fs.readFile),在内存中解析(parse)为一个对象(使用JSON.parse)。第二步,把这个对象存入数据库中(EJDB使用save方法,MongoDB使用insert方法)。CS示例代码如下:
fs.readFile("data/mydata.json",cb = (err,data) ->
    mydata = JSON.parse(data)
    console.log("The data file you parsed is " + mydata.name)
    db.save("parrots", mydata, cb = (err,oids) ->
      console.log("mydata saved.")
注意:为保证执行顺序,要把存入数据库的动作写在readFile的回调函数里。
这种方法对于较大的JSON文件要慎重使用,因为parse一个巨大的JSON文件是很耗费时间和内存的。
把数据库的某个集合(collection)存为一个JSON文件要复杂些。TaffyDB有个db.stringfy()方法可以直接把集合内容输出为字符串,然后把这个字符串写入某个JSON文件即可。如:
//export all records to JSON string
    var dbstream = db().stringify();
    
    fs.writeFile('mydata2.json',dbstream,function(err){
      if(err) throw err;
      console.log('has finished');
    });
EJDB和MongoDB就没有这样方便的方法,第一步,要用查询的方法把集合中的全部记录一条一条都查询出来。在EJDB中是用cursor的滚动,在MongoDB是功能使用find.each方法。把所有查出来的记录循环写入到一个JSON数组中(这里用JSON数组,而不是JSON对象,是因为使用了数组的push方法来连缀所有对象到一个数组中)。然后再把这个数组字符串化(使用JSON.stringify),然后用fs.writeFile方法写入到一个JSON文件中。
这个方法唯一的缺点就是数组是一个“整体对象”,字符串化后难以分行,JSON文件不好读懂。不过程序来“读”倒是没有问题。
CS写的完整的导出代码:
    db.find("parrots", cb = (err, cursor, count) ->
    parrotObj = []
    while cursor.next()
            parrotObj.push(cursor.object())            
        console.log(parrotObj)
        parrotObjStr = JSON.stringify(parrotObj)
        console.log(parrotObjStr)
        fs.writeFile('data/backup.json', parrotObjStr, cb = (err) ->
            console.log('backup JSON is saved!')


coffeescript在数据库中的应用

数据库的创建、连接和关闭,集合的新建和删除,内容的插入、查询、更新、删除,JSON数据的导入导出,数据库的备份。还有异步的问题,都可以通过CS来解决。
首先,确认coffeescipt已经在全局模式下装好。完整的操作EJDB数据库的例子参见ejdb.cs。



MongoDB的使用

Mongodb的好处

-本身面向对象,省略了ORM;
-在客户端和服务器端统一;
-支持未来的数据多样化;
-在大数据量和可扩展性方面表现优异;
-和nodejs配合良好;
-支持完全的查询,类似标准SQL。
起决定作用的是可扩展性,因为我的程序要不断变化,如果数据库太刚性,调整结构是很不容易的。而Mongodb没有数据库模型,可以随时变化起结构。
缺点:基于C++,需要单独安装和启动。可以给用户提供安装包,设为随系统自动启动。数据单独存储:可以设使用的数据库为Mongodb安装路径下的\data\db,log文件同理。(将来项目数据文件要拷贝进来)。Windows下的多版本:分别提供Windows XP/Windows 7-32bit/Windows7-64bit三个版本。


Mongodb的数据类型
Float,Double Class, Integers, Long Class, String, Date,RegExp, Binary Class, Code Class,ObjectID Class,DbRef Class, Symbol Class


MongoDB的安装

Windows:mongodb2.2.2以上只支持Windows7以上系统,所以要在windowsXP上使用请下载历史版本(2.0.9)。从官网下载预编译版本,解压缩到某路径下。在其中新建/data/db用于存储数据,/log用于存储日志(要新建一个mongod.log空文件)。然后写一个mongod.cfg配置文件,其中就两句:
dbpath=E:/mongodb/data
logpath=E:/mongodb/log/mongod.log
用下述命令注册为Windows服务,这样每次就可以自动启动了:
E:\mongodb\bin\mongod.exe --config E:\mongodb\mongod.cfg --install
手动启动(或关闭)其服务进程可用如下命令:
net start mongodb
net stop mongodb

Ubuntu:按照官网www.mongodb.org的install 指南
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv 7F0CEB10 (添加验证key)
echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/10gen.list
(添加源)
sudo apt-get update  (更新源)
sudo apt-get install mongodb-10gen (安装包)   在我的笔记本上按这种方式安装的是32位的版本(V2.4.3),支持的数据文件最大2GB。64位的运行不了。
下载完后会自动增加mongodb用户组和mongodb用户。
对于今后要打包的项目,还有简易安装方法,见http://docs.mongodb.org/manual/tutorial/install-mongodb-on-linux
下载32位的(因为我的笔记本是32位的)mongodb-linux-x86_i686-2.4.3.tgz, 解压缩后拷贝到software目录下改名为mongodb.在命令行进入该文件夹,执行如下命令:mkdir -p data/db建立数据存储文件夹。进入bin目录,运行./mongod --dbpath ../data/db,更改数据文件目录为刚刚建立data/db,这会启动mongod,在data/db/下自动建立几个初始文件:_tmp, local.0, local.ns,mongod.lock。运行mongod 要在bin目录下用命令行使用./mongod 命令,但每次启动都要加参数,--dbpath ../data/db,很烦。能否写个配置文件在启动时自动调用呢? 这需要先写config 文件(mongodb.conf),再写control stript(用upstart).
只要把我做的/mongodb文件夹中的mongodb.conf拷贝到/etc/init下,把mongodb拷贝到/etc/init.d下就可以了。其他所有的文件夹都在我的/mongodb中。
所有数据文件存放在/var/lib/mongodb里面,所有日志文件存放在/var/log/monogodb中。这样数据与程序分离,便于以后升级程序和安全。这两个文件夹会在初次mongodb运行时由mongodb.conf自动创建。

#用upstart管理mongodb的启动
Upstart是linux系统现在的启动管理工具。只要在/etc/init下放一个mongod.conf文件,这是一个文本文件,.conf前面的就是upstart的任务(job)的名字。
设完conf文件后,可以使用如下控制台命令(在任何目录下)控制mongodb:
sudo start monogod
sudo stop mongod
sudo restart mongod
sudo status mongod

$ initctl list 列出所有启动要运行的进程
./mongod --help 查询帮助


Mongodb Shell的操作

mongo是mongodb默认的客户端程序,需要进入/bin目录,运行./mongo启动。
集合=表,文档=记录, 域=列, _id=主键
>help 命令查看帮助,常用的是show dbs, show collections, show users, use db(使用某数据库),db(显示当前数据库名),exit命令。
测试看是否连上test数据库:
> db.test.save( { a: 1 } ) //自动往test里插入一条记录
> db.test.find()   //查询test集合
默认连接的是test数据库,但只有插入一条以上记录后才能显示出来(show dbs)。
注意:集合和数据库都不用事先创建(也没有模型可以创建),用到了自然会被创建出来。
如下命令创建了数据库mydb,集合things,并插入了一个文档(记录),并把内容都查出来了。
>use mydb
>db.things.insert({x:3})
>db.things.find()
*mongodb用游标(cursor)返回结果,默认一次只返回20个结果,如果要看更多的需要使用it命令再调出下20个。这是为了方式数据库被大数据量的显示而拖住。
如果想要一次显示所有结果,可以使用如下命令:
>var c = db.things.find()
//Print the full result set by using a while loop to iterate over the c variable:
while ( c.hasNext() ) printjson( c.next() )

当然,可以显式地创建文档 db.createCollection("users"),或删除文档db.users.drop()

查询语句:
查询所有文档   db.things.find()
查询第5条文档  var c = db.things.find
             printjson(c[4])
查询符合条件的某条(多条)文档   db.things.find( { name : "mongo" } )
查询第一条文档 db.things.findOne()
只返回前三条文档 db.tings.find().limit(3)


node.js操作Mongodb

使用node的驱动node-mongodb-native,安装方法 npm install mongodb  (我安的是1.3.5)

数据库连接与关闭

在MongoDB js driver中,有两个类用于连接mongo数据库。其一是经典的Db(),其二是新的MongoClient().


传统的Db()类连接方式--以前的代码都是这种形式.
Db.open()
Db.close()
Db.db()
Db.collection()


示例如下:
var Db = require('mongodb').Db,
    Server = require('mongodb').Server;


var db = new Db('test', new Server('localhost', 27017), {w:1});
// Establish connection to db
db.open(function(err, db) {
  
  db.collection('collection1', function(err,collection){
  // do something
  db.close();
 });
});
注意:db.close()必须放在最后一个操作函数的内部,如果放在外面,由于nodejs的异步特性,会在前面的操作执行完之前,先执行这个关闭动作,就出不来结果了。


collection操作

基本形式:将collection赋值给某个变量,后面调用它的方法。如:
var Db = require('mongodb').Db,
    Server = require('mongodb').Server;


var db = new Db('test', new Server('localhost', 27017), {w:1});


db.open(function(err, db) {
  
  var col1 = db.collection('collection1');
  var col2 = db.collection('collection2');
  
  col1.find().each(function(err, result) {
      if (result!= null) console.dir(result);
  });
  
  col2.find().each(function(err, result) {
      if (result!= null) console.dir(result);
      db.close();
  });
});


一种是新的MongoClient()类,常用的方法有
MongoClient = function(server, options);
MongoClient.open
MongoClient.close
MongoClient.db
MongoClient.connect


示例如下:
var MongoClient = require('mongodb').MongoClient,
    Server = require('mongodb').Server;
    
// Set up the connection to the local db
var mongoclient = new MongoClient(new Server("localhost", 27017, {w:1}));


// Open the connection to the server
mongoclient.open(function(err, mongoclient) {


  // Get the first db 
  var db = mongoclient.db("test");
  db.collection('collection1', function(err, result) {
    //do something
  });


  // Get another db 
  var db2 = mongoclient.db("test2");
  db2.collection('collection2', function(err, result) {
      //do something
     // Close the connection
      mongoclient.close(); 
  });  
});


还可以利用MongoClient.connect写成单行URL式。
mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
这是最简洁的,回避了new用法,和Server的require,可以加参数和连接多库,推荐标准形式!!!


同时连多个数据库和多个集合,示例如下:
var mongoClient = require('mongodb').MongoClient;
   
mongoClient.connect("mongodb://localhost:27017/test?w=1", function(err, db) {
  if(err) { return console.dir(err); }


  console.log("connected to db 'test'.");
  var col1 = db.collection('collection1');
  var col2 = db.collection('collection2');
  
  console.log("=======the follwing is content of collection1========");
  col1.find().each(function(err, result) {
    if (result!= null) console.dir(result);
  });
  
  console.log("=======the follwing is content of collection2========");
  col2.find().each(function(err, result) {
    if (result!= null) console.dir(result);
    db.close();
  });
});


mongoClient.connect("mongodb://localhost:27017/test2?w=1", function(err, db) {
  if(err) { return console.dir(err); }


  console.log("connected to db 'test2'.");
  var col3 = db.collection('collection3');
  var col4 = db.collection('collection4');
  
  console.log("=======the follwing is content of collection3========");
  col3.find().each(function(err, result) {
    if (result!= null) console.dir(result);
  });
  
  console.log("=======the follwing is content of collection4========");
  col4.find().each(function(err, result) {
    if (result!= null) console.dir(result);
    db.close();
  });
});


这将创建到本地机27017端口(mongod服务运行的默认端口)的连接,连接数据库test和test2(如果不存在就创建),默认允许的tcp连接是5个,可以通过poolSize参数来设定。注意:?w=1是连接的option,表示在数据库连接这一级就启动write concern。这样以后所有写操作就不用单写{w:1}了。
write concern是mongodb的一种写操作机制,原本mongodb是异步的,写操作执行结果如何不管就进入其他操作,加上write concern后mongodb会等待写操作返回结果(成功或失败)后再进入下一步。在程序中不加write concern就会无法立即显示数据库变化的结果。
关闭数据库使用内部对象db的close()方法。此方法要放在最后一个collection操作的内部。db.close()将发出close事件,关闭所有的db连接。


(Collection)集合的操作

创建
虽然mongodb可以不创建集合就插入数据(在插入时自动创建),但还是推荐先创建集合再使用。
db.createCollection('test', function(err, collection) {});
返回集合的名字:
console.log("Collection name: "+collection.collectionName)
列出现有集合:
db.collectionNames(function(err,collections){
  console.log(collections);
});
集合的删除:
db.dropCollection(collection_name,callback)


数据库的CRUD操作

关于mongodb的CRUD,必须记住一点,它是异步操作的,所有CRUD操作并不立即执行,而只有调用{w:1}参数才能确保它执行,加上错误返回函数才能返回显示结果。
注意:如果不带 {w:1}, function(err, result) {}, 会产生Error: Cannot use a writeConcern without a provided callback.但是,我在数据库连接级加入了?w=1参数,就不用在CRUD级加{w:1}了。但是必须带有{safe:true}才能在操作成功之后回调,否则来不及写操作就回调了文档对象的查询(query)。


文档对象的插入(insert)
  var doc2 = {'hello':'doc2'};
  var lotsOfDocs = [{'hello':'doc3'}, {'hello':'doc4'}];


  collection.insert(doc2, {safe:true}, function(err, records) {});  
  collection.insert(lotsOfDocs, {safe:true}, function(err, result) {});


文档内容的删除
collection.remove({mykey:2}, {safe:true}, function(err, result) {});
将{mykey:2}的文档全部删除


文档的修改(update)
 collection.update({mykey:1}, {$set:{fieldtoupdate:2}}, {safe:true}, function(err, result) {});
将mykey:1的文档的fieldtoupdate 域值设为2.这种操作默认是只更改符合条件的第一条记录。
*一次修改多条记录
 col2.update({'hello':'doc2'}, {$set:{‘hello’:‘2’}}, {safe:true, multi:true}, function(err, result) {});


文档对象的统计
    col2.count( function(err, count) {
      console.log("There are " + count + " records.");
    });


文档对象的查询(query)
查询所有记录
  col2.find().each(function(err, result) {
    console.log(result);
  });
查询符合条件的第一条记录
  console.log("=======the first content of query========");
  //query a collection the 1st records
  col2.findOne({hello:'doc1'},function(err, result) {
    console.log(result);
    db.close(); //最后一个函数中别忘了关闭数据库
  });
组合条件查询AND:查询hello项的值是'doc3'且item2项的值是'item2'的记录。
  col2.findOne({hello:'doc3', item2:'item2'},function(err, result) {
    console.log(result);
  });
组合条件查询OR: 查询hello项的值是'doc3'或'doc1'的记录。
col2.find().each($or:[{hello:'doc3', hello:'doc1'}],function(err, result) {
    //console.log(result);
    //db.close();
  //});

新建数据库
引入一个json文件或数组/对象
var friendList = [
{"id":1,"gender":"M","first":"John","last":"Smith",
    "city":"Seattle, WA","status":"Active"},
  {"id":2,"gender":"F","first":"Kelly","last":"Ruth",
    "city":"Dallas, TX","status":"Active"},
  {"id":3,"gender":"M","first":"Jeff","last":"Stevenson",
    "city":"Washington, D.C.","status":"Active"},
  {"id":4,"gender":"F","first":"Jennifer","last":"Gill",
    "city":"Seattle, WA","status":"Active"}  
];
将它定义为一个数据库
var friend_db = TAFFY(friendList);


数据的插入
friend_db.insert({"id":5,"gender":"F","first":"Peter","last":"Pan",
               "city":"Seattle, WA","status":"Active"});
数据的更新
friend_db({first:"John",last:"Smith"}).update({city:"Las Vegas, NV:"});
数据的删除
friend_db({id:5}).remove();


数据的查询
查全部记录
friend_db().each(function (r) {
   console.log(r);
});



附录:EJDB操作实例

用readline实现命令行交互选择执行某数据库操作。主语言:coffeescript.

#coffeescript operation of EJDB 

EJDB = require("ejdb")
util = require('util')
fs = require('fs')
readline = require("readline")
rl = readline.createInterface(process.stdin, process.stdout)

rl.setPrompt("test> ")
rl.prompt()

#=================database operation part==============================
#Establish zoo DB
db = EJDB.open("data/zoo", EJDB.DEFAULT_OPEN_MODE)
console.log("database zoo is opened.")
rl.prompt()

 #prepare the data
parrot1 = 
    "name" : "Grenny"
    "type" : "African Grey"
    "male" : true
    "age" : 1
    "birthdate" : new Date()
    "likes" : ["green color", "night", "toys"]
    "extra" : 321

parrot2 = 
    "name" : "Bounty"
    "type" : "Cockatoo"
    "male" : false
    "age" : 15
    "birthdate" : new Date()
    "likes" : ["sugar cane"]

confirmDB = () ->
  if db.isOpen() == true
    console.log("database is opened.")
  else
    console.log("database is not opened, openning now...")
    db = EJDB.open("data/zoo", EJDB.DEFAULT_OPEN_MODE)

#Close the database
closeDB = () ->
  db.close()
  console.log("database zoo is closed.")

#establish a collection,set its option
setCollection = () ->
  copts = 
    cachedrecords : 1000
    records : 65535
    compressed : true
    large : false

  db.ensureCollection("parrots",copts)
  console.log("parrot collection is established.")

#drop the collection
dropCollection = () ->
  db.dropCollection("parrots", prune = true, cb = (err) ->
    console.log("Collection parrots is dropped.")
    rl.prompt())

#insert objects to the collection
insertData = () ->
  fs.readFile("data/mydata.json",cb = (err,data) ->
    mydata = JSON.parse(data)
    console.log("The data file you parsed is " + mydata.name)
    db.save("parrots", mydata, cb = (err,oids) ->
      console.log("mydata saved.")
      rl.prompt()))

#add objects to the collection
saveData = () ->
  db.save("parrots", [parrot1, parrot2], cb = (err, oids) -> 
    console.log ("parrot data saved")
    rl.prompt())

#show all data in the collection
showData = () ->
	db.find("parrots", cb = (err, cursor, count) ->
        console.log("Found " + count + " records")
        while cursor.next()
            #console.log(cursor.field("name") + " " + cursor.field("_id"))
            console.log(cursor.object())
        rl.prompt())
  

#simple inquiry
simpleInquiry = () ->
    db.find("parrots", {"name":"Bounty"}, cb = (err, cursor, count) ->
        console.log("Found " + count + " parrots");
        while (cursor.next()) 
            console.log(cursor.object())
        rl.prompt())

#find one record
findOne = () ->
    db.findOne("parrots", {"name":"Bounty"}, cb = (err, obj) ->
        console.log("This is findOne result:")
        console.log(obj);
        rl.prompt())	

#'or' inquiry
orInquiry = () ->
	db.find("parrots", {$or: [{"male":true}, {"age":15}]}, cb = (err, cursor, count) ->
        console.log("Found " + count + " parrots");
        while (cursor.next()) 
            console.log(cursor.object())
        rl.prompt())

#'and' inquiry
andInquiry = () ->
	db.find("parrots", {$and: [{"male":true}, {"age":15}]}, cb = (err, cursor, count) ->
        console.log("Found " + count + " parrots");
        while (cursor.next()) 
            console.log(cursor.object())
        rl.prompt())

#'andor' inquiry
andorInquiry = () ->
	db.find("parrots", 
		{$and: [ {$or : [{"male" : true}, {"age" : 15}]}, 
                 {$or : [{"type" : "Cockatoo"}, {"extral" : null}]} ]} , 
		cb = (err, cursor, count) ->
            console.log("Found " + count + " parrots");
            while (cursor.next()) 
                console.log(cursor.object())
            rl.prompt())

#not inquiry
notInquiry = () ->
	db.find("parrots", {"male": {'$not': false}}, cb = (err, cursor, count) ->
        console.log("Found " + count + " parrots");
        while (cursor.next()) 
            console.log(cursor.object())
        rl.prompt())

#prefix inquriy
prefixInquiry = () ->
	db.find("parrots", {"name": {'$begin': "G"}}, cb = (err, cursor, count) ->
        console.log("Found " + count + " parrots");
        while (cursor.next()) 
            console.log(cursor.object())
        rl.prompt())

#'><' inquiry
gtInquiry = () ->
	db.find("parrots", {"age": {'$gt': 10}}, cb = (err, cursor, count) ->
        console.log("Found " + count + " parrots");
        while (cursor.next()) 
            console.log(cursor.object())
        rl.prompt())

#scope inquiry
scopeInquiry = () ->
	db.find("parrots", {"age": {'$bt': [0,10]}}, cb = (err, cursor, count) ->
        console.log("Found " + count + " parrots");
        while (cursor.next()) 
            console.log(cursor.object())
        rl.prompt())

#'in' inquiry
inInquiry = () ->
	db.find("parrots", {"age": {'$in': [0,10,15]}}, cb = (err, cursor, count) ->
        console.log("Found " + count + " parrots");
        while (cursor.next()) 
            console.log(cursor.object())
        rl.prompt())

#exist inquiry
existInquiry = () ->
	db.find("parrots", {"extra": {'$exists': true}}, cb = (err, cursor, count) ->
        console.log("Found " + count + " parrots");
        while (cursor.next()) 
            console.log(cursor.object())
        rl.prompt())

#hint inquiry
hintInquiry = () ->
	db.find("parrots", 
		{"name": "Grenny"},
        {'$orderby' : {"birthdate": - 1}, 
        '$fields'  : {"_id": 1, "name": 1, "birthdate": 1},
        '$max': 3 }, 
		cb = (err, cursor, count) ->
            console.log("Found " + count + " parrots");
            while (cursor.next()) 
                console.log(cursor.object())
            rl.prompt())

#simple update
simpleUpdate = () ->
	db.update("parrots", 
    {'name': "Bounty", '$set': {"birthdate": "2013-08-05"}},  
    cb = (err, count) ->
        console.log("Update " + count + " parrots")
        rl.prompt())

#combo update
comboUpdate = () ->
	db.update("parrots", 
    {$and : [{'age': {'$gt': 10}},{'male':false}], '$set': {"birthdate": "2013-08-05"}},  
    cb = (err, count) ->
        console.log("Update " + count + " parrots")
        rl.prompt())

#upsert records
upsertRecords = () ->
	db.update("parrots", 
    {"name": "Bounty", $upsert: {"birthdate": "2013-08-05", "sing": "jiujiujiu"}},  
    cb = (err, count) ->
        console.log("Update " + count + " parrots")
        rl.prompt())


#increment numbe
incrementNumber = () ->
	db.update("parrots", 
    {"name": "Bounty", $inc: {"age": 3}},  
    cb = (err, count) ->
        console.log("Update " + count + " parrots")
        rl.prompt())

#drop all
dropallRecords = () ->
	db.update("parrots", 
    {"name": "Grenny", '$dropall' : true},  
    cb = (err, count) ->
        console.log("Update " + count + " parrots")
        rl.prompt())

#add to set
addToSet = () ->
	db.update("parrots", 
    {"name": "Bounty", '$addToSet' : {'likes': "bamboo"}},  
    cb = (err, count) ->
        console.log("Update " + count + " parrots")
        rl.prompt())

#reduce set contents
reduceSet = () ->
	db.update("parrots", 
    {"name": "Bounty", '$pull' : {'likes': "bamboo"}},  
    cb = (err, count) ->
        console.log("Update " + count + " parrots")
        rl.prompt())


#count records
countRecords = () ->
	db.count("parrots", 
    {"name": "Bounty"},  
    cb = (err, count) ->
        console.log("There are " + count + " parrots fit in this condition")
        rl.prompt())

#delet records
deleteRecords = () ->
    parrot = db.findOne("parrots", {"name":"Bounty"})
    parrotID = parrot._id
    console.log(parrotID)
    db.remove("parrots", parrotID, cb = (err) ->
    	if err
    		console.log("This ID is not true.")
    	else
    	    console.log("parrot " + parrotID + " is removed.")
    	rl.prompt())

#syncronize the entire database
syncDB = () ->
	db.sync(cb = (err) ->
		if err
			console.log("db sync failed.")
		else
		    console.log("db sync successed.")
		rl.prompt())

#export records from a collection to a json file in disk
exportRecords = () ->
    db.find("parrots", cb = (err, cursor, count) ->
    	parrotObj = []
    	while cursor.next()
            parrotObj.push(cursor.object())            
        console.log(parrotObj)
        parrotObjStr = JSON.stringify(parrotObj)
        console.log(parrotObjStr)
        fs.writeFile('data/backup.json', parrotObjStr, cb = (err) ->
            console.log('backup JSON is saved!')
            rl.prompt()))


  
#console.log(util.inspect(parrotObj, {showHidden: true, colors: true}));
#====================interactive part===================================

rl.on('line', cb = (line) ->
  switch(line.trim()) 
    when "help" 
      console.log """
        -confirm db     : 1     -simple inquiry : 10    -'><' inqiry   :17    -simple update :30    -count  :40
        -close db       : 2     -find one       : 11    -scope inquiry :18    -combo update  :31    -export :41
        -set collection : 3     -'or' inquiry   : 12    -'in' inquiry  :19    -upsert        :32    -delete :42
        -drop collecton : 4     -'and' inquiry  : 13    -exist inquiry :20    -increment     :33    -sync   :43
        -save data      : 5     -'andor' inquiry: 14    -hint inquiry  :21    -dropall       :34
        -insert data    : 6     -'not' inquiry  : 15                          -add to set    :35
        -show data      : 7     -prefix inquiry : 16                          -reduce content:36
        """
    when "1" 
      confirmDB()
    when "2" 
      closeDB() 
    when "3" 
      setCollection()
    when "4"
      dropCollection()
    when "5"
      saveData()
    when "6"
      insertData()
    when "7"
      showData()
    when "10"
      simpleInquiry()
    when "11"
      findOne()
    when "12"
      orInquiry()
    when "13"
      andInquiry()
    when "14"
      andorInquiry()
    when "15"
      notInquiry()
    when "16"
      prefixInquiry()
    when "17"
      gtInquiry()
    when "18"
      scopeInquiry()
    when "19"
      inInquiry()
    when "20"
      existInquiry()
    when "21"
      hintInquiry()
    when "30"
      simpleUpdate()
    when "31"
      comboUpdate()
    when "32"
      upsertRecords()
    when "33"
      incrementNumber()
    when "34"
      dropallRecords()
    when "35"
      addToSet()
    when "36"
      reduceSet()
    when "40"
      countRecords()
    when "41"
      exportRecords()
    when "42"
      deleteRecords()
    when "43"
      syncDB()
    when ''
       rl.prompt()  
    else
      console.log("There's no command `" + line.trim() + "`")

  rl.prompt()
)

rl.on('SIGINT', cb = () ->
  rl.question("Are you sure you want to exit?", cb = (answer) ->
    if (answer.match(/^y(es)?$/i)) 
      rl.close()
    else 
      rl.prompt()
  )
)