Наследование свойств моделей в Mongoose

Всех с прошедшим Новым Годом! Сегодня я покажу как наследовать свойства моделей в ODM Mongoose. Для тех, кто не знает mongoose - это ODM (object document mapper) для node.js и MongoDB.

Применение

Часто бывает необходимо в рамках одной коллекции объектов выделить несколько типов с некоторыми различиями в наборе свойств. Т.к. MongoDB реализует schema-less дизайн, мы не ограничены базой данных и не обязаны следовать какому-то строгому набору полей в наших коллекциях. Однако, удобнее использовать ODM, которые реализовывают дополнительные плюшки, такие как валидация, фильтрация, методы модели и т.д. Соответственно описание данных на уровне исходного кода модели все же остаётся. Для того, чтобы решить проблему с типами объектов в рамках одной коллекции можно наследовать базовые свойства и дополнять их специфическими для каждого типа объектов.

Рассмотрим простой пример. У нас есть два типа пользователей: юридическое лицо и физическое лицо. Каждый тип имеет общие и специфические поля. Чтобы выделить общие поля создадим объект с набором полей, которые встречаются в каждом из типов:

var BaseUser = {
    inn: {type: String, 
        validate: inn_validator,
        required: true},
    email: {type: String, required: true, unique: true,
        validate: email_validator},
    phone: {type: String, validate: phone_validator,
        required: true},
    username: {type: String, unique: true,
        required: true, validate: username_validator},
    password: {type: String, required: true, 
        validate: password_validator}
};

Далее для каждого из типов пользователей создаём объект со специфическими полями:

// Физическое лицо
var IndividualPerson = {
    first_name: {type: String, required: true,
        validate: first_name_validator},
    last_name: {type: String, required: true,
        valdiate: last_name_validator},
    patronymic: {type: String, 
        validate: patronymic_validator},
    type: {type: String, default: 'individual'}
};

// Юридическое лицо
var LegalPerson = {
    full_name: {type: String, validate: full_name_validator, 
        required: true},
    short_name: {type: String, required: true, 
        validate: short_name_validator},
    type: {type: String, default: 'legal'}
};

Также мы добавили поле type для дифференциации записей, т.к. они будут храниться в одной коллекции. Далее необходимо просто при создании схемы модели выполнять слияние (merge) значений из BaseUser и соответствующего объекта типа:

IndividualUserSchema = new Schema(Object.extended(BaseUser).merge(IndividualPerson));
LegalUserSchema = new Schema(Object.extended(BaseUser).merge(LegalPerson));

Здесь я использую sugar.js, но при необходимости это можно реализовать и вручную.

Затем модели необходимо создавать таким образом, чтобы они использовали одну и ту же коллекцию в базе данных:

exports.LegalUser = function(db) {
    return db.model('LegalUser', LegalUserSchema, 'users');
};

exports.IndividualUser = function(db) {
    return db.model('IndividualUser', IndividualUserSchema, 'users');
};

Заключение

В итоге мы получили две модели со своими наборами полей/валидаторов и при этом остались преимущества использования одной коллекции - поиск записей по одной коллекции, уникальные значения username и email через индексы и т.д. При желании можно реализовать что-то вроде фабрики, чтобы при поиске в зависимости от значения в поле type создавался объект соответствующей модели.