Миграции Active Record

Миграции - это особенность Active Record, позволяющая изменять схему вашей базы данных время от времени. Вместо того, чтобы записывать изменения схемы на чистом SQL, миграции позволяют использовать простой Ruby DSL для описания изменений в ваших таблицах.

После прочтения этого руководства, вы узнаете о:

  • Генераторах, используемых для их создания
  • Методах Active Record, обеспечивающих взаимодействие с вашей базой данных
  • Задачи bin/rails, воздействующие на миграции и вашу схему
  • Как миграции связаны со schema.rb

1. Обзор миграций

Миграции - это удобный способ изменять схему вашей базы данных всё время неизменным и простым образом. Они используют Ruby DSL. Поэтому вам не нужно писать SQL вручную, позволяя вашей схеме быть независимой от базы данных.

Каждую миграцию можно рассматривать как новую 'версию' базы данных. Схема изначально ничего не содержит, а каждая миграция изменяет ее, добавляя или убирая таблицы, столбцы или записи. Active Record знает, как обновлять вашу схему со временем, перенося ее из определенной точки в прошлом в последнюю версию. Active Record также обновляет ваш файл db/schema.rb, чтобы он соответствовал текущей структуре вашей базы данных.

Вот пример миграции:

class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.string :name
      t.text :description

      t.timestamps
    end
  end
end

Эта миграция добавляет таблицу products со строковым столбцом name и текстовым столбцом description. Первичный ключ, названный id, также будет неявно добавлен по умолчанию, так как это первичный ключ по умолчанию для всех моделей Active Record. Макрос timestamps добавляет два столбца, created_at и updated_at. Эти специальные столбцы автоматически управляются Active Record, если существуют.

Отметьте, что мы определили изменение, которое мы хотим, чтобы произошло при движении вперед во времени. До запуска этой миграции таблицы нет. После - таблица будет существовать. Active Record также знает, как обратить эту миграцию: если мы откатываем эту миграцию, он удалит таблицу.

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

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

Если хотите миграцию для чего-то, что Active Record не знает, как обратить, вы можете использовать reversible:

class ChangeProductsPrice < ActiveRecord::Migration[5.0]
  def change
    reversible do |dir|
      change_table :products do |t|
        dir.up   { t.change :price, :string }
        dir.down { t.change :price, :integer }
      end
    end
  end
end

С другой стороны, можно использовать up и down вместо change:

class ChangeProductsPrice < ActiveRecord::Migration[5.0]
  def up
    change_table :products do |t|
      t.change :price, :string
    end
  end

  def down
    change_table :products do |t|
      t.change :price, :integer
    end
  end
end

2. Создание миграции

2.1. Создание автономной миграции

Миграции хранятся как файлы в директории db/migrate, один файл на каждый класс. Имя файла имеет вид YYYYMMDDHHMMSS_create_products.rb, это означает, что временная метка UTC идентифицирует миграцию, затем идет знак подчеркивания, затем идет имя миграции, где слова разделены подчеркиваниями. Имя класса миграции содержит буквенную часть названия файла, но уже в формате CamelCase (т.е. слова пишутся слитно, каждое слово начинается с большой буквы). Например, 20080906120000_create_products.rb должен определять класс CreateProducts, а 20080906120001_add_details_to_products.rb должен определять AddDetailsToProducts. Rails использует эту метку, чтобы определить, какая миграция должна быть запущена и в каком порядке, так что если вы копируете миграции из другого приложения или генерируете файл сами, будьте более бдительны.

Конечно, вычисление временных меток не забавно, поэтому Active Record предоставляет генератор для управления этим:

$ bin/rails generate migration AddPartNumberToProducts

Это создаст пустую, но правильно названную миграцию:

class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
  def change
  end
end

Если имя миграции имеет форму "AddXXXToYYY" или "RemoveXXXFromYYY" и далее следует перечень имен столбцов и их типов, то в миграции будут созданы соответствующие выражения add_column и remove_column.

$ bin/rails generate migration AddPartNumberToProducts part_number:string

создаст

class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
  def change
    add_column :products, :part_number, :string
  end
end

Если вы хотите добавить индекс на новый столбец, вы можете сделать это так

