Хакер — человек, который наступает на грабли, которые спрятаны в сарай и закрыты на замок
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, это будет отрабатывать немного быстрее, но менее читабельным).
6. Отключение автоматического построения индексов
Для обеспечения уникальности полей в 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.
7. Не забываем о зарезервированных ключах
Возможно, не все сталкиваются с подобной проблемой, но все же обращу внимание на то, что в объектах mongoose есть набор зарезервированных названий для атрибутов, они приводятся в документации. Приходилось сталкиваться с именованием атрибутов ключами из списка зарезервированных, после чего необходимо было отскребать обращения к этим ключам по всему коду. Mongoose на использование зарезервированных ключей ничуть не ругается. Граблями, на которые наступил я в данном списке ключей, оказался ключ options.
Автор: alexahdp