Разворачиваем базу проекта на JavaScript

Растут JavaScript проекты и соответственно становятся лучше инструменты сборки. Самое время задуматься о разворачивании базы своего проекта на JavaScript. А ведь и правда: если проект написан 100% на одной технологии (да, и сервер тоже), зачем думать о другой технологии для разворачивания базы?

Вот с такими мыслями я и начал исследовать данный вопрос с целью найти удобное решение для своего пэт-проектика (стек: JavaScript/AngularJs – JavaScript/Express – MySql). JavaScript сообщество (а точнее Андрей Листочкин), посоветовало обратить внимание на модуль для ноды db-migrate и плагин для гранта grunt-db-migrate. Пост является набором заметок по ходу внедрения модуля в проект.

На данный момент db-migrate поддерживает Mysql,  PostgreSQLsqlite3. Поэтому, если вы хотите мигрировать другую базу – придется либо писать самому скрипт миграции либо использовать другое решение.

Установка

Устанавливаем модуль ноды и плагин гранта:

$ npm install db-migrate
$ npm install grunt-db-migrate

Настраиваем grunt

В grunt файле добавляем блок:

migrate:{
    options:{
        env: {
            DATABASE_URL: databaseUrl
        }
    }
}

databaseUrl – строка подключения к базе, например:

databaseUrl =  'mysql://root:@localhost/mypetdb';

Создаем файл миграции

Чтобы создать файл миграции(с шаблоном внутри) выполняем команду:

$ grunt migrate:create:migrate_name

После чего у нас в проекте появится директория “migrations”, и файл в ней с таким содержимым:

var dbm = require('db-migrate');
var type = dbm.dataType;

exports.up = function(db, callback) {

};

exports.down = function(db, callback) {

};

exports.up – инструкции для наката изменений, export.down – соответственно для отката сделанных изменений.

Разберем пример предложенный на офсайте:

exports.up = function (db, callback) {
  db.createTable('pets', {
    id: { type: 'int', primaryKey: true },
    name: 'string'
  }, callback);
};

exports.down = function (db, callback) {
  db.dropTable('pets', callback);
};

Вроде бы все просто: db.createTable() – добавляем таблицу, db.dropTable() – удаляем.

Свойства, который можно определять для полей:

  • type – тип данный, полный список поддерживаемых типов тут
  • length – размерность (там где поддерживается)
  • primaryKey [true/false]
  • autoIncrement [true/false]
  • notNull [true/false]
  • unique [true/false]
  • defaultValue

Список поддерживаемых методов:

  • createTable(tableName, columnSpec, callback)
  • dropTable(tableName, [options,] callback)
  • renameTable(tableName, newTableName, callback)
  • addColumn(tableName, columnName, columnSpec, callback)
  • removeColumn(tableName, columnName, callback)
  • renameColumn(tableName, oldColumnName, newColumnName, callback)
  • changeColumn(tableName, columnName, columnSpec, callback)
  • addIndex(tableName, indexName, columns, [unique], callback)
  • insert(tableName, columnNameArray, valueArray, callback)
  • removeIndex([tableName], indexName, callback)
  • runSql(sql, [params,] callback)
  • all(sql, [params,] callback)

Запуск

Теперь grunt может выполнить задачу:

$ grunt migrate:up

и чтобы откатить изменения:

$ grunt migrate:down

создать новый файл миграции:

$ grunt migrate:create:migration_name

Дополнительно

Когда нам нужно создать несколько таблиц, удобно использовать модуль async:

var async = require('async');

exports.up = function (db, callback) {
  async.series([
    db.createTable.bind(db, 'pets', {
      id: { type: 'int', primaryKey: true },
      name: 'string'
    }),
    db.createTable.bind(db, 'owners', {
      id: { type: 'int', primaryKey: true },
      name: 'string'
    });
  ], callback);
};

метод async.series позволит избежать вложенности

Пример из реальной жизни

Вот файл миграции, который я создал для своего пет-проектика:

var dbm = require('db-migrate');
var async = require('async');

exports.up = function (db, callback) {
  async.series([
    db.createTable.bind(db, 'marks', {
      user_id: {
        type: 'int',
        notNull: true
      },
      release_id: {
        type: 'int',
        notNull: true
      },
      feed: {
        type:'string',
        notNull: true
      }
    })
  ], function(){
      db.addIndex('marks', 'usermark', ['user_id', 'release_id'], true);
      callback();
  });
};

exports.down = function (db, callback) {
  async.series([
    db.dropTable.bind(db, 'marks', {
      ifExists: true
    }),
    db.removeIndex('marks', 'usermark'),
  ], callback);
};

Проект портировался с локальной mysql базы на postgress, которая стояла на сервере. Особых косяков модуля db-migrate замечено не было. Можно отметить только мелкие неудобства, которых характерны для любых универсальных (кросс-субд) систем миграции – отсутствие реализации специфических типов: у меня изначально был тип ENUM, который пришлось перевести в STRING. Для личного проекта это нормальная замена, но для продакшена пришлось бы писать отдельный дополнительный конвертер.

Более подробно можно почитать на офсайте либо покопать код.