18 March 2015

Грабли mongoose

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

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.
Tags:mongodbmongooseграбли
Hubs: Node.JS MongoDB
+14
49.7k 137
Comments 8
Popular right now
Профессия iOS-разработчик
November 30, 202075,000 ₽SkillFactory
Основы HTML и CSS
November 30, 2020FreeНетология
Frontend-разработчик с нуля
November 30, 202077,940 ₽Нетология
Курс по аналитике данных
November 30, 202053,500 ₽SkillFactory
SMM-менеджер
November 30, 202059,998 ₽GeekBrains