Rails миграции - факты, которых вы, возможно не знали

Что такое миграции и зачем они нужны?

Эта статья рассказывает о том, что такое миграции баз данных, и, о некоторых возможностях в Rails, о которых не все знают. Если Вы ищете конкретное решение, то Вам будет проще найти его в моем сборнике рецептов

Миграции - это простой механизм управления структурой базы данный и данными в ней. Используя данный механизм программисты смогут легко синхронизиорвать структуру базы данных на локальных машинах и продакшен серверах.

Вместо того, чтобы изменять структуру и данные в базе данных руками, программист описывает миграцию - как некий процесс изменения текущего состояния базы к требуемому, Для этого он должен описать шаг UP

На псевдо языке примерно так

UP {
  СОЗДАТЬ ТАБЛИЦУ “пользователи”
  ДОБАВИТЬ КОЛОНКУ К “пользователи” С ИМЕНЕМ “имя” И ТИПОМ “строка”
  ДОБАВИТЬ КОЛОНКУ К “проекты” С ИМЕНЕМ “инедтификатор_пользователя” И ТИПОМ “число”
}

и, тогда все другие программисты, получив эту команду, смогут выполнить ее у себя, тем самым изменив свою базу соответсвующим образом.

Но, вполне возможно, что программист ошибся описывая свои изменения, либо от них по каким-то причинам было решено отказаться, Для этого, программист вместе с методом UP должен описать метод DOWN, которые позволит откатить изменения до первоначального состояния

DOWN {
  УДАЛИТЬ ТАБЛИЦУ “пользователи”
  УДАЛИТЬ КОЛОНКУ “идентификатор пользователя” В ТАБЛИЦЕ “проекты” 
}

Теперь наши данные будут целостными в любом случае

Миграции в Rails

Для создания новых миграций в rails есть встроенный генератор


$ rails generate migration AddFirstAndLastNameToUsers
    invoke  active_record
    create  db/migrate/20150713171627_add_first_and_last_name_to_users.rb

Он сгенерирует новый файл в папке #{APP_ROOT}db/migrate/


class AddFirstAndLastNameToUsers < ActiveRecord::Migration
  def up 
    add_column :users, :first_name, :string
    add_column :users, :last_name, :string
  end
  
  def down
    remove_column :users, :first_name
    remove_column :users, :last_name
  end
end

Так выглядели миграции в Rails 2

Начиная с Rails 3 миграции стали более умными. Стало очевидно, что описание способа миграции описывает и способ роллбека. Например, очевидно, что add_column :users, :first_name, :string откатывается командой remove_column :users, :first_name. create_table :projects - drop_table :projects и т.д;
Но change_column :users, :email, :text - не дает информации для отката. Она говорит, что поле :email должно стать текстом, но не дает никакой информации о типе поля, который был раньше. И, в таких случаях нам по прежнему были нужны методы up и down

Используем change вместо up и down


class AddFirstAndLastNameToUsers < ActiveRecord::Migration
  
  def change
    add_column :users, :first_name, :string
    add_column :users, :last_name, :string
  end
  
end

Небольшой справочный материал

Доступные методы для изменения структуры данных

  1. create_table(name, options): Создать таблицу с именем name и в больщинстве случаев в качестве параметра принимает блок, который принимает объект таблицы и позволяет управлять структурой таблицы.Подробнее в официальной документации. В качестве опций может принимать следующие параметры:
    1. :id - По умолчанию миграция автоматиччески добавит в таблицу primary_key c именем :id. Чтобы это отключить передаем опцию id: false
    2. :primary_key - имя первичного ключа если id: false. ActiveRecord автоматически подхватит новый ключ
    3. :temporary - сделать таблицу временной
    4. :force - По умолчанию false. При значении true удалит перед созданием таблицу с таким же именем. При значении :cascade удалит также все связанные таблицы
    5. :as - позволяет указать SQL запрос для создания таблицы, При этом блок, опции :id и :primary_key игнорируются/span>
  2. drop_table(name): Удалит таблицу с именем name
  3. change_table(name, options): - Позволяет изменить таблицу с именем name, при этом синтаксис такой же как и с create_table.
  4. rename_table(old_name, new_name): Переименование таблицы
  5. add_column(table_name, column_name, type, options): - Добавление колонки. В качестве опций может принимать следующие параметры:
    1. :limit - максимальная длина строки для типов :string && :text и количество байтов для :binary и :integer
    2. :default - Значение по умолчанию.
    3. :null - null: false наложит NOT NULL ограничение на колонку
    4. :precision - максимальное число цифр в типа :decimal
    5. :scale - количество цифр после запятой в типе :decimal
  6. rename_column(table_name, column_name, new_column_name): - переименование столбца
  7. change_column(table_name, column_name, type, options): Изменение столбца - те же опции что и для добавления в зависимости от типа столбца
  8. remove_column(table_name, column_name, type, options): Удаление столбца - те же опции что и для добавления в зависимости от типа столбц
  9. add_index(table_name, column_names, options): - Добавление индекса, примеры можно найти в официальной документации
  10. remove_index(table_name, column: column_name):- Удаляет индекс

