Ruby on Rails Cookbook - Миграции
Ruby on Rails Cookbook - Миграции - популярные вопросы
Всем привет. Решил собрать в кучу основные рецепты использования миграций в rails, со сниппетами и быстрой навигацией, чтобы не пришлось гуглить каждый раз если вдруг что-то подзабыл. Может пригодится кому-то еще.
Каждую команду буду описывать в методе, чтобы было понятно, как использовать.
- Операции с таблицей
- Операции со столбцами
- Как добавить столбец
- Как удалить столбец/
- Как переименовать столбец
- Как изменить значение по умолчанию
- Как изменить тип столбца
- Как привести тип при изменнии
- Как добавить таймстампы в существующую таблицу
- Как сделать значением по умолчанию текущее время
- Как ограничить длину строк
- Как изменить NOT NULL constraint
- Как управлять размерностью и точностью для float
- Индексы
- Миграция данных
- Другая полезная информация
Операции с таблицей
Как создать таблицу
def change
create_table :articles do |t|
t.string :title, null: false
t.text :description
t.text :body
t.string :slug
t.integer :user_id
t.timestamps, null: false
end
end
Как удалить таблицу
Миграция не откатываемая, поэтому 2 метода
def up
drop_table :articles
end
def down
create_table :articles do |t|
t.string :title, null: false
t.text :description
t.text :body
t.string :slug
t.integer :user_id
t.timestamps, null: false
end
end
Как переименовать таблицу
def change
rename_table :articles, :blog_articles
end
Как изменить таблицу
Обязательно должна быть обратная миграция
def up
change_table :articles do |t|
t.remove :slug
t.integer :rating
t.rename :description, :short_description
end
end
def down
change_table :articles do |t|
t.remove :rating
t.string :slug
t.rename :short_description, :description
end
end
Как создать таблицу без первичного ключа
Иногда нужно создать таблицу бе ID. Делается это очень просто, добавлением id: false
def change
create_table :user_subscriptions, id: false do |t|
t.string :user_id, null: false
t.string :category_id, null: false
end
end
Операции со столбцами
Как добавить столбец
def change
add_column :articles, :views, :integer, default: 0
end
Как удалить столбец
Обязательно должна быть обратная миграция
def up
remove_column :articles, :views
end
def down
add_column :articles, :views, :integer, default: 0
end
Как переименовать столбец
def change
rename_column :articles, :views, :visits_count
end
Как указать значение по умолчанию
def change
add_column :articles, :views, :integer, default: 0
end
Как изменить значение по умолчанию
А вот этот случай немного сложнее чем кажется, потому что кроме изменения значения по умолчанию обычно еще требуется изменить данные
class Migration
class Article < ActiveRecord::Base ; end
def up
change_column :articles, :views, :integer, default: 1
Article.where(views: 0).update_all(views: 1)
end
def down
change_column :articles, :views, :integer, default: 0
Article.where(views: 1).update_all(views: 0)
end
end
Я объявил класс Article внутри миграции, потому что, на момент прогона миграции в приложении может быть уже другая модель, например BlogArticle
. Важно помнить, что все миграции должны проходить с нуля у того, кто устанавливает проект и переименованая модель может это поломать. В моем случае миграция пройдет всегда.
Как изменить тип столбца
def up
change_column :articles, :views, :string
end
def down
change_column :articles, :views, :integer
end
Как привести тип при изменнии
Но, база данных не всегда способна самостоятельно выполнить преобразование типа. В этом случае ей нужно помочь. Например, вот пример миграции для PostgreSQL для преобразования json колонки в jsonb
def up
change_column :settings, :config, 'jsonb USING CAST(config AS jsonb)'
end
def down
change_column :settings, :config, 'json USING CAST(config AS json)'
end
Как добавить таймстампы в существующую таблицу
def change
add_column :users, :created_at, :datetime
add_column :users, :updated_at, :datetime
end
Как сделать значением по умолчанию текущее время
Казалось бы, что тут сложного
def change
add_column :users, :registered_at, :datetime, null: false, default: Time.now
end
Но, проблема в том, что Time.now вернет значение в момент запуска миграции и именно оно будет всегда использоваться как значение по умолчанию.
Это пожалуй единственный случай, когда дефолтное значение лучше указать через код, а не через базу. Это предотвратит возможную рассинхронизацию дат, если часовой пояс приложения не будет соответсвовать часовому поясу сервера.
Но, если уж ну очень надо - нужно использовать SQL команду, для каждой базы она своя
def change
execute 'alter table foo alter column starts_at set default now()'
end
Как ограничить длину строк
Опять же, два метода, чтобы миграцию можно было откатить.
def up
change_column :articles, :title, :string, limit: 255
end
def down
change_column :articles, :title, :string, limit: nil
end
Этот же пример показывает и как убрать лимит по длине из строки
Как изменить NOT NULL constraint
Убрать NOT NULL ограничение очень просто.
def up
change_column :articles, :published_at, :datetime, null: true
end
def down
change_column :articles, :published_at, :datetime, null: true
end
А вот добавить NOT NULL немного сложнее, потому что сначала нужно заполинть существоющие колонки какими-то значениями. Иногда можно это сделать через значение по умолчанию, а иногда - придется заполнять самому
class Migration
class Article < ActiveRecord::Base ; end
def up
change_column :articles, :views, :integer, default: 0, null: false
Article.update_all(published_at: Time.now)
change_column :articles, :published_at, :datetime, null: false
end
def down
change_column :articles, :views, :integer, default: 0, null: true
change_column :articles, :published_at, :datetime, null: true
end
end
Как управлять размерностью и точностью для float
def up
change_column :articles, :rating, :float, precision: 3, scale: 2
end
def down
change_column :articles, :rating, :float, precision: nil, scale: nil
end
Это значит, что у нас рейтинг может максимум состоять из 3х цифр, две из которых после запятой. Например 4.75
Индексы
Как добавить индекс
def change
add_index :article, :user_id
end
Как добавить индекс уникальности
def change
add_index :users, :email, unique: true
end
Как добавить индекс на несколько колонок
def change
add_index :users, [:first_name, :last_name]
end
Как указать имя индекса
Обычно, когда создается индекс по составному полю, автоматически генерируемое имя индекса длиннее, чем то, которое поддерживает база данных. В таких случаях имя нужно будет задать вручную.
def change
add_index :users, [:long_column_name, :another_long_column_name], name: 'custom_index_name'
end
Миграция данных
Миграция данных обычно довольно трудоемкий процесс. Как правило, это нетривиальная задача, которая имеет несколько вариантов решения и каждый из них имеет свои преймущества и недостатки. У помещения кода миграции данных в миграции базы есть 2 основных преймущества:
- Эту миграцию не забудут прогнать
- В случае неудачи все изменеия откатятся вместе с самой миграцией.
Но, у такого подхода есть и ограничения. Все то время, пока проходят миграции таблицы будут заблокированы, а значит, что пользоваться вашим приложением будет невозможно. Но, если ваши измения не очень сложные, или их можно сделать достаточно быстрыми за счет ислзоваия чистого ruby или еще лучше - SQL, то им самое место в миграциях баз данных.
Как мигрировать данные с помощью ActiveRecord
Не забываем внутри объявлять такой же класс
class MyMigration
class User < ActiveRecord::Base;
def up
add_column :users, :auth_token, :string
add_index :users, :auth_token, unique: true
User.all.each { |user| user.update(auth_token: SecureRandom.hex(12))}
end
def down
remove_column :users, :auth_token
end
end
Как мигрировать данные с помошью SQL
Рассмотрим на примере генерации слага для дружественного URL в таблице пользователей
class MyMigration
class User < ActiveRecord::Base;
def up
add_column :users, :slug, :string
add_index :users, :slug, unique: true
#User.where(id: ids).find_each(&:save)
# Нее будет работать если пользователей несколько десятков тысяч, нужно сделать быстрее
User.connection.execute "UPDATE users SET slug = replace(lower(first_name || '-' || last_name), ' ', '-') WHERE slug IS NULL"
# Слепили слаг из имени пользователя. Но. есть проблема - имена могут совпадать
duplicated_slugs = User.select(:slug).group(:slug).having("count(*) > 1").count
# Находим все записи с вовпадающими именами
duplicated_slugs.keys.each do |slug|
ids = User.where(slug: slug).pluck(:id)
# Очищаем слаг для всех дубликатов
User.where(id: ids).update_all(name_profile: nil)
# Для всех таких записей, очевидно что их будет совсем немного, генерируем слаг просто пересохраняя модель
# Механизм генерации слага, в моем случае - gem friendly_id, сам сгенерирует нужное значение
User.where(id: ids).find_each(&:save)
end
end
def down
remove_column :users, :slug
end
end
Как мигрировать данные с помощью ruby
Для миграции данных необязательно строить весь ActiveRecord объект, иногда можно работать просто с хешами.
ActiveRecord::Base.connection.execute ‘SELECT * FROM users’
Вернет объект типа PG::RESULT
. Получить из него данные неудобно, но этот объект обладает всеми свойствами перечеслимого типа, а значит, можно использовать each, map, reduce и т.д.
Другая полезная информация
Как запускать миграцию без транзакции
Давайте снова рассмотрим пример, когда нам нужно отслеживать количество просмотров в статье. Очень простая задача, мы ее ужее решали чуть выше.
def change
add_column :articles, :views, :integer, default: 0, null: false
end
Дело в том, что это будет работать, пока у нас относительно мало статей. Если их несколько деястков миллионов, такая миграция будет выполняться несколько часов и все это время приложение не будет работать нормально. Дело в том, что добавление новой колонки - очень быстрая операция, а вот заполнение миллионов строк нулями - нет. Но, так как все выполняется в одной транзакции, то приложение не будет полноценно работать все это время.
Решение очень простое
сlass MyMigration
disable_ddl_transaction!
def up
add_column :articles, :views, :integer
change_column :articles, :views, :integer, default: 0, null: false
end
def down
remove_column :articles, :views
end
end
Мы запретили выполнение внутри транзакции disable_ddl_transaction!
и потом сделали добавление в 2 шага.
Какие есть полезные rake задачи для работы с миграциями
rake db:create
- Cоздать баду данныхrake db:drop
- Удалить базу данныхrake db:migrate
- Запустить миграцию базыrake db:rollback
- Откатить миграцию данных к предыдущему состояниюrake db:schema:dump
- Построение файла schema.rb на основе текущей структуры базы данныхrake db:schema:load
- Восстановление структуры базы данных из schema.rbrake db:seed
- Запускает скрипт db/seed.rb который по идее должен заполнить базу первоначальными даннымиrake db:setup
- Запустит поочереди create, schema:load, seedrake db:structure:dump
- Сделает дамп структуры в db/structue.sqlrake db:structure:load
- Восстановит базу из db/structue.sql
Как запустить несколько задач одной командой
rake db:setup
эквивалентно rake db:{create,schema:load,seed}
Как откатить миграцию
rake db:rollback
- Откатит последнюю миграцию. Важно - имеется ввиду именно файл миграции, их может быть несколько при выполнении rake db:migrate. Откатится только изменения последнегоrake db:rollback STEP=3
- Откатит последние 3 файлаrake db:migrate:down VERSION=integer
- Откатит все изменения до указанной версии
Список доступных типов в Rails миграции
- :binary
- :boolean
- :date
- :datetime
- :decimal
- :float
- :integer
- :primary_key
- :references
- :string
- :text
- :time
- :timestamp
Если используете PostgreSQL и Rails 4 и выше, то так же можно использовать
- :hstore
- :json
- :array
- :cidr_address
- :ip_address
- :mac_address
Если Вам есть чем дополнить, или Вы нашли ошибку - присылайте мне в комментариях.
Комментарии