$ bin/rails generate migration AddPartNumberToProducts part_number:string:index

создаст

class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
  def change
    add_column :products, :part_number, :string
    add_index :products, :part_number
  end
end

Точно так же, вы можете сгенерировать миграцию для удаления столбца из командной строки:

$ bin/rails generate migration RemovePartNumberFromProducts part_number:string

создаст

class RemovePartNumberFromProducts < ActiveRecord::Migration[5.0]
  def change
    remove_column :products, :part_number, :string
  end
end

Вы не ограничены одним создаваемым столбцом. Например:

$ bin/rails generate migration AddDetailsToProducts part_number:string price:decimal

создаст

class AddDetailsToProducts < ActiveRecord::Migration[5.0]
  def change
    add_column :products, :part_number, :string
    add_column :products, :price, :decimal
  end
end

Если имя миграции имеет форму "CreateXXX" и затем следует список имен и типов столбцов, то будет сгенерирована миграция, создающая таблицу XXX с перечисленными столбцами. Например:

$ bin/rails generate migration CreateProducts name:string part_number:string

создаст

class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.string :name
      t.string :part_number
    end
  end
end

Как всегда, то, что было сгенерировано, является всего лишь стартовой точкой. Вы можете добавлять и убирать строки, как считаете нужным, отредактировав файл db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb.

Также генератор принимает такой тип столбца, как references (или его псевдоним belongs_to). Например

$ bin/rails generate migration AddUserRefToProducts user:references

создаст

class AddUserRefToProducts < ActiveRecord::Migration[5.0]
  def change
    add_reference :products, :user, index: true, foreign_key: true
  end
end

Эта миграция создаст столбец user_id и соответствующий индекс.

Существует также генератор, который будет производить объединение таблиц, если JoinTable является частью названия.

Например

$ bin/rails generate migration CreateJoinTableCustomerProduct customer product

Сгенерирует следующую миграцию:

class CreateJoinTableCustomerProduct < ActiveRecord::Migration[5.0]
  def change
    create_join_table :customers, :products do |t|
      # t.index [:customer_id, :product_id]
      # t.index [:product_id, :customer_id]
    end
  end
end

2.2. Генераторы модели

Генераторы модели и скаффолда создадут миграции, подходящие для создания новой модели. Миграция будет содержать инструкции для создания соответствующей таблицы. Если вы сообщите Rails, какие столбцы вы хотите, то выражения для добавления этих столбцов также будут созданы. Например, запуск:

$ bin/rails generate model Product name:string description:text

создаст миграцию, которая выглядит так

class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.string :name
      t.text :description

      t.timestamps
    end
  end
end

Можно определить сколько угодно пар имя_столбца/тип.

2.3. Передача модификаторов

Некоторые часто используемые модификаторы типа могут быть переданы непосредственно в командной строке. Они указываются в фигурных скобках и следуют за типом поля:

К примеру, запуск:

$ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}

создаст миграцию, которая выглядит как эта:

class AddDetailsToProducts < ActiveRecord::Migration[5.0]
  def change
    add_column :products, :price, :decimal, precision: 5, scale: 2
    add_reference :products, :supplier, polymorphic: true, index: true
  end
end

Чтобы узнать подробности, обратите внимание на выводимые сообщения генератора.

3. Написание миграции

Как только вы создали свою миграцию, используя один из генераторов, пришло время поработать!

3.1. Создание таблицы

Метод create_table один из самых фундаментальных, но в большинстве случаев, он будет создан для вас генератором модели или скаффолда. Обычное использование такое

create_table :products do |t|
  t.string :name
end

Это создаст таблицу products со столбцом name (и, как обсуждалось выше, подразумеваемым столбцом id).

По умолчанию create_table создаст первичный ключ, названный id. Вы можете изменить имя первичного ключа с помощью опции :primary_key (не забудьте также обновить соответствующую модель), или, если вы вообще не хотите первичный ключ, можно указать опцию id: false. Если нужно передать базе данных специфичные опции, вы можете поместить фрагмент SQL в опцию :options. Например:

create_table :products, options: "ENGINE=BLACKHOLE" do |t|
  t.string :name, null: false
end

добавит ENGINE=BLACKHOLE к SQL выражению, используемому для создания таблицы (при использовании MySQL по умолчанию передается ENGINE=InnoDB).

3.2. Создание соединительной таблицы

Миграционный метод create_join_table создает соединительную таблицу HABTM (has and belongs to many, многие ко многим). Обычное использование будет таким:

create_join_table :products, :categories

что создаст таблицу categories_products с двумя столбцами по имени category_id и product_id. У этих столбцов есть опция :null, установленная в false по умолчанию. Это может быть переопределено опцией :column_options:

create_join_table :products, :categories, column_options: { null: true }

По умолчанию, имя соединительной таблицы получается как соединение первых двух аргументов, переданных в create_join_table, в алфавитном порядке. Чтобы настроить имя таблицы, передайте опцию :table_name:

create_join_table :products, :categories, table_name: :categorization

создает таблицу categorization.

По умолчанию create_join_table создаст два столбца без опций, но можно определить эти опции с использованием опции :column_options. Например,

create_join_table :products, :categories, column_options: {null: true}

создаст product_id и category_id с опцией :null равной true.

create_join_table также принимает блок, который можно использовать для добавления индексов (которые по умолчанию не создаются) или дополнительных столбцов:

create_join_table :products, :categories do |t|
  t.index :product_id
  t.index :category_id
end

3.3. Изменение таблиц

Близкий родственник create_table это change_table, используемый для изменения существующих таблиц. Он используется подобно create_table, но у объекта, передаваемого в блок, больше методов. Например:

change_table :products do |t|
  t.remove :description, :name
  t.string :part_number
  t.index :part_number
  t.rename :upccode, :upc_code
end

удаляет столбцы description и name, создает строковый столбец part_number и добавляет индекс на него. Наконец, он переименовывает столбец upccode.

3.4. Изменение столбцов

Подобно remove_column и add_column, Rails предоставляет миграционный метод change_column.

change_column :products, :part_number, :text

Он меняет тип столбца part_number в таблице products на :text. Отметьте, что команда change_column — необратима.

Кроме change_column, методы change_column_null и change_column_default используются чтобы изменить ограничение не-null или значение столбца по умолчанию.

change_column_null :products, :name, false
change_column_default :products, :approved, from: true, to: false

Это настроит поле :name в products быть NOT NULL столбцом и изменит значение по умолчанию для поля :approved с true на false.

Note: Также можно написать предыдущую миграцию change_column_default как change_column_default :products, :approved, false, но, в отличие от предыдущего примера, это сделало бы вашу миграцию необратимой.

3.5. Модификаторы столбца

Модификаторы столбца могут быть применены при создании или изменении столбца:

  • limit Устанавливает максимальный размер полей string/text/binary/integer.
  • precision Определяет точность для полей decimal, определяющую общее количество цифр в числе.
  • scale Определяет масштаб для полей decimal, определяющий количество цифр после запятой.
  • polymorphic Добавляет столбец type для связей belongs_to.
  • null Позволяет или запрещает значения NULL в столбце.
  • default Позволяет установить значение по умолчанию для столбца. Отметьте, что если вы используете динамическое значение (такое как дату), значение по умолчанию будет вычислено лишь один раз (т.е. на дату, когда миграция будет применена).
  • index Добавляет индекс для столбца.

Некоторые адаптеры могут поддерживать дополнительные опции; за подробностями обратитесь к документации API конкретных адаптеров.

3.6. Внешние ключи

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

add_foreign_key :articles, :authors

Это добавит новый внешний ключ к столбцу author_id таблицы articles. Ключ ссылается на столбец id таблицы authors. Если имена столбцов не могут быть произведены из имен таблиц, можно использовать опции :column и :primary_key.

Rails сгенерирует имя для каждого внешнего ключа, начинающееся с fk_rails_ плюс 10 символов, которые детерминировано генерируются на основе from_table и column. Также есть опция :name, если хотите указать другое имя.

Active Record поддерживает внешние ключи только для отдельных столбцов. Чтобы использовать составные внешние ключи, требуются execute и structure.sql. Смотрите Экспорт схемы

Убрать внешний ключ также просто:

