Грабли mongoose

от автора


Хакер — человек, который наступает на грабли, которые спрятаны в сарай и закрыты на замок

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.

ссылка на оригинал статьи http://habrahabr.ru/post/253395/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *