Ruby on Rails + legacy_migrations: односторонняя синхронизация данных между двумя проектами

    Эта статья ставит целью описать решение одной нетривиальной задачи — автоматическая односторонняя синхронизация данных в базах двух проектов средствами Ruby on Rails, гема legacy_migrations и относительно прямых рук.

    Исходная ситуация


    Имеется нагруженный проект, писавшийся на протяжении 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
    Хочу заострить внимание на том, зачем сохранять id элементов, есть следующий способ — создать атрибут наподобие old_id и по нему сделать перепривязку на новые id. Но задача стоит не просто в создании нового проекта, а в замещении старого проекта новым, а из этого вытекает как минимум идентичность всех url'ов. Чтобы понять насколько это важно, достаточно поймать на улице специалиста по SEO и рассказать о том, что вы хотите поменять урлы на работающем проекте. Должна последовать однозначная реакция, которая может проявляться в разных формах — от обморока до психоза :)
    Для устранения выявленных недостатков я сделал этот форк. Чтобы лучше понять чего же такого я там понаписал можете проследовать сюда.
    Надо отметить, что способ переноса данных через 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.
    • +27
    • 3,4k
    • 9
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 9

      +1
      paperclip => carrierwave
      было бы интересно
        0
        Дело в том, что ситуация гораздо плачевнее и на старом проекте в некоторых местах используется это. Посмотрите на год последнего коммита и посочувствуйте.
          0
          OldNewsDoc.each do |old|
            if old.image
              new = NewsDoc.find old.id
              new.image = open(old.image.path)
              new.save
            end
          end
          


          Ну и rescue воткнуть на всякий
          0
          Интересно, а нельзя было сделать проще? Т.е. плавно двигаться в будущее. Зачем начинать именно новый проект и перетаскивать туда всё? Почему нельзя было обновить rails до последней 2.3.x версии, починить все ворнинги, потом поменять структуру базы миграциями так как надо, заменить mysql на postgres (если так охота) и мигрировать на rails 3?
            0
            Да вы прям фантаст! Начнем с того, что изначально проект писался на rails 1.2 и некоторая его часть сейчас — это просто мертвый груз. Второе — проект использует части другого полумертвого проекта и вынужден соединяться со сторонней базой — это необходимо устранить, ну и третье — проект не имел здорового жизненного цикла, поэтому во многом код представляет собой абсолютное месиво.

            P.S. Абсолютно везде — во view, model и controller присутствует русский язык — еще один фактор.
            0
            Почему солр вместо сфинкса выбрали?
              0
              Честно говоря, сначала я польстился на особенность гема sunspot_rails Который для разработки не требует никаких дополнительных установок — все работает из коробки (я так подозреваю, что для продакшн придется ставить полноценный Solr). Плюс у Solr есть некоторые преимущества перед Sphinx одно из которых лучшая реализация faceted search, который упрощает жизнь при необходимости продвинутого поиска на проекте.
                +1
                А чем она лучше сфинксовой-то на Ваш взгляд?
                  0
                  Реализация фасеточного поиска

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

            Самое читаемое