пятница, 3 апреля 2015 г.

Пример оптимизации миграции в Ruby on Rails

Очень часто при разработке веб-приложения возникает необходимость внести изменения в структуру базы данных: добавить колонку, изменить ее тип, перенести значения из одной колонки в другую и т.п.
Для осуществления этих задач в веб-фреймворке Ruby on Rails существуют миграции.
$ rails g migration add_invoice_number_to_statements invoice_number:string{20}

Эта команда, выполненная в консоли, создаст файл миграции:
db/migrate/20150403113019_add_invoice_number_to_statements.rb
class AddInvoiceNumberToStatements < ActiveRecord::Migration
  def change
    add_column :statements, :invoice_number, :string, :limit => 20
  end
end

Предположим, что нам нужно, в колонку invoice_number еще поместить данные из другой колонки - invoice_id, имеющей тип int. Миграция может выглядеть так:
class AddInvoiceNumberToStatements < ActiveRecord::Migration
  def up
    add_column :statements, :invoice_number, :string, :limit => 20
    
    Statement.find_each do |statement|
      statement.update_column(:invoice_number, statement.invoice_id) if statement.invoice_id
    end
  end

  def down
    remove_column :statements, :invoice_number
  end
end

Вроде мы молодцы, в методе up написали условие if, благодаря которому не будет запроса на обновление поля invoice_number, если в поле invoice_id ничего нет, уменьшает общее число запросов к БД. Кроме того, мы использовали метод find_each, который загружает Statement пачками по 1000 штук, материализует их в объекты и обрабатывает. Это позволит нашей миграции не затратить много оперативной памяти. Какие мы молодцы!
Однако, это не так. Если в таблице statements 50 000 записей, то данная миграция приведет к 50 000 запросам UPDATE.

Иногда полезно миграцию писать на чистом добром SQL:
class AddInvoiceNumberToStatementsAndMigrateData < ActiveRecord::Migration
  def up
    add_column :statements, :invoice_number, :string, :limit => 20

    execute <<-SQL
      COMMENT ON COLUMN statements.invoice_number IS 'Номер счет-фактуры';
      UPDATE statements SET invoice_number = invoice_id::text;
    SQL
  end

  def down
    remove_column :statements, :invoice_number
  end
end

А теперь мы молодцы - миграция отработает молниеносно. А создание комментария к полю является актом творения добра и еще одним поводом для гордости.

Комментариев нет:

Отправить комментарий