Хранилища данных в Node.js: MongoDB
MongoDB — документо-ориентированная база данных, ориентированная на кластеризацию и написанная на C++. Сами разработчики утверждают, что она находится как раз посередине между key-value stores и традиционными реляционными БД. Я решил её покопать после того как увидел на How to Node пост о создании блога на основе Express + MongoDB.
Документы в Mongo хранятся в виде JSON-подобных объектов, так что в JavaScript с ними работать довольно удобно. Кстати, для предварительного изучения MongoDB есть вот такая интерактивная веб-консоль со встроенным tutorial.
Установка
Выбираем версию на странице релизов, качаем wget‘ом и распаковываем:
wget http://downloads.mongodb.org/linux/mongodb-linux-i686-1.4.1.tgz tar -xvzf mongodb-linux-i686-1.4.1.tgz
Создадим каталог для хранения файлов баз MongoDB:
mkdir ~/mongo-data
Теперь запускаем
cd mongodb-linux-i686-1.4.1/bin ./mongod --dbpath ~/mongo-data/ &
Сервер должен запуститься в виде фонового процесса и сразу предупредить нас что размер базы данных ограничен двумя гигабайтами (если Вы используете 32-хбитную сборку). Чтобы проверить его работу, сразу к нему подключимся:
./mongo
Должна открыться консоль MongoDB. Можно например набрать show dbs и увидеть список доступных баз.
По умолчанию MongoDB не запускается вместе с системой, если Вам это нужно, добавьте её самостоятельно в init.d. Я рассчитываю просто поизучать, поэтому пока этим заниматься не буду.
В качестве коннектора воспользуемся node-mongodb-native от christkv. Забираем архив последней версии со страницы загрузок:
wget http://github.com/christkv/node-mongodb-native/tarball/V0.7.1 tar -xvzf christkv-node-mongodb-native-V0.7.1-0-g08527ba.tar.gz mv christkv-node-mongodb-native-V0.7.1-0-g08527ba node-mongodb-native-V0.7.1
Папку с коннектором я переименовал исключительно для удобства использования. В папке будет Makefile, но он используется только для запуска тестов. Нас же интересует сам коннектор — он лежит в lib/mongodb.
Использование
В принципе к коннектору прилагается директория examples, содержащая аж 11 примеров работы с MongoDB, причём довольно интересных. Я тут приведу только самые базовые операции.
Открытие базы данных:
sys = require("sys");
test = require("mjsunit");
var mongo = require('../lib/mongodb');
// Хост и порт берутся из переменных окружения
var host = process.env['MONGO_NODE_DRIVER_HOST'] != null ? process.env['MONGO_NODE_DRIVER_HOST'] : 'localhost';
var port = process.env['MONGO_NODE_DRIVER_PORT'] != null ? process.env['MONGO_NODE_DRIVER_PORT'] : mongo.Connection.DEFAULT_PORT;
sys.puts("Connecting to " + host + ":" + port);
var db = new mongo.Db('node-mongo-examples', new mongo.Server(host, port, {}), {});
db.open(function(err, db) {
// Подключились к базе
});
Создание коллекции и добавление элементов:
// Открываем коллекцию. Если её не существует, она будет создана
db.collection('test', function(err, collection) {
// Добавляем три элемента
for(var i = 0; i < 3; i++) {
collection.insert({'a':i});
}
});
Показ элементов из коллеции:
collection.count(function(err, count) {
sys.puts("There are " + count + " records in the test collection. Here they are:");
// Получаем все элементы коллекции с помощью find()
collection.find(function(err, cursor) {
cursor.each(function(err, item) {
// Null обозначает последний элемент
if (item != null) {
sys.puts(sys.inspect(item));
} else {
sys.puts("That's all!");
}
});
});
});
В MongoDB проходить по элементам коллекции можно с помощью итератора, необязательно доставать элементы по одному, как это приходится делать в CouchDB.
Курсор нужен для того чтобы запрашивать объекты из MongoDB по мере необходимости. Т.е., новый документ будет получен только после того как будет вызван each()/nextObject()/toArray().
Можно также производить поиск по коллекции, доставая только элементы с нужными свойствами:
db.collection('test', function(err, collection) {
collection.insert({'name':'Robert', 'age': 12});
collection.insert({'name':'Agatha', 'age': 20});
collection.insert({'name':'Sam', 'age': 6});
// Получаем все элементы с age = 6
collection.find({'age': 6}, function(err, cursor) {
// Преобразовываем их в массив
cursor.toArray(function(err, items) {
// items - массив документов с age = 6
});
});
});
Удаление документов из коллекции:
db.collection('test', function(err, collection) {
// Удаляем элементы с age = 20
collection.remove({'age': 20}, function(err, collection) {
// Удаляем все элементы
collection.remove(function(err, collection) {
// Все элементы удалены
});
})
});
При сохранении документа в MongoDB ему назначается _id, как и в CouchDB. Но в отличии от Couch, в Mongo удобнее оперировать документами с помощью свойств.
Кроме этого, в MongoDB есть много других интересных вещей: хранение двоичных файлов, ссылки на документы в других коллекциях (примерный аналог — foreign key в SQL, насколько я понял), поддержка индексов. MongoDB выглядит очень удобной базой данных для приложений разной сложности. Полагаю, следующее мини-приложение я напишу именно с её помощью, чтобы лучше освоиться с базой.

Сергей, не могли бы вы пояснить вот эти моменты:
> В MongoDB проходить по элементам коллекции можно с помощью итератора, необязательно доставать элементы по одному, как это приходится делать в CouchDB.
и
> При сохранении документа в MongoDB ему назначается _id, как и в CouchDB. Но в отличии от Couch, в Mongo удобнее оперировать документами с помощью свойств.
PS: оставил тот же комментарий на nodejs.ru — не сразу понял что там не оригинал.
1. Мы указываем что нам нужны определённые элементы, и мы хотим их обработать. Мы навешиваем обработчик с помощью
each, и он достаёт элементы по одному и обрабатывает. Примерно какeachв jQuery. Плюс в том что нам не нужно ждать пока загрузится весь набор элементов (а он может быть очень большим), мы обрабатываем их в потоковом режиме. Заодно и память экономится.Eachпроходит по всем элементам самостоятельно, но можно это делать вручную с помощьюnextObject.2. У документа есть свойство
_id, и есть другие, “второстепенные” свойства. Вот например мы сохраняем HTML-страницы. Вместе с каждой страницей мы сохраняем её текст и URL. В CouchDB мы не можем достать страницу по “второстепенному” свойству – например, с определённым URL, нам надо обязательно указывать_idдокумента. А в Mongo мы можем делать выборку по любому из свойств – по_id, по URL и т.д. В CouchDB можно конечно задавать пользовательские_id– например, использовать URL, но мы всё равно ограничены только одним свойством, которое идентифицирует записи.Зато в CouchDB удобнее делать “аггрегирующие” запросы благодаря Map/Reduce и хранению промежуточных результатов.
добрый день, а можете поподробнее о “потоковом режиме” рассказать:
1. все ли операции выполняются асинхронно?
2. в туториалах (и интерактивной веб-консоле) работа с тем же find много проще – как раз из-за блокировки до получения данных?
еще мне очень интересно, имеет ли ли право на жизнь такой подход: при инициниализации считываем (можно и синхронно) все элементы коллекции и “перегоняем” в js-массив, при изменении js-объектов (которые в массиве) вызываем асинхронный save/update вообще без callback’а?
заранее спасибо
ps не напишете русским языком, как ссылаться на объекты из других коллекций?
Save/update без callback’ов вполне возможен, и в принципе достаточно распространен когда надо просто сохранить данные. Да, все операции насколько я знаю асинхронны, это немного усложняет код (это можно немного исправить библиотекой вроде Do).
Способ с массивом в принципе вполне имеет право на жизнь (можно сохранять данные при апдейте с помощью сеттеров).
Про ссылки на объекты из других коллекций могу посмотреть завтра, как доберусь до сервера.
большое спасибо!
и забыл
в монго injecting, как в sql, при исользовании json’а в принципе не возможен?
Injecting думаю вряд ли возможен – команда и данные там всё таки разделены, JSON формируется встроенными средствами V8.
Nodejs.ru я тоже просматриваю, но нечасто. У моих статей там внизу ссылка на этот блог, здесь я отвечаю быстрее.
Про итерацию так и не понял, почему в CouchDB нужно что-то доставать по одному. Что мне мешает так же брать по чуть-чуть и итерироваться.
> А в Mongo мы можем делать выборку по любому из свойств – по _id, по URL и т.д. В CouchDB можно конечно задавать пользовательские _id – например, использовать URL, но мы всё равно ограничены только одним свойством, которое идентифицирует записи.
Так сделав в CouchDB индекс вида:
emit(doc.url, doc);
можно тоже делать запросы по урлу. Конечно это лишнее движение, но всё-равно достаточно просто.
1. В MongoDB для этого есть нативный интерфейс, в CouchDB придётся писать свой. Но да, можно.
2. Здесь мы получим *все* документы с их урлами. И потом нам придётся их перебрать. Что если мне нужны документы с урлом “example.com”? Или собранные 1 апреля 2010 года? Во вьюшки же нельзя передавать параметры, насколько я понимаю.
Поправка — похоже, запрос по определённому свойству сделать можно, если объявить соответствующую вьюшку.