На самом деле таких методов намного больше, я привел только самые часто используемые. Полный перечень всех методов можно найти здесь

Rails 4 принес нам Reversible

$ rails generate migration ReplaceFirstNameAndLAstNameWIthName
    invoke  active_record
    create  db/migrate/20150713174412_replace_first_name_and_l_ast_name_w_ith_name.rb

Создаем новую миграцию. Ее целью будет замена в таблице :users полей last_name и first_name на одно поле name и сохранение существующих данных


class ReplaceFirstNameAndLAstNameWIthName < ActiveRecord::Migration
  def change
    add_column :users, :name, :string

    reversible do |direction|
      User.reset_column_information
      User.all.each do |user|
        direction.up { user.name= "#{user.first_name} #{user.last_name}" }
        direction.down { user.first_name, user.last_name = user.name.split(' ') }

        user.save
      end
    end

    revert do
      add_column :users, :first_name, :string
      add_column :users, :last_name, :string
    end
  end
end

Давайте разберем подробнее, что здесь происходит. Метод revert принимает параметром блок и позволяет описывать часть миграции с обращаемой стороны, Т.е.


revert do
  add_column :users, :first_name, :string
  add_column :users, :last_name, :string
end

#эквавалентно

remove_column_users, :first_name
remove_column_users, :last_name

Но, во втором случае было бы невозможно выполнять миграцию назад.

Так же важно понимать, как работает сам механизм миграции, Когда rails выполняют команду add_column :users, :name, :string - они физически ничего в базе не изменяют, Они составляют очередь изменений, которые необходимо выполнить и запускают их в нужном порядке, Т.е. при проходе UP выполнится add_column, reversible, revert, при проходе DOWN - revert, reversible, remove_column

Миграции больших объемов данных

Довольно часто нам нужно изменить структуру данных не потеряв данные.Все методы и средства Rails прямо из миграции, но, при большом объеме данных такая миграция может занимать не приемлемое количество времени. В таком случае лучше использовать функии базы данных для ускорения этой обработки, Пример с генерацией имени пользователя. Миграцию вперед мы заменили на быструю миграцию использюя функцию concat PostgreSQL


class ReplaceFirstNameAndLAstNameWIthName < ActiveRecord::Migration
  def change
    add_column :users, :name, :string

    reversible do |direction|
      direction.up do
       say_with_time ‘Generating names’ do
         execute "UPDATE users SET name = concat(first_name, ' ', last_name)"
       end
      end
      
      direction.down do
        User.reset_column_information
        User.all.each do |user|
          user.first_name, user.last_name = user.name.split(' ')
          user.save
        end   
      end
    end

    revert do
      add_column :users, :first_name, :string
      add_column :users, :last_name, :string
    end
  end
end

Также мы использовали метод say_with_time для отображения времени выполнения длянных операций

Выводы

Миграции отличный механизм для поддержания целостности структуры данных. Они легко позволяют работать в команде и изменять базу данных на сервере. Краткие заметки

  1. Для создания новой миграции можно использовать генератор rails g migration MigrationName
  2. Для запуска миграции rake db:migrate
  3. Для отката миграции rake db:rollback
  4. По возможности, все миграции запускаются в транзакции и все изменения будут отменены, если миграции не удалось успешно завершиться
  5. Всегда используйте default опцию колонки, вместо установки значения перед валидацие в модели.
  6. Для миграции данных лучше использовать чистый SQL
  7. Чтобы сделать SQL немного читаемей можно положить его части в массив, а потом соединить их в один запрос

execute [
      'UPDATE channel_meta AS c_m ',
      'SET inappropriate = true',
      'WHERE',
      'EXISTS(',
        'SELECT channels.id AS channel_id, c.user_id AS user_id',
        'FROM channels',
        "INNER JOIN complaints  AS c ON c.complaintable_id = channels.id AND c.complaintable_type = 'Channel'",
        'WHERE c_m.channel_id = channels.id AND c_m.user_id = c.user_id)'
    ].join(' ')

Больше информации можно найти в API документации, а также в официальных гайдах на русском и на английском

Комментарии