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 низкая.