Грабли 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.

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 8

    +5
    Извините что не по теме, но эпиграф про хакера — шикарный.
      0
      Вообще-то это все есть в документации.
        +2
        console.log(''====);
        

        Я вначале даже успел расстроиться от того что существует такое «низкоуровневое» преобразование, о котором я и не знал. Оказалось всё нормально, не существует. Исправьте пожалуйста :)
          0
          Спасибо)
          0
          1. Стандартное соглашение для именования коллекций MongoDB: название plural в нижнем регистре.
          2. Почему бы не добавить virtual атрибут с getter-ом для этого случая? Наверняка ведь он нужен не только для сериализации json.
          3. У каждого документа есть виртуальный атрибут id, это строка и его можно сравнивать как строку.
            0
            6. autoIndex можно отключать при подключении начиная с версии 3.9.5:
            mongoose.connect('mongodb://localhost/myapp', {autoIndex: process.env.NODE_ENV !== 'production'});
            
              +1
              Я расскажите пожалуйста, зачем нужно отключать его в production? Я бегло прогуглил, тут тоже не понимают.
                +1
                В документации написано, что это отрицательно влияет на производительность.

                Полный текст из документации mongoose:
                «When your application starts up, Mongoose automatically calls ensureIndex for each defined index in your schema. Mongoose will call ensureIndex for each index sequentially, and emit an 'index' event on the model when all the ensureIndex calls succeeded or when there was an error. While nice for development, it is recommended this behavior be disabled in production since index creation can cause a significant performance impact. Disable the behavior by setting the autoIndex option of your schema to false.»

                Источник: mongoosejs.com/docs/guide.html

          Only users with full accounts can post comments. Log in, please.