# позволим Active Record вычислить имя столбца
remove_foreign_key :accounts, :branches

# уберем внешний ключ для определенного столбца
remove_foreign_key :accounts, column: :owner_id

# уберем внешний ключ по имени
remove_foreign_key :accounts, name: :special_fk_name

3.7. Когда хелперов недостаточно

Если хелперов, предоставленных Active Record, недостаточно, можно использовать метод execute для запуска произвольного SQL:

Product.connection.execute("UPDATE products SET price = 'free' WHERE 1=1")

Больше подробностей и примеров отдельных методов содержится в документации по API. В частности, документация для ActiveRecord::ConnectionAdapters::SchemaStatements (который обеспечивает методы, доступные в методах up, down и change), ActiveRecord::ConnectionAdapters::TableDefinition (который обеспечивает методы, доступные у объекта, переданного методом create_table) и ActiveRecord::ConnectionAdapters::Table (который обеспечивает методы, доступные у объекта, переданного методом change_table).

3.8. Использование метода change

Метод change это основной метод написания миграций. Он работает в большинстве случаев, когда Active Record знает, как обратить миграцию автоматически. На текущий момент метод change поддерживает только эти определения миграции:

  • add_column
  • add_foreign_key
  • add_index
  • add_reference
  • add_timestamps
  • change_column_default (необходимо указать опции :from и :to)
  • change_column_null
  • create_join_table
  • create_table
  • disable_extension
  • drop_join_table
  • drop_table (необходимо указать блок)
  • enable_extension
  • remove_column (необходимо указать тип)
  • remove_foreign_key (необходимо указать вторую таблицу)
  • remove_index
  • remove_reference
  • remove_timestamps
  • rename_column
  • rename_index
  • rename_table

change_table так же является обратимым, пока блок не вызывает change, change_default или remove.

remove_column обратима, если предоставить тип столбца третьим аргументом. Также предоставьте опции оригинально столбца, иначе Rails не сможет в точности пересоздать этот столбец при откате:

remove_column :posts, :slug, :string, null: false, default: '', index: true

Если вы нуждаетесь в использовании иных методов, следует использовать reversible или писать методы up и down вместо метода change.

3.9. Использование reversible

Комплексная миграция может включать процессы, которые Active Record не знает как обратить. Вы можете использовать reversible, чтобы указать что делать когда запускается миграция и когда она требует отката. Например:

class ExampleMigration < ActiveRecord::Migration
  def change
    create_table :distributors do |t|
      t.string :zipcode
    end

    reversible do |dir|
      dir.up do
        # добавим ограничение CHECK
        execute <<-SQL
          ALTER TABLE distributors
            ADD CONSTRAINT zipchk
              CHECK (char_length(zipcode) = 5) NO INHERIT;
        SQL
      end
      dir.down do
        execute <<-SQL
          ALTER TABLE distributors
            DROP CONSTRAINT zipchk
        SQL
      end
    end

    add_column :users, :home_page_url, :string
    rename_column :users, :email, :email_address
  end
end

Использование reversible гарантирует, что инструкции выполнятся в правильном порядке. Если предыдущий пример миграции откатывается, down блок начнёт выполнятся после того как столбец home_page_url будет удалён и перед перед тем как произойдёт удаление таблицы distributors.

Иногда миграция будет делать то, что просто необратимо; например, она может уничтожить некоторые данные. В таких случаях, вы можете вызвать ActiveRecord::IrreversibleMigration в вашем down блоке. Если кто-либо попытается отменить вашу миграцию, будет отображена ошибка, что это не может быть выполнено.

3.10. Использование методов up/down

Вы также можете использовать старый стиль миграций используя up и down методы, вместо change. Метод up должен описывать изменения, которые вы хотите внести в вашу схему, а метод down вашей миграции должен обращать изменения, внесенные методом up. Другими словами, схема базы данных должна остаться неизменной после выполнения up, а затем down. Например, если вы создали таблицу в методе up, ее следует удалить в методе down. Разумно производить отмену изменений в полностью противоположном порядке тому, в котором они сделаны в методе up. Например в сравнении с кодом reversible следующий код будет эквивалентным

class ExampleMigration < ActiveRecord::Migration[5.0]
  def up
    create_table :distributors do |t|
      t.string :zipcode
    end

    #добавляем ограничение CHECK
    execute <<-SQL
      ALTER TABLE distributors
        ADD CONSTRAINT zipchk
        CHECK (char_length(zipcode) = 5);
    SQL

    add_column :users, :home_page_url, :string
    rename_column :users, :email, :email_address
  end

  def down
    rename_column :users, :email_address, :email
    remove_column :users, :home_page_url

    execute <<-SQL
      ALTER TABLE distributors
        DROP CONSTRAINT zipchk
    SQL

    drop_table :distributors
  end
end

Если ваша миграция не обратима вам следует вызвать ActiveRecord::IrreversibleMigration из вашего метода down. Если кто-либо попытается отменить вашу миграцию, будет отображена ошибка, что это не может быть выполнено.

3.11. Возвращение к предыдущим миграциям

Вы можете использовать возможность Active Record откатить миграции используя revert метод:

require_relative '20121212123456_example_migration'

class FixupExampleMigration < ActiveRecord::Migration[5.0]
  def change
    revert ExampleMigration

    create_table(:apples) do |t|
      t.string :variety
    end
  end
end

Метод revert так же может принимает блок. Это может быть полезным для отката выбранной части предыдущих миграций. Для примера, давайте представим что ExampleMigration закоммичена, а позже мы решили, что было бы лучше использовать валидации Active Record, вместо ограничения CHECK, для проверки zipcode.

class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration[5.0]
  def change
    revert do
      reversible do |dir|
        dir.up do
          # добавим ограничение CHECK
          execute <<-SQL
            ALTER TABLE distributors
              ADD CONSTRAINT zipchk
                CHECK (char_length(zipcode) = 5);
          SQL
        end
        dir.down do
          execute <<-SQL
            ALTER TABLE distributors
              DROP CONSTRAINT zipchk
          SQL
        end
      end

      # The rest of the migration was ok
    end
  end
end

Подобная миграция так же может быть написана без использования revert, но это бы привело к ещё нескольким шагам: изменение порядка(следования) create table и reversible, замена create_table на drop_table и в конечном итоге изменение up down наоборот. Обо всём этом уже позаботился revert.

Если хотите добавить ограничения CHECK, как в вышеуказанных примерах, нужно использовать structure.sql в качестве метода экспорта. Смотрите Экспорт схемы.

4. Запуск миграций

Rails предоставляет ряд задач bin/rails для запуска определенных наборов миграций.

Самая первая команда bin/rails, относящаяся к миграциям, которую вы будете использовать, это rails db:migrate. В своей основной форме она всего лишь запускает метод change или up для всех миграций, которые еще не были запущены. Если таких миграций нет, она выходит. Она запустит эти миграции в порядке, основанном на дате миграции.

Заметьте, что запуск db:migrate также вызывает задачу db:schema:dump, которая обновляет ваш файл db/schema.rb в соответствии со структурой вашей базы данных.

Если вы определите целевую версию, Active Record запустит требуемые миграции (методы up, down или change), пока не достигнет требуемой версии. Версия это числовой префикс у файла миграции. Например, чтобы мигрировать к версии 20080906120000, запустите:

$ bin/rails db:migrate VERSION=20080906120000

Если версия 20080906120000 больше текущей версии (т.е. миграция вперед) это запустит метод change (или up) для всех миграций до и включая 20080906120000, но не запустит какие-либо поздние миграции. Если миграция назад, это запустит метод down для всех миграций до, но не включая, 20080906120000.

4.1. Откат

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

$ bin/rails db:rollback

Это вернёт ситуацию к последней миграции, или обратив метод change, или запустив метод down. Если нужно отменить несколько миграций, можно указать параметр STEP:

$ bin/rails db:rollback STEP=3

произойдёт откат на 3 последних миграции.

Задача db:migrate:redo это ярлык для выполнения отката, а затем снова запуска миграции. Так же, как и с задачей db:rollback можно указать параметр STEP, если нужно работать более чем с одной версией, например:

$ bin/rails db:migrate:redo STEP=3

Ни одна из этих команд bin/rails не может сделать ничего такого, чего нельзя было бы сделать с db:migrate. Они просто более удобны, так как вам не нужно явно указывать версию миграции, к которой нужно мигрировать.

4.2. Установка базы данных

Задача rails db:setup создаст базу данных, загрузит схему и инициализирует ее с помощью данных seed.

4.3. Сброс базы данных

Задача rails db:reset удалит базу данных и установит ее заново. Функционально это эквивалентно rails db:drop db:setup.

Это не то же самое, что запуск всех миграций. Оно использует только текущее содержимое файла db/schema.rb или db/structure.sql. Если миграция не может быть откачена, 'rails db:reset' может не помочь вам. Подробнее об экспорте схемы смотрите раздел Экспорт схемы.

4.4. Запуск определенных миграций

Если вам нужно запустить определенную миграцию (up или down), задачи db:migrate:up и db:migrate:down сделают это. Просто определите подходящий вариант и у соответствующей миграции будет вызван метод change, up или down, например:

$ bin/rails db:migrate:up VERSION=20080906120000

запустит метод up у миграции 20080906120000. Эта задача сперва проверит, была ли миграция уже выполнена, и ничего делать не будет, если Active Record считает, что она уже была запущена.

4.5. Запуск миграций в различных средах

По умолчанию запуск bin/rails db:migrate запустится в окружении development. Для запуска миграций в другом окружении, его можно указать, используя переменную среды RAILS_ENV при запуске команды. Например, для запуска миграций в среде test, следует запустить:

$ bin/rails db:migrate RAILS_ENV=test

4.6. Изменение вывода результата запущенных миграций

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

==  CreateProducts: migrating =================================================
-- create_table(:products)
   -> 0.0028s
==  CreateProducts: migrated (0.0028s) ========================================

Некоторые методы в миграциях позволяют вам все это контролировать:

Метод Назначение
suppress_messages Принимает блок как аргумент и запрещает любой вывод, сгенерированный этим блоком.
say Принимает сообщение как аргумент и выводит его как есть. Может быть передан второй булевый аргумент для указания, нужен отступ или нет.
say_with_time Выводит текст вместе с продолжительностью выполнения блока. Если блок возвращает число, предполагается, что это количество затронутых строк.

Например, эта миграция:

class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    suppress_messages do
      create_table :products do |t|
        t.string :name
        t.text :description
        t.timestamps
      end
    end

    say "Created a table"

    suppress_messages {add_index :products, :name}
    say "and an index!", true

    say_with_time 'Waiting for a while' do
      sleep 10
      250
    end
  end
end

сгенерирует следующий результат

==  CreateProducts: migrating =================================================
-- Created a table
   -> and an index!
-- Waiting for a while
   -> 10.0013s
   -> 250 rows
==  CreateProducts: migrated (10.0054s) =======================================

Если хотите, чтобы Active Record ничего не выводил, запуск rails db:migrate VERBOSE=false запретит любой вывод.

5. Изменение существующих миграций

Периодически вы будете делать ошибки при написании миграции. Если вы уже запустили миграцию, вы не сможете просто отредактировать миграцию и запустить ее снова: Rails посчитает, что он уже выполнял миграцию, и ничего не сделает при запуске rails db:migrate. Вы должны откатить миграцию (например, с помощью bin/rails db:rollback), отредактировать миграцию и затем запустить rails db:migrate для запуска исправленной версии.

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

Метод revert может быть очень полезным при написании новой миграции для возвращения предыдущей в целом или какой то части (смотрите Возвращение к предыдущим миграциям

6. Экспорт схемы

6.1. Для чего нужны файлы схемы?

Миграции, какими бы не были они мощными, не являются авторитетным источником для вашей схемы базы данных. Это роль достается или файлу db/schema.rb, или файлу SQL, которые генерирует Active Record при исследовании базы данных. Они разработаны не для редактирования, они всего лишь отражают текущее состояние базы данных.

Не нужно (это может привести к ошибке) развертывать новый экземпляр приложения, применяя всю историю миграций. Намного проще и быстрее загрузить в базу данных описание текущей схемы.

Например, как создается тестовая база данных: текущая рабочая база данных выгружается (или в db/schema.rb, или в db/structure.sql), а затем загружается в тестовую базу данных.

Файлы схемы также полезны, если хотите быстро взглянуть, какие атрибуты есть у объекта Active Record. Эта информация не содержится в коде модели и часто размазана по нескольким миграциям, но собрана воедино в файле схемы. Имеется гем annotate_models, который автоматически добавляет и обновляет комментарии в начале каждой из моделей, составляющих схему, если хотите такую функциональность.

6.2. Типы выгрузок схемы

Есть два способа выгрузить схему. Они устанавливаются в config/environment.rb в свойстве config.active_record.schema_format, которое может быть или :sql, или :ruby.

Если выбрано :ruby, тогда схема хранится в db/schema.rb. Посмотрев в этот файл, можно увидеть, что он очень похож на одну большую миграцию:

ActiveRecord::Schema.define(version: 20080906171750) do
  create_table "authors", force: true do |t|
    t.string   "name"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  create_table "products", force: true do |t|
    t.string   "name"
    t.text "description"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.string "part_number"
  end
end

Во многих случаях этого достаточно. Этот файл создается с помощью проверки базы данных и описывает свою структуру, используя create_table, add_index и так далее. Так как он не зависит от типа базы данных, он может быть загружен в любую базу данных, поддерживаемую Active Record. Это очень полезно, если Вы распространяете приложение, которое может быть запущено на разных базах данных.

Однако, тут есть компромисс: db/schema.rb не может описать специфичные элементы базы данных, такие как триггеры, хранимые процедуры или ограничения CHECK. В то время как в миграции вы можете выполнить произвольное выражение SQL, выгрузчик схемы не может воспроизвести эти выражения из базы данных. Если Вы используете подобные функции, нужно установить формат схемы :sql.

Вместо использования выгрузчика схемы Active Records, структура базы данных будет выгружена с помощью инструмента, предназначенного для этой базы данных (с помощью задачи rails db:structure:dump) в db/structure.sql. Например, для PostgreSQL используется утилита pg_dump. Для MySQL этот файл будет содержать результат SHOW CREATE TABLE для разных таблиц.

Загрузка таких схем это просто запуск содержащихся в них выражений SQL. По определению создастся точная копия структуры базы данных. Использование формата :sql схемы, однако, предотвращает загрузку схемы в СУБД иную, чем использовалась при ее создании.

6.3. Выгрузки схем и контроль исходного кода

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

db/schema.rb содержит число текущей версии базы данных. Это обеспечивает возникающие конфликты в случае слияния двух веток, каждая из которых затрагивала схему. Когда такое случится, исправьте конфликты вручную, оставив наибольшее число версии.

7. Active Record и ссылочная целостность

Способ Active Record требует, чтобы логика была в моделях, а не в базе данных. По большому счету, функции, такие как триггеры или ограничения, которые переносят часть логики обратно в базу данных, не используются активно.

Валидации, такие как validates :foreign_key, uniqueness: true, это один из способов, которым ваши модели могут соблюдать ссылочную целостность. Опция :dependent в связях позволяет моделям автоматически уничтожать дочерние объекты при уничтожении родителя. Подобно всему, что работает на уровне приложения, это не может гарантировать ссылочной целостности, таким образом кто-то может добавить еще и внешние ключи как ограничители ссылочной целостности в базе данных.

Хотя Active Record не предоставляет каких-либо инструментов для работы напрямую с этими функциями, метод execute может использоваться для запуска произвольного SQL.

8. Миграции и сиды

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

class AddInitialProducts < ActiveRecord::Migration[5.0]
  def up
    5.times do |i|
      Product.create(name: "Product ##{i}", description: "A product.")
    end
  end

  def down
    Product.delete_all
  end
end

Чтобы добавить изначальные данные в базу данных после создания, в Rails имеется встроенная особенность 'seeds', которая делает процесс быстрым и простым. Это особенно полезно при частой перезагрузке базы данных в средах разработки и тестирования. Этой особенностью легко начать пользоваться: просто заполните db/seeds.rb некоторым кодом Ruby и запустите rails db:seed:

5.times do |i|
  Product.create(name: "Product ##{i}", description: "A product.")
end

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