Эта статья ставит целью описать решение одной нетривиальной задачи — автоматическая односторонняя синхронизация данных в базах двух проектов средствами Ruby on Rails, гема legacy_migrations и относительно прямых рук.
Имеется нагруженный проект, писавшийся на протяжении 3-х лет в несколько этапов без серьезного рефакторинга, отчего код разбух и используемые технологии ощутимо устарели. Было принято решение переписать проект с нуля на всем новом.
Основная трудность — это синхронизация контента баз данных с возможностью отойти от старой архитектуры БД в новом проекте. Было рассмотрено множество вариантов, но выбранный в конечном итоге позволил создать автоматическую систему синхронизации контента, которая сохраняет id записей, сохраняет временные метки записей, добавляет новые и обновляет уже имеющиеся записи. И при всем при этом не обязывает вас строго копировать схему существующей базы данных.
В процессе поиска полуготового решения был найден (не без помощи группы ror2ru) удобный gem, написанный еще во времена Rails 2.3.x, который позволяет переносить содержимое произвольных атрибутов одной модели в другую, с возможностью выполнить произвольные операции над ними. Это было хорошее начало, но в процессе тестирования обнаружились существенные недостатки:
Для устранения выявленных недостатков я сделал этот форк. Чтобы лучше понять чего же такого я там понаписал можете проследовать сюда.
Надо отметить, что способ переноса данных через AR модели имеет один существенный недостаток — низкая производительность, однако в моем случае база оказалась относительно небольшой (около 400 Мб в целом), а сервер достаточно мощным, чтобы не отказываться от этого подхода.
Для начала надо отметить, что мне повезло и при переносе старого проекта (и параллельном апгрейде версии Rails с 2.3.4 до 2.3.12) базы данных обоих проектов оказались на одном сервере — для периодической синхронизации нет ничего лучше.
Для начала необходимо убедиться, что в Gemfile вписаны адаптеры для обеих СУБД:
Для установки legacy_migrations есть два варианта — форк (в который уже внесены необходимые изменения) или оригинальный gem с возможностью собственноручного допиливания (привожу строки из Gemfile для обоих вариантов соответственно):
после чего правки в код можно вносить самостоятельно, а чтобы gem оказался в path надо выполнить вот такую команду в корне вашего приложения:
Суть работы legacy_migrations следующая: в проекте должны существовать модели из которых мы берем данные и модели в которые мы эти записи зеркалим. Таким образом config/database.yml нового проекта будет выглядеть примерно следующим образом:
Где legacy — это конфигурация для базы старого проекта (название можно выбрать произвольно).
Далее следует посмотреть на модели старого проекта и выбрать свободный префикс во избежание дальнейшей путаницы. В моем случае это был префикс «Old». После чего создаем директорию app/models/old и помещаем туда абстрактный класс, от которого будут наследоваться все остальные. Пример app/models/old/old_base.rb:
где аргумент 'legacy' должен соответствовать названию группы настроек для старой базы в config/database.yml. Таким образом все модели, наследуемые от класса OldBase (а не напрямую от ActiveRecord::Base) будут знать, к какой базе необходимо подключиться. Далее приведу пример одной такой модели:
так как наши классы теперь имеют префикс, который изначально не предполагался, необходимо напрямую указывать название таблицы.
Для того, чтобы классы из app/models/old автоматически подгружались необходимо прописать этот путь в config/application.rb вот так:
А далее все довольно просто необходимо создать rake задачу, например вот такую (lib/tasks/legacy.rake):
Все — теперь запуск задачи возможен примерно так:
Подробнее про возможности legacy_migrations стоит читать в этом авторском посте.
Тут возможны варианты, так как rake задача уже есть а уж как ее запускать — дело десятое, однако я хотел бы предложить вариант периодического запуска любых задач для проекта, который мне больше всего понравился.
Есть очень удобный gem — whenever — для целей автоматического запуска заданий для нужд приложения посредством cron, который с легкостью интегрируется в Capistrano и позволяет подстраивать запуск основных вещей (таких как rake задача, runner скрипт или консольная команда) под конкретную production среду и писать собственные типы исполняемых заданий.
Для этого необходимо установить whenever (строка из Gemfile):
и поместить во вновь созданный файл config/schedule.rb примерно следующее:
я переписал определение rake задачи под мою среду: я использую rvm пользовательской установки и ree в качестве интерпретатора ruby (как только Rails 3.1 станут стабильны переключусь на 1.9.2 — пока наблюдаются некоторые проблемы), также я использую bundler, поэтому любой бинарник или скрипт следует запускать через bundle exec.
В Capistrano wheneverize интегрируется так же легко и непринужденно (deploy.rb):
После деплоя можно полюбоваться красивыми и опрятными строками в crontab:
P.S. Надеюсь, что статья будет полезной для людей столкнувшихся с аналогичной проблемой переноса данных из одного проекта в другой.
Андрей Воронков, Evrone.com.
Исходная ситуация
Имеется нагруженный проект, писавшийся на протяжении 3-х лет в несколько этапов без серьезного рефакторинга, отчего код разбух и используемые технологии ощутимо устарели. Было принято решение переписать проект с нуля на всем новом.
Старый проект:
- Rails 2.3.4 (позже обновлен до 2.3.12 + контроль зависимостей через bundler)
- MySQL 5
- Sphinx, Delayed Job, AR sendmailer, interlock + memcached для кеширования и остальное по мелочи
Новый проект:
- Rails 3.1.0.rc5 (на данный момент)
- Postgresql 8.4 (возможно 9 в последствии)
- О мелочах пока еще рано говорить, но предполагается Solr, Redis, resque
Основная трудность — это синхронизация контента баз данных с возможностью отойти от старой архитектуры БД в новом проекте. Было рассмотрено множество вариантов, но выбранный в конечном итоге позволил создать автоматическую систему синхронизации контента, которая сохраняет id записей, сохраняет временные метки записей, добавляет новые и обновляет уже имеющиеся записи. И при всем при этом не обязывает вас строго копировать схему существующей базы данных.
Gem legacy_migrations
В процессе поиска полуготового решения был найден (не без помощи группы ror2ru) удобный gem, написанный еще во времена Rails 2.3.x, который позволяет переносить содержимое произвольных атрибутов одной модели в другую, с возможностью выполнить произвольные операции над ними. Это было хорошее начало, но в процессе тестирования обнаружились существенные недостатки:
- не сохранялись id записей (он укладывал объекты в базу подряд начиная с id=1)
- не сохранялись временные штампы записей (updated_at, created_at)
- при повторной прогонке rake задачи данные в таблице дублировались под новыми id
Для устранения выявленных недостатков я сделал этот форк. Чтобы лучше понять чего же такого я там понаписал можете проследовать сюда.
Надо отметить, что способ переноса данных через AR модели имеет один существенный недостаток — низкая производительность, однако в моем случае база оказалась относительно небольшой (около 400 Мб в целом), а сервер достаточно мощным, чтобы не отказываться от этого подхода.
Процесс переноса
Для начала надо отметить, что мне повезло и при переносе старого проекта (и параллельном апгрейде версии Rails с 2.3.4 до 2.3.12) базы данных обоих проектов оказались на одном сервере — для периодической синхронизации нет ничего лучше.
Установка необходимых гемов
Для начала необходимо убедиться, что в Gemfile вписаны адаптеры для обеих СУБД:
gem 'mysql2'
gem 'pg'
Для установки legacy_migrations есть два варианта — форк (в который уже внесены необходимые изменения) или оригинальный gem с возможностью собственноручного допиливания (привожу строки из Gemfile для обоих вариантов соответственно):
gem 'legacy_migrations', :git => 'git://github.com/Antiarchitect/legacy_migrations.git'
gem 'legacy_migrations', :path => 'vendor/gems/legacy_migrations-0.3.7'
после чего правки в код можно вносить самостоятельно, а чтобы gem оказался в path надо выполнить вот такую команду в корне вашего приложения:
gem unpack legacy_migrations --target vendor/gems
Принципы работы
Суть работы legacy_migrations следующая: в проекте должны существовать модели из которых мы берем данные и модели в которые мы эти записи зеркалим. Таким образом config/database.yml нового проекта будет выглядеть примерно следующим образом:
production:
adapter: postgresql
encoding: utf8
database: newapp_production
username: postgres
password: somecomplicatedpassword
legacy:
adapter: mysql2
encoding: utf8
database: oldapp_production
username: root
password: anothercomplicatedpassword
Где legacy — это конфигурация для базы старого проекта (название можно выбрать произвольно).
Далее следует посмотреть на модели старого проекта и выбрать свободный префикс во избежание дальнейшей путаницы. В моем случае это был префикс «Old». После чего создаем директорию app/models/old и помещаем туда абстрактный класс, от которого будут наследоваться все остальные. Пример app/models/old/old_base.rb:
class OldBase < ActiveRecord::Base
self.abstract_class = true
establish_connection 'legacy'
end
где аргумент 'legacy' должен соответствовать названию группы настроек для старой базы в config/database.yml. Таким образом все модели, наследуемые от класса OldBase (а не напрямую от ActiveRecord::Base) будут знать, к какой базе необходимо подключиться. Далее приведу пример одной такой модели:
class OldNewsDoc < OldBase
set_table_name 'news_docs'
end
так как наши классы теперь имеют префикс, который изначально не предполагался, необходимо напрямую указывать название таблицы.
Для того, чтобы классы из app/models/old автоматически подгружались необходимо прописать этот путь в config/application.rb вот так:
module NewApp
class Application < Rails::Application
...
config.autoload_paths += %W(#{config.root}/app/models/old)
...
end
end
А далее все довольно просто необходимо создать rake задачу, например вот такую (lib/tasks/legacy.rake):
require 'legacy_migrations'
namespace :legacy do
namespace :transfer do
desc 'Transfers News Docs from onru to onru2'
task :news_docs => :environment do
transfer_from OldNewsDoc, :to => NewsDoc do
from :id, :to => :id
from :updated_at, :to => :updated_at
from :created_at, :to => :created_at
from :news_rubric_id, :to => :news_rubric_id
from :title, :to => :title
from :annotation, :to => :annotation
from :text, :to => :text
end
end
end
end
Все — теперь запуск задачи возможен примерно так:
bundle exec rake legacy:transfer:news_docs RAILS_ENV=production
Подробнее про возможности legacy_migrations стоит читать в этом авторском посте.
Автоматизация процесса
Тут возможны варианты, так как rake задача уже есть а уж как ее запускать — дело десятое, однако я хотел бы предложить вариант периодического запуска любых задач для проекта, который мне больше всего понравился.
Gem Whenever
Есть очень удобный gem — whenever — для целей автоматического запуска заданий для нужд приложения посредством cron, который с легкостью интегрируется в Capistrano и позволяет подстраивать запуск основных вещей (таких как rake задача, runner скрипт или консольная команда) под конкретную production среду и писать собственные типы исполняемых заданий.
Для этого необходимо установить whenever (строка из Gemfile):
gem 'whenever', :require => false
из корня приложения запустить команду wheneverize .
и поместить во вновь созданный файл config/schedule.rb примерно следующее:
job_type :rake, "rvm use ree && cd :path && RAILS_ENV=:environment bundle exec rake :task :output"
if environment == 'production'
every :day, :at => '2am' do
rake "legacy:transfer:news_docs"
end
end
я переписал определение rake задачи под мою среду: я использую rvm пользовательской установки и ree в качестве интерпретатора ruby (как только Rails 3.1 станут стабильны переключусь на 1.9.2 — пока наблюдаются некоторые проблемы), также я использую bundler, поэтому любой бинарник или скрипт следует запускать через bundle exec.
В Capistrano wheneverize интегрируется так же легко и непринужденно (deploy.rb):
require 'whenever/capistrano'
...
set :whenever_command, "bundle exec whenever" # это обязательно, если используется bundler - иначе он не найдет команду whenever
После деплоя можно полюбоваться красивыми и опрятными строками в crontab:
crontab -l
P.S. Надеюсь, что статья будет полезной для людей столкнувшихся с аналогичной проблемой переноса данных из одного проекта в другой.
Андрей Воронков, Evrone.com.