1. NoSQL和文档
1.1 CouchDB
CouchDB提供了JavaScript环境下基于MVCC的文档存储。在CouchDB里添加或修改文档时,整个数据集都会保存到存储上,并且把老版本标记为过时的。每当创建一个完整的新版本时,都会写入到连续的内存中。
CouchDB的一大优点是,它的API全都是HTTP接口。因此我们可以直接对数据库操作,而不需要借助于其他客户端,例如:
var http = require('http'); http.createServer(function(req, res) { var client = http.createClient(5984, '127.0.0.1'); var request = client.request('GET', '/_all_dbs'); request.end(); request.on('response', function(response) { var responseBody = ''; response.on('data', function(data) { responseBody += data; }); response.on('end', function() { res.writeHead(200, {'Content-Type': 'text/plain'}); res.write(responseBody); res.end(); }); }); }).listen(8080);
如前所述,所有的CouchDB方法都是HTTP调用的,因此,创建和删除数据库分别是通过向服务器提交相应的PUT和DELETE语句来实现的,例如:
var client = http.createClient(5984, '127.0.0.1'); var request = client.request('PUT', '/dbname'); request.end(); request.on('response', function(response) { response.on('end', function() { if (response.statusCode == 201) { console.log('Dababase successfully created.'); } else { console.log('Could not create dababase.'); } }); });
删除资源用的是PUT的反操作:DELETE命令,HTTP代码返回200确认了该请求已成功执行,例如:
var client = http.createClient(5984, '127.0.0.1'); var request = client.request('DELETE', '/dbname'); request.end(); request.on('response', function(response) { response.on('end', function() { if (response.statusCode == 200) { console.log('Deleted database.'); } else { console.log('Could not delete dababase.'); } }); });
用HTTP来使用CouchDB是很有用的,但这个方法太繁琐,大多数程序依然倾向于使用更高级的抽象级的工具。使用CouchDB驱动来创建表的代码如下:
var dbHost = '127.0.0.1'; var dbPort = 5984; var dbName = 'users'; var couchdb = require('felix-couchdb'); var client = couchdb.createClient(dbPort, dbHost); var db = client.db(dbName); db.exists(function(err, exists) { if (!exists) { db.create(); console.log('Database ' + dbName + ' created.'); } else { console.log('Database' + dbName + ' exists.'); } });
下例演示如何在CouchDB中创建一个文档:
var dbHost = '127.0.0.1'; var dbPort = 5984; var dbName = 'users'; var couchdb = require('felix-couchdb'); var client = couchdb.createClient(dbPort, dbHost); var user = { name: { first: 'John', last: 'Doe' } }; var db = client.db(dbName); db.saveDoc('jdoe', user, function(err, doc) { if (err) { console.log(JSON.stringify(err)); } else { console.log('Saved user.'); } });
文档一旦保存在CouchDB里,就可以以对象的方式读取,例如:
var dbHost = '127.0.0.1'; var dbPort = 5984; var dbName = 'users'; var couchdb = require('felix-couchdb'); var client = couchdb.createClient(dbPort, dbHost); var db = client.db(dbName); db.getDoc('jdoe', function(err, doc) { console.log(doc); });
更新文档使用的是与创建文档一样的saveDoc命令,如果CouchDB检测到有同样ID的记录存在,它会把旧的记录覆盖,例如:
var dbHost = '127.0.0.1'; var dbPort = 5984; var dbName = 'users'; var couchdb = require('felix-couchdb'); var client = couchdb.createClient(dbPort, dbHost); var db = client.db(dbName); db.getDoc('jdoe', function(err, doc) { doc.name.first = 'Johnny'; doc.email = '[email protected]'; db.saveDoc('jdoe', doc); db.getDoc('jdoe', function(err, revisedUser) { console.log(revisedUser); }); });
要从CouchDB里删除文档,需要同时提供ID和版本号,例如:
var dbHost = '127.0.0.1'; var dbPort = 5984; var dbName = 'users'; var couchdb = require('felix-couchdb'); var client = couchdb.createClient(dbPort, dbHost); var db = client.db(dbName); db.getDoc('jdoe', function(err, doc) { db.removeDoc(doc._id, doc._rev); });
1.2 Redis
Redis是基于内存的key-value存储,并具备了持久化功能,它在性能和扩展性要求很高的情况下会被使用。除了key-value的存储能力,Redis还提供了可供网络访问的共享内存、非阻塞的事件总线,还有订阅和发布的功能。
如下演示了通过Node对Redis进行基础的set和get操作,例如:
var redis = require('redis'); var client = redis.createClient(); client.on('error', function(err) { console.log('Error ' + err); }); client.set('key', 'Hello World!', redis.print); client.get('key', function(err, reply) { console.log('Results for key:'); console.log(reply); client.end(); });
哈希是包含多个key的对象,例如:
var redis = require('redis'); var client = redis.createClient(); client.on('error', function(err) { console.log('Error ' + err); }); client.hset('user', 'username', 'johndoe'); client.hset('user', 'firstname', 'john'); client.hset('user', 'lastname', 'doe'); client.hkeys('user', function(err, replies) { console.log('Results for user:'); console.log(replies.length + ' replies:'); replies.forEach(function(reply, i) { console.log(i + ': ' + reply); }); client.end(); });
可以一次设置多个key的内容,例如:
var redis = require('redis'); var client = redis.createClient(); client.on('error', function(err) { console.log('Error ' + err); }); client.hmset('user', 'username', 'johndoe', 'firstname', 'john', 'lastname', 'doe'); client.hkeys('user', function(err, replies) { console.log('Results for user:'); console.log(replies.length + ' replies:'); replies.forEach(function(reply, i) { console.log(i + ': ' + reply); }); client.end(); });
列表类型可以想像成一个key包含了多个值,可以往列表的头部和尾部添加内容,例如:
var redis = require('redis'); var client = redis.createClient(); client.on('error', function(err) { console.log('Error ' + err); }); client.lpush('pendingusers', 'user1'); client.lpush('pendingusers', 'user2'); client.lpush('pendingusers', 'user3'); client.rpop('pendingusers', function(err, username) { if (!err) { console.log('Processing ' + username); } client.end(); });
当需要一个没有重复内容的列表时,可以使用集合,例如:
var redis = require('redis'); var client = redis.createClient(); client.on('error', function(err) { console.log('Error ' + err); }); client.sadd('myteam', 'Neil'); client.sadd('myteam', 'Peter'); client.sadd('myteam', 'Brian'); client.smembers('myteam', function(err, members) { console.log(members); client.end(); });
和普通集合一样,有序集合也不允许重复成员,它增加了权重的概念,允许在数据上进行基于分数的操作,例如:
var redis = require('redis'); var client = redis.createClient(); client.on('error', function(err) { console.log('Error ' + err); }); client.zadd('contestants', 60, 'Deborah'); client.zadd('contestants', 65, 'John'); client.zadd('contestants', 26, 'Patrick'); client.zcard('contestants', function(err, length) { if (!err) { var contestantCount = length; var membersPerTeam = Math.ceil(contestantCount / 3); client.zrange('contestants', membersPerTeam * 0, membersPerTeam * 1 - 1, function(err, values) { console.log('Young team: ' + values); } ); client.zrange('contestants', membersPerTeam * 1, membersPerTeam * 2 - 1, function(err, values) { console.log('Middle team: ' + values); } ); client.zrange('contestants', membersPerTeam * 2, contestantCount, function(err, values) { console.log('Elder team: ' + values); client.end(); } ); } });
1.3 MongoDB
因为MongoDB提供了JavaScript环境下的BSON对象存储,因此从Node去读写数据非常高效。Mongo把传入的数据保存在内存里,因此适合高并发写操作的情况。、
往MongoDB集合中写入记录需要在Node中创建一个JSON对象,然后直接往Mongo中打印,例如:
var mongo = require('mongodb'); var host = 'localhost'; var port = mongo.Connection.DEFAULT_PORT; var db = new mongo.Db('node-mongo', newmongo.Server(host, port, {}), {}); db.open(function(err, db) { db.collection('users', function(err, collection) { collection.insert({username:'Bilbo',firstname:'Shilbo'}, function(err, docs) { console.log(docs); db.close(); } ); }); });
Node使用Mongoose库能支持大量的Mongo操作,与原生驱动相比,Mongoose是一个表达更清楚的环境,例如:
var mongoose = reuqire('mongoose'); var Schema = mongoose.Schema; var ObjectId = Schema.ObjectId; var AuthorSchema = new Schema({ name: { first : String, last : String, full : String }, contact: { email : String, twitter : String, google : String } }); var Author = mongoose.model('Author', AuthorSchema);
Mongoose允许直接操作对象数据集合,例如:
mongoose.connect('mongodb://localhost:27017/upandrunning', function(err) { if (err) { console.log('Could not connect to mongo'); } }); newAuthor.save(function(err) { if (err) { console.log('Could not save author'); } else { console.log('Author saved'); } }); Author.find(funciton(err, doc) { console.log(doc); });
2. 关系型数据库
2.1 MySQL
node-db模块提供了常用数据库系统的原生代码接口,包括与MySQL的接口。它通过该模块公开的通用API来给Node使用,例如:
var mysql = require('db-mysql'); var connectParams = { 'hostname': 'localhost', 'user': 'dev', 'passport': 'dev', 'database': 'upandrunning' }; var db = new mysql.Database(connectParams); db.connect(function(error) { if (error) { return console.log('Failed to connect'); } this.query().select(['id', 'user_login']).from('users') .execute(function(error, rows, columns) { if (error) { console.log('error on query'); } else { console.log(rows); } }); });
插入数据和选择数据的方法类似,例如:
var mysql = require('db-mysql'); var connectParams = { 'hostname': 'localhost', 'user': 'dev', 'passport': 'dev', 'database': 'upandrunning' }; var db = new mysql.Database(connectParams); db.connect(function(error) { if (error) { return console.log('Failed to connect'); } this.query().insert('users', ['user_login'], ['newbie']) .execute(function(error, rows, columns) { if (error) { console.log('error on query'); console.log(error); } else { console.log(rows); } }); });
更新操作也是依赖链式函数来生成等价的SQL操作,例如:
var mysql = require('db-mysql'); var connectParams = { 'hostname': 'localhost', 'user': 'dev', 'passport': 'dev', 'database': 'upandrunning' }; var db = new mysql.Database(connectParams); db.connect(function(error) { if (error) { return console.log('Failed to connect'); } this.query().update('users').set({'user_nicename': 'New User'}) .where('user_login = ?', ['newbie']) .execute(function(error, rows, columns) { if (error) { console.log('error on query'); console.log(error); } else { console.log(rows); } }); });
删除与更新非常类似,唯一不同的是,在删除的时候,不需要指定哪一列有更新,例如:
var mysql = require('db-mysql'); var connectParams = { 'hostname': 'localhost', 'user': 'dev', 'passport': 'dev', 'database': 'upandrunning' }; var db = new mysql.Database(connectParams); db.connect(function(error) { if (error) { return console.log('Failed to connect'); } this.query().delete().from('users').where('user_login = ?', ['newbie']) .execute(function(error, rows, columns) { if (error) { console.log('error on query'); console.log(error); } else { console.log(rows); } }); });
Sequelize是一个对象关系映射(ORM),可以使用Sequelize来定义数据库与程序间共享的对象,这样就不需要为每个操作写查询语句,而直接过操作这些对象来写入或读取数据库,例如:
var Sequelize = require('sequelize'); var db = new Sequelize('upandrunning', 'dev', 'dev', { host:'localhost' }); var Author = db.define('Author', { name: Sequelize.STRING, biography: Sequelize.TEXT }); Author.sync().on('success', function() { console.log('Author table was created.'); }).on('failure', function(error) { console.log('Unable to create author table'); });
Sequelize与之前的库有所区别,它是基于监听事件驱动的架构,而不是其他地方采用的回调函数驱动的架构,因此需要在每个操作之后同时监听成功和失败事件,例如:
var Sequelize = require('sequelize'); var db = new Sequelize('upandrunning', 'dev', 'dev', { host:'localhost' }); var Author = db.define('Author', { name: Sequelize.STRING, biography: Sequelize.TEXT }); var Book = db.define('Book', { name: Sequelize.STRING }); Author.hasMany(Book); Book.hasMany(Book); db.sync().on('success', function() { Book.build({ name: 'Through the Storm' }).save().on('success', function(record) { console.log('Book saved'); Author.build({ name: 'Lynne Spears', biography: 'Author and mother of Britney' }).save().on('success', function() { console.log('Author & Book Relation created'); }); }).on('failure', function(error) { console.log('Could not save book'); }); }).on('failure', function(error) { console.log('Failed to sync database'); });
2.2 PostgreSQL
PostgreSQL是面向对象的RDBMS,其前身是Ingres数据库。该数据库系统作为功能强大的发行版赢得了良好的声誉,对于有Oracle背景的用户来说尤为方便。
以下代码演示了如何从PostgreSQL选出数据:
var pg = require('pg'); var connectionString = 'pg://dev:dev@localhost:5432/upandrunning'; pg.connect(connectionString, function(err, client) { if (err) { console.log(err); } else { var sqlStmt = 'Select username, firstname, lastname from users'; client.query(sqlStmt, null, function(err, result) { if (err) { console.log(err); } else { console.log(result); } pg.end(); }); } });
pg库接受参数形式的查询,这样就能够用上来自外部资源里的值,例如:
以下代码演示了如何在PostgreSQL中更新数据:
var pg = require('pg'); var connectionString = 'pg://dev:develocalhost:5432/upandrunning'; pg.connect(connectionString, function(err, client) { if (err) { console.log(err); } else { var sqlStmt = "update users set firstname = $1 where username = $2"; var sqlParams = ['jane', 'jdoe']; var query = client.query(sqlStmt, sqlParams, function(err, result) { if (err) { console.log(err); } else { console.log(result); } pg.end(); }); } });
以下代码演示了如何从PostgreSQL中删除数据:
var pg = require('pg'); var connectionString = 'pg://dev:develocalhost:5432/upandrunning'; pg.connect(connectionString, function(err, client) { if (err) { console.log(err); } else { var sqlStmt = "delete from users where username = $1"; var sqlParams = ['jdoe']; var query = client.query(sqlStmt, sqlParams, function(err, result) { if (err) { console.log(err); } else { console.log(result); } pg.end(); }); } });
3. 连接池和消息队列
3.1 连接池
连接池在Web开发中是非常重要的概念,因为建立一个数据库连接的开销相对来说还是很大的。为每个请求创建一个甚至多个连接会对高流量网站造成不必要的额外负担。解决方案是在内部缓存池里维护数据库连接,当某连接不再需要时,它会被放回连接池里。
许多数据库驱动提供了连接池功能,但该模式违反了Node的“一个模块,一个功能”的理念。所以,Node开发者在数据层之上应使用通用的连接池模块来进行数据库连接服务,例如:
var mysql = require('db-mysql'); var poolModule = require('generic-pool'); var connectParams = { 'hostname': 'localhost', 'user': 'dev', 'password': 'dev', 'database': 'zborowski' }; var pool = pooModule.Pool({ name: 'mysql', create: function(callback) { var db = new mysql.Database(connectParams); db.connect(function(error) { callback(error, db); }); }, destroy: function(client) {client.disconnect();}, max: 10, idleTimeoutMillis: 3000, log: true }); pool.acquire(function(error, client) { if (error) { reutrn console.log('Failed to connnect'); } client.query().select(['id', 'user_login']).from('wp_users') .execute(function(error, rows, columns) { if (error) { console.log('Error on query'); } else { console.log(rows); } pool.release(client); }); });
3.2 RabbitMQ
RabbitMQ使用标准的AMQP协议进行通信。AMQP提供了对厂商中立的抽象规范,可以提供通用的消息中间件服务,并且旨在解决不同类型系统间通信的问题。
以下代码演示了AMQP/RabbitMQ使用方法:
var connection = require('amqp').createConnection(); connection.on('ready', function() { console.log('Connected to ' + connection.serverProperties.product); var e = connect.exchange('up-and-running'); var q = connect.queue('up-and-running-queue'); q.on('queueDeclareOk', function(args) { console.log('Queue opened'); q.bind(e, '#'); q.on('queueBindOk', function() { console.log('Queue bound'); q.on('basicConsumeOk', function() { console.log('consumer has subscribed, publishing message.'); e.publish('routingKey', {hello:'world'}); }); }); q.subscribe(function(msg) { console.log('Message received.'); console.log(msg); connection.end(); }); }); });
如果长时间运行任务超出了用户容忍度,或者是该任务会堵塞整个程序,使用队列就很合适,例如:
var connection = require('amqp').createConnection(); var count = 0; connection.on('ready', function() { console.log('Connected to ' + connection.serverProperties.product); var e = connect.exchange('up-and-running'); var q = connect.queue('up-and-running-queue'); q.on('queueDeclareOk', function(args) { console.log('Queue opened'); q.bind(e, '#'); q.on('queueBindOk', function() { console.log('Queue bound'); setInterval(function() { console.log('Publishing message #' + ++count); e.publish('routingKey', {count:count}); }, 1000); }); }); });
以下代码演示了如何写相应的客户端:
var connection = require('amqp').createConnection(); var count = 0; connection.on('ready', function() { console.log('Connected to ' + connection.serverProperties.product); var e = connect.exchange('up-and-running'); var q = connect.queue('up-and-running-queue'); q.on('queueDeclareOk', function(args) { console.log('Queue opened'); q.bind(e, '#'); q.subscribe({ack:true}, function(msg) { console.log('Message received:'); console.log(msg.count); sleep(5000); console.log('Processed. Waiting for next message.'); q.shift(); }); }); });
本文出自 “青锋笔记” 博客,谢绝转载!