Node.js хорошо известен своей хорошей связью с базами данных NoSQL. Менее известный факт — это также очень эффективно с реляционными базами данных. Среди десятков ORM в JavaScript один выделяется для реляционных баз данных: Sequelize . Это довольно легко узнать, но не так много указателей о том, как организовать код модели с помощью этого модуля. Вот несколько советов, которые мы узнали, используя Sequelize в проекте среднего размера.
Sequelize 101
Sequelize утверждает, что поддерживает MySQL, PostgreSQL и SQLite. Документы Sequelize объясняют первые шаги с помощью JavaScript ORM. Сначала инициализируйте соединение с базой данных, затем несколько моделей, не беспокоясь о первичных или внешних ключах:
var sequelize = new Sequelize('database', 'username'[, 'password'])
var Project = sequelize.define('Project', {
title: Sequelize.STRING,
description: Sequelize.TEXT
});
var Task = sequelize.define('Task', {
title: Sequelize.STRING,
description: Sequelize.TEXT,
deadline: Sequelize.DATE
});
Project.hasMany(Task);
Затем создайте новые экземпляры и сохраните их, выполните запросы к базе данных и т. Д.
// create an instance
var task = Task.build({title: 'very important task'})
task.title // ==> 'very important task'
// persist an instance
task.save()
.error(function(err) {
// error callback
})
.success(function() {
// success callback
});
// query persistence for instances
var tasks = Task.all({ where: ['dealdine < ?', new Date()] })
.error(function(err) {
// error callback
})
.success(function() {
// success callback
});
Sequelize использует обещания, так что вы можете связать обратные вызовы ошибок и успехов, и все это хорошо работает с модульными тестами.
Все это довольно хорошо задокументировано, но документация Sequelize охватывает только основное использование. Как только вы начнете использовать Sequelize в реальных проектах, поиск правильного способа реализации функции становится более сложным.
Структура файла модели
Все примеры в документации Sequelize показывают все объявления моделей, сгруппированные в один файл. Как только проект достигает объема производства, это не является жизнеспособным подходом. Альтернатива — импортировать модели из модуля, используя sequelize.import().
Проблема в том, что отношения основаны на нескольких моделях. Когда вы объявляете отношение, модели с обеих сторон отношения уже должны быть импортированы. Вы не должны импортировать файлы модели из других файлов модели из-за политики кэширования модуля Node.js (подробнее об этом позже); вместо этого вы можете определить отношения в отдельном файле.
Вот структура файла, с которой мы работали:
models/ index.js # import all models and creates relationships PhoneNumber.js Task.js User.js ...
А вот как основная models/index.jsинициализирует всю модель:
var Sequelize = require('sequelize');
var config = require('config').database; // we use node-config to handle environments
// initialize database connection
var sequelize = new Sequelize(
config.name,
config.username,
config.password,
config.options
);
// load models
var models = [
'PhoneNumber',
'Task',
'User'
];
models.forEach(function(model) {
module.exports[model] = sequelize.import(__dirname + '/' + model);
});
// describe relationships
(function(m) {
m.PhoneNumber.belongsTo(m.User);
m.Task.belongsTo(m.User);
m.User.hasMany(m.Task);
m.User.hasMany(m.PhoneNumber);
})(module.exports);
// export connection
module.exports.sequelize = sequelize;
Использование моделей в коде
Из других частей приложения, если вам нужен класс модели, models/index.jsвместо файла модели требуется отдельный файл. Таким образом, вам не нужно повторять инициализацию Sequelize.
var models = require('./models');
var User = models.User;
var user = User.build({ first_name: "John", last_name: "Doe "});
Проблема в том, что когда вам требуется models/index.jsфайл, узел может использовать кэшированную версию модуля … или нет:
С узлаjs.org :
Несколько вызовов require (‘foo’) могут не вызывать многократное выполнение кода модуля. (…) Модули кэшируются на основе их разрешенного имени файла. Поскольку модули могут разрешать разные имена файлов в зависимости от местоположения вызывающего модуля (загрузка из папок node_modules), это не гарантирует, что require (‘foo’) всегда будет возвращать один и тот же объект, если он будет разрешен в разных файлах. ,
Это означает, что использование require('./models')для получения моделей может создать более одного подключения к базе данных. Чтобы избежать этого, modelsпеременная должна быть как-то одноэлементной. Это может быть легко достигнуто, если вы используете фреймворк, такой как expressjs , подключив modelsмодуль к приложению:
app.set('models', require('./models'));
И когда вам требуется класс модели в контроллере, используйте этот параметр приложения, а не прямой импорт:
var User = app.get('models').User;
Доступ к другим моделям
Модели Sequelize могут быть расширены с помощью методов класса и экземпляра. Вы можете добавить способности к классам моделей, как в настоящей реализации ActiveRecord. Но возникает проблема при добавлении метода, который зависит от другой модели: как модель может получить доступ к другой модели?
// in models/User.js
module.exports = function(sequelize, DataTypes) {
return sequelize.define('User', {
first_name: DataTypes.STRING,
last_name: DataTypes.STRING,
}, {
instanceMethods: {
countTasks: function() {
// how to implement this method ?
}
}
});
};
Если две модели имеют общие отношения, есть способ. Здесь Userесть много Tasks, что делает Taskмодель доступной через User.associations['Tasks'].target. И вот еще одна проблема: поскольку Sequelize не использует наследование на основе прототипов, как Userэкземпляр может получить доступ к Userклассу? Копание в источнике Sequelize приносит защищенный __factoryсвет. Со всеми этими недокументированными знаниями теперь можно написать countTasks()метод экземпляра:
countTasks: function() {
return this.__factory.associations['Tasks'].target.count({ where: { user_id: this.id } });
}
Обратите внимание, что Task.count()возвращает обещание, поэтому countTasks()также возвращает обещание:
user.countTasks().success(function(nbTasks) {
// do somethig with the user task count
});
Расширяющиеся модели (aka Behaviors)
Что если вам нужно повторно использовать несколько методов в нескольких моделях? Sequelize не имеет системы поведения как таковой (или «проблем» в терминологии Ruby on Rails), хотя это довольно легко реализовать . На данный момент вы обречены на импорт общих методов перед вызовом sequelize.define()и использование Sequelize.Utils._.extend()для добавления их в объект instanceMethodsили classMethods:
// in models/FriendlyUrl.js
module.exports = function(keys) {
return {
getUrl: function() {
var ret = '';
keys.forEach(function(key) {
ret += this[key];
})
return ret
.toLowerCase()
.replace(/^\s+|\s+$/g, "") // trim whitespace
.replace(/[_|\s]+/g, "-")
.replace(/[^a-z0-9-]+/g, "")
.replace(/[-]+/g, "-")
.replace(/^-+|-+$/g, "");
}
};
}
// in models/User.js
var friendlyUrlMethods = require('./FriendlyUrl')(['first_name', 'last_name']);
module.exports = function(sequelize, DataTypes) {
return sequelize.define('User', {
first_name: DataTypes.STRING,
last_name: DataTypes.STRING,
}, {
instanceMethods: Sequelize.Utils._.extend({}, friendlyUrlMethods, {
countTasks: function() {
return this.__factory.associations['Tasks'].target.count({ where: { user_id: this.id } });
}
});
})
};
Теперь Userэкземпляры модели получают доступ к getUrl()методу:
var user = User.build({ first_name: 'John', last_name: 'Doe' });
user.getUrl(); // 'john_doe'
Ограничением этого трюка является то, что вы должны определить поведение перед фактической моделью. Это запрещает поведению доступ к другим моделям.
Серия запросов
Sequelize предоставляет инструмент, который называется QueryChainerдля облегчения повторной синхронизации запросов.
new Sequelize.Utils.QueryChainer()
.add(User, 'find', [id])
.add(Task, 'findAll')
.error(function(err) { /* hmm not good :> */ })
.success(function(results) {
var user = results[0];
var tasks = results[1];
// do things with the results
});
После небольшого использования эта утилита оказывается очень ограниченной. В частности, QueryChainerпо умолчанию все запросы выполняются параллельно. И вы получаете доступ только к результатам запросов в окончательном обратном вызове — нет способа передать значения из одного запроса в другой.
Мы обнаружили, что гораздо удобнее использовать универсальный модуль ресинхронизации наподобие async, который предоставляет замечательную async.auto()утилиту. Этот метод позволяет составить список задач вместе с зависимостями и определить, какую задачу можно запускать параллельно, а какую — последовательно.
async.auto([
user: function(next) {
User.find(id).complete(next);
},
tasks: ['user', function(next) {
Tasks.findAll({ where: { user_id: user.id } }}).complete(next);
}]
], function(err, results) {
var user = results.user;
var tasks = results.tasks;
// do things with the results
});
Обратите внимание на complete()метод, который является альтернативой двум success()и error()обратным вызовам. complete()принимает обратный вызов с подписью (err, res), что более естественно в мире Node.js и совместимо с async.
Предзагрузка
ORM обычно хорошо умеют сводить запросы к минимуму. Sequelize предлагает функцию предварительной выборки, позволяющую сгруппировать два запроса в один с помощью JOIN. Например, если вы хотите получить задачу вместе со связанным пользователем, напишите запрос следующим образом:
Task.find({ where: { id: id } }, include: ['User'])
.error(function(err) {
// error callback
})
.success(function(task) {
task.getUser(); // does not trigger a new query
});
Это еще одна недокументированная функция, хотя документация должна быть обновлена в ближайшее время .
Миграции
Sequelize предоставляет утилиту командной строки миграции. Но поскольку он позволяет только модифицировать модель с помощью команд Sequelize (и не вызывать какой-либо собственный асинхронный метод ), эта команда миграции не выполняется .
На данный момент мы выполняем миграции вручную, используя пронумерованные файлы SQL и специальную утилиту для их запуска по порядку.
Пользовательские запросы SQL
Sequelize построен на адаптерах базы данных и, таким образом, предоставляет возможность выполнять произвольные запросы SQL к базе данных. Вот пример:
var util = require('util');
var query = 'SELECT * FROM `Task` ' +
'LEFT JOIN `User` ON `Task`.`userid` = `User`.`id` ' +
'WHERE `User`.`last_name` = %s';
var escapedName = sequelize.constructor.Utils.escape(last_name);
queryWithParams = util.format(query, escapedName);
sequelize.query(queryWithParams, Task)
.error(function(err) {
// error callback
})
.success(function(tasks) {
task.getUser(); // does not trigger a new query
});
sequelize.query()возвращает обещание, как и другие функции запроса. Если вы предоставляете модель для использования в гидратации ( Taskв данном случае), query()метод возвращает экземпляры модели, а не простой JSON.
Обратите внимание, что вы должны экранировать значения вручную, прежде чем объединять их в запрос SQL. Для строк, sequelize.constructor.Utils.escape()это твой друг. Для целых чисел, util.format('%d')нужно сделать свое дело.
Вывод
Готов ли Sequelize к прайм-тайм? Почти. Кривая обучения длится дольше из-за неполной документации, но большинство функций, требуемых для ORM производственного уровня, присутствуют. Тем не менее, я бы не рекомендовал его для производства, пока вы не готовы работать на своем собственном форке, поскольку скорость слияния PR в репозитории Sequelize GitHub низкая.
