Статьи

Sequelize, JavaScript ORM, на практике

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