Хакер — человек, который наступает на грабли, которые спрятаны в сарай и закрыты на замок
Mongoose — самый популярный модуль для работы с mongodb на javascript. Примеры на сайте позволяют достаточно быстро и успешно начать его использовать, однако mongoose имеет ряд неожиданных особенностей, которые могут заставить программиста начать выдирать волосы на голове. Именно об этих особенностях я и собираюсь рассказать.
1. Именование коллекций
Начну с самой безобидной и легкообнаруживаемой особенности. Вы создаете модель:
var mongoose = require('mongoose'); var User = new mongoose.Schema({ email: String, password: String, data: { birthday: { type: Date, default: Date.now }, status: { type: String, default: 'active', enum: ['active', 'unactive'] }, mix: { type: mongoose.Schema.Types.Mixed, default: {} } } }); module.exports = mongoose.model('User', User);
Создаете пользователя:
var user = new User({email: 'test@test.com', password: '12345'}); user.save(ok(function() { console.log('ok'); }));
Если теперь мы выполним в консоли mongodb команду «show collections», то увидим, что была создана коллекция users. Т.е. mongoose при создании коллекций приводит их названия к нижнему регистру и множественному числу.
2. Переопределение метода toJSON()
Пусть нам понадобилось модифицировать наш экземпляр модели, внеся в него атрибут, не описанный в модели:
User.findOne({email: 'test@test.com'}, ok(function(user) { user.someArea = 'custom value'; console.log(user.someArea); console.log('===='); console.log(user); }));
В консоли мы увидим (вместо console.log может быть использовать res.json):
custom value ==== { __v: 0, _id: 54fc8c22c90fb7dd025eee7c, email: 'test@test.com', password: '12345', data: { mix: {}, status: 'active', birthday: Thu Mar 12 2015 23:46:06 GMT+0300 (MSK) } }
Как видно, у объекта есть атрибут someArea, но при дампе в консоль он куда-то внезапно пропал. Все дело в том, что mongoose переопределяет метод toJson и все поля, не описанные в схеме модели выбрасываются. Может возникнуть ситуация, когда мы добавляем в объект атрибут и отдаем его клиенту, но до клиента атрибут ни в какую не доходит. Для того, чтобы он успешно попал на клиент, модифицировать надо не mongoose-объект. Для этих целей у экземпляров моделей есть метод toObject, который возвращает native-Object, который можно как угодно модифицировать и уж из него ничего не потеряется.
3. Сравнение _id
Может показаться, что _id имеет тип String, однако, это совсем не так. _id — объект и сравнивать идентификаторы экземпляров mongoose-моделей надо как объекты. Пример:
User.findOne({email: 'test@test.com'}, ok(function(user1) { User.findOne({email: 'test@test.com'}, ok(function(user2) { log(user1._id == user2._id); // false log(user1._id.equals(user2._id)); // true log(user1._id.toString() == user2._id.toString()); // true })); }));
4. Сохранение mixed-полей
У нас в схеме есть одно поле с типом mixed, это data.mix. Если мы его изменим, например:
User.findOne({email: 'test@test.com'}, ok(function(user) { user.data.mix = {msg: 'hello world'}; user.save(ok(function() { console.log('ok'); })); }));
, то изменения успешно попадут в БД.
Однако, если теперь мы выполним изменение внутри data.mix, то изменения в БД не попадут.
User.findOne({email: 'test@test.com'}, ok(function(user) { user.data.mix.msg = 'Good bye'; user.save(ok(function() { log(user); })); }));
В консоль выведется объект user, содержащий наши модификацию, а запрос к БД покажет, что пользователь не был изменен. Для того, чтобы изменения попали в БД, нужно перед методом save оповестить mongoose о том, что мы модифицировали mixed-поле:
user.markModified('data.mix');
Эту же операцию необходимо производить и с объектами типа Date при их модификации встроенными методами (setMonth, setDate, ...), об этом сказано в документации
5. Дефолты для массивов
Пусть при описании схемы модели мы решили, что у нас в поле должен лежать массив объектов. Нам необходимо прописать дефолты для самого массива и для всех вложенных в него объектов. В mongoose для этого используется специальный ключ type:
var Lib = new mongoose.Schema({ userId: mongoose.Schema.ObjectId, images: { // правила валидации и дефолты для каждого из полей объекта массива images type: [{ uploaded: { type: Date, default: Date.now }, src: String }], // значение по-умолчанию для поля images default: [{uploaded: new Date(2012, 11, 22), src: '/img/default.png'}] } }); module.exports = mongoose.model('Lib', Lib);
Аналогично с помощью ключевого слова type мы можем создавать многоуровневые дефолты для объектов.
6. Потоковое обновление
Иногда необходимо выполнить обновление очень большой коллекции из кода. Загружать всю коллекцию — не хватит памяти. Можно вручную выставлять лимиты, загружать документы пачками и обновлять, но в mongoose есть очень удобные для этой операции интерфейсы — stream-ы.
e.m.users.find({}).stream() .on('data', function(user) { var me = this; me.pause(); // выполняем надо пользователем очень хитрые асинронные манипуляции user.save(function(err) { me.resume(err); }); }) .on('error', function(err) { log(err); }) .on('close', function() { log('All done'); });
(Однако, если мы будем извлекать пользователей пачками, редактировать и сохранять через async.parallel, это будет отрабатывать немного быстрее, но менее читабельным).
7. Отключение автоматического построения индексов
Для обеспечения уникальности полей в mongodb используются уникальные индексы. С помощью mongoose их очень легко создавать. Mongoose вообще создает высокий уровень абстракции при работе с данными. Однако, наши недостатки являются продолжениями наших достоинств и многие забывают отключать в production-режиме автоматическое создание индексов, хотя в официальной документации об этом четко сказано.
В mongoose для этих целей есть даже специальный флаг {autoIndex: false}, который надо указывать при описании схемы данных:
var User = new mongoose.Schema({ email: { type: String, unique: true, required: true }, password: String }, { autoIndex: process.env('mode') == 'development' });
Теперь автоматическое построение индексов будет работать только в режиме development.
8. Не забываем о зарезервированных ключах
Возможно, не все сталкиваются с подобной проблемой, но все же обращу внимание на то, что в объектах mongoose есть набор зарезервированных названий для атрибутов, они приводятся в документации. Приходилось сталкиваться с именованием атрибутов ключами из списка зарезервированных, после чего необходимо было отскребать обращения к этим ключам по всему коду. Mongoose на использование зарезервированных ключей ничуть не ругается. Граблями, на которые наступил я в данном списке ключей, оказался ключ options.
