ФорумПрограммированиеБольше языковRuby → Killich: Переносим БД с PHP на Rails при помощи Rake

Killich: Переносим БД с PHP на Rails при помощи Rake

  • killich

    Сообщения: 270 Репутация: N Группа: Адекваты

    Spritz Авг. 16, 2009, 1:31 д.п., спустя 4 дня 11 часов 14 минут

    Приветствую, дорогой товариСч! =)

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

    ..зачем.. ведь этого все равно нет в твоей жизни =)

    ..а расскажу я тебе о том, как потратил несколько часов своей замечательной молодой жизни
    на написание rake задачи, организующей перенос данных из одной БД на в другую.

    И так:

    Постановка задачи:

    Имеем:

    Локальная машина с двумя MySQL БД

    БД1 => взята из 2х летнего php проекта, который переносится на рельсы
    БД2 => Новая БД на рельсах с совершенно другой структурой данных

    БД1 - вмещает в себе несколько десятков таблиц. БД1 использовалась несколькими экземплярами одного движка.
    Таблицы каждого экземпляра имеют уникальный префикс перед именем, который можно считать Логином администратора сайта.
    Основное содержимое БД1 - контентные страницы и файлы связанные со страницами.
    В БД1 организована кривая древовидная структура - Страницы хранятся в отдельной таблице - Информация о дереве в другой.

    БД2 - БД под многопользовательскую Rails систему. Обладает таблицей страниц, с привязкой к конкретному пользователю и полями,
    обеспечивающими функционал дерева (На основе awesome_nested_set)

    Основная цель - перенести деревья страниц в новый движок и обеспечить, что информация о файлах,
    прикрепленных к страницам не терялась
    (ранее была программная отрисовка прикрепленных файлов - мы заменим ее на простой html поскольку
    новый движок не предполагает автоматической отрисоки прикрепленных файлов)

    Начинаем:

    1. Создаем файл для rake и его основу:

    lib\tasks\import.rake


    namespace :db do
    namespace :import do
    # rake db:import:start
    desc 'import data form OldSite'
    task :start => :environment do
    #
    #
    #
    end# db:import:start
    end# db:import
    end#:db


    У нас должно быть два соединения - одно с нашей текущей базой,
    другое со старой базой.

    Соединение с новой базой у нас будет создаваться по умолчанию - тут вопросов нет.
    Соединение с другой базой мы организуем через промежуточный класс порожденный от ActiveRecord::Base

    class OldSiteConnect < ActiveRecord::Base
    establish_connection(
    :adapter => "mysql",
    :host => "localhost",
    :username => "root",
    :password => "",
    :database => "OldSite",
    :encoding => "utf8"
    )
    end


    Все порожденные классы от OldSiteConnect будут с одной стороны ActiveRecord::Base
    а, с другой стороны будут иметь отличное от стандартного соединение.

    Обратите внимение старая база у меня в cp1251
    опция :encoding => "utf8" позволила мне не думать о переконвертации текста в utf8.

    У меня в старой базе три таблицы с которыми мне придетсф работать:

      class OldSiteSection < OldSiteConnect
    set_table_name "#{login}_sections"
    end
    class OldSitePage < OldSiteConnect
    set_table_name "#{login}_pages"
    end
    class OldSiteLinkedFiles < OldSiteConnect
    set_table_name "#{login}_linked_files"
    end



    set_table_name "#{login}_sections"


    Функция set_table_name позволила мне изменить имя таблицы связанной с Моделью,
    поскольку у меня ИМЯ МОДЕЛИ и ИМЯ ТАБЛИЦЫ БД не совпадали.
    Пришлось немного обмануть ожидания ActiveRecord

    Внутри класса моя внешняя переменная login интерпретироваться не захотела и я
    не став долго думать запихнул код в eval

    =) ну да да.. бросайте в меня камни =) подумаешь один eval =) а шуму то, шуму =)

    Итого получаем так:

    namespace :db do
    namespace :import do
    # rake db:import:start
    desc 'import data form OldSite'
    task :start => :environment do
    class OldSiteConnect < ActiveRecord::Base
    establish_connection(
    :adapter => "mysql",
    :host => "localhost",
    :username => "root",
    :password => "",
    :database => "OldSite",
    :encoding => "utf8"
    )
    end

    logins= %w{ town1 town2 town3 town4 town5 }

    logins.each do |login|
    user= User.find_by_login(login)

    eval("
    class OldSiteSection < OldSiteConnect
    set_table_name '#{login}_sections'
    end
    class OldSitePage < OldSiteConnect
    set_table_name '#{login}_pages'
    end
    class OldSiteLinkedFiles < OldSiteConnect
    set_table_name '#{login}_linked_files'
    end
    ")

    sections= OldSiteSection.find(:all, :order=>"Prev_Id ASC")
    end# logins.each do |login|

    end# db:import:start
    end# db:import
    end#:db


    И так - мы видим список логинов пользователей.
    Для каждого пользователя будут переопределяться классы Модели для доступа к нужным таблицам старой БД.

    Видите?! Видите?!

    sections= OldSiteSection.find(:all,  :order=>"Prev_Id ASC")


    Я уже обращаюсь к старой базе и получаю от туда структуру дерева страниц (там оно было названо sections)

    По этой структуре sections надо выбрать страницы.
    Страницы надо перенести в новую БД и построить из них дерево.
    Дерево в новой БД будет строится по новым ID страниц, и не будет опираться на старые ID.
    Точнее старые ID будут определять дерево, а по новым ID нужно это дерево уже строить.

    Что бы привести все это в какое то соответствие - я делаю ассоциативный массив.
    Ключ ассоциативной пары - это старый ID страницы (в старой БД он назывался Prev_Id)
    Значение ассоциативной пары - новый ID страницы в новой БД.

    При создании каждой страницы я буду сохранять во внешнем хеше пары - старый ID => Новый ID
    И при необходимости буду вызывать функцию перемещения страницы к предку, если в ассоциативной паре есть нужная информация.

    На самом деле объяснить на словах довольно трудно - была бы у вас такая задачка - сами бы поняли что к чему.

    namespace :db do
    namespace :import do

    # rake db:import:start
    desc 'import data form OldSite'
    task :start => :environment do
    class OldSiteConnect < ActiveRecord::Base
    establish_connection(
    :adapter => "mysql",
    :host => "localhost",
    :username => "root",
    :password => "",
    :database => "OldSite",
    :encoding => "utf8"
    )
    end

    logins= %w{ town1 town2 town3 town4 town5 }

    logins.each do |login|
    user= User.find_by_login(login)

    eval("
    class OldSiteSection < OldSiteConnect
    set_table_name '#{login}_sections'
    end
    class OldSitePage < OldSiteConnect
    set_table_name '#{login}_pages'
    end
    class OldSiteLinkedFiles < OldSiteConnect
    set_table_name '#{login}_linked_files'
    end
    ")

    # перебираем все разделы (фактически это дерево)
    sections= OldSiteSection.find(:all, :order=>"Prev_Id ASC")

    # Хеш для соответствия старого и нового id
    ids_set= Hash.new

    sections.each do |s|
    # Старый id страницы
    old_id= s.Page_Id
    # Старая страница
    basic_page= OldSitePage.find(old_id)

    title= basic_page.Description
    content= basic_page.Content

    page= Page.new( :user_id=>user.id,
    :title=>title,
    :content=>content
    )
    page.save

    new_id= page.id

    # Если в спискt родителей имеется такой id, то страницу нужно переместить к родителю
    page.move_to_child_of(Page.find(ids_set[s.Prev_Id])) if ids_set[s.Prev_Id]

    # Добавить в список соответствий id
    ids_set[old_id] = new_id
    end# sections.each do |s|

    end# logins.each do |login|
    end# db:import:start
    end# db:import
    end#:db


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

      title= basic_page.Description.gsub(">", '>').gsub("<", '<').gsub(""", "'")

    content= basic_page.Content.gsub(">", '>').gsub("<", '<').gsub(""", "'")

    content= content.gsub("./files/common/", "/uploads/files/#{login}/")
    content= content.gsub("./files/pages/", "/uploads/files/#{login}/")

    content= content.gsub("./files/#{login}/common/", "/uploads/files/#{login}/")
    content= content.gsub("./files/#{login}/pages/", "/uploads/files/#{login}/")


    А еще я вспомнил, что в старом движке к странице программно прикреплялись файлы.
    Я не буду заниматься написанием аналогичного программного функционала - а просто найду
    все прикрепленные к странице файлы и в конце страницы сделаю список со ссылками.

    Для каждой страницы я буду вызывать

      # Найти файлы если они прикреплены к странице
    files= OldSiteLinkedFiles.find(:all, :conditions => ['Page_Id = ? and Linked = ?', old_id, 1])


    Page_Id в старом движке означал ID страницы, а Linked - это флаг того, что файл отображается,
    при просмотре страницы.

      content= basic_page.Content.gsub(">", '>').gsub("<", '<').gsub(""", "'")

    # если массив найденных файлов не пуст то к содержимому страницы присоединяю
    # HTML список со ссылками на файлы
    (content = content + file_div(files) ) unless files.empty?


    Функция file_div(files) должна генерировать HTML

    Мне не хотелось использовать чистый HTML код и я хотел использовать хелперы из ACTION VIEW

      def file_div(files)
    res= ""
    files.each do |f|
    res<< content_tag(:li, link_to(f.Description, f.Path) )
    end
    res= content_tag(:ul, res, :class=>:linked_files)
    end


    Однако в Rake content_tag и link_to работать не захотело.

    Я сделал так:

      require 'action_view/helpers/tag_helper'
    require 'action_view/helpers/url_helper'
    class Helpers
    include ActionView::Helpers::TagHelper
    include ActionView::Helpers::UrlHelper
    end


    А потом в file_div(files) породил экземпляр help= Helpers.new
    от которого и вызвал нужные функции.

    Уж не знаю на сколько криво я это сделал - но оно работает и это главное =)

    require 'action_view/helpers/tag_helper'
    require 'action_view/helpers/url_helper'
    class Helpers
    include ActionView::Helpers::TagHelper
    include ActionView::Helpers::UrlHelper
    end

    def file_div(files)
    help= Helpers.new
    res= ""
    files.each do |f|
    res<< help.content_tag(:li, help.link_to(f.Description, f.Path) )
    end
    res= help.content_tag(:ul, res, :class=>:linked_files)
    end


    Ну и собственно получил

    namespace :db do
    namespace :import do

    # rake db:import:start
    desc 'import data form OldSite'
    task :start => :environment do
    class OldSiteConnect < ActiveRecord::Base
    establish_connection(
    :adapter => "mysql",
    :host => "localhost",
    :username => "root",
    :password => "",
    :database => "OldSite",
    :encoding => "utf8"
    )
    end

    require 'action_view/helpers/tag_helper'
    require 'action_view/helpers/url_helper'
    class Helpers
    include ActionView::Helpers::TagHelper
    include ActionView::Helpers::UrlHelper
    end

    def file_div(files)
    help= Helpers.new
    res= ""
    files.each do |f|
    res<< help.content_tag(:li, help.link_to(f.Description, f.Path) )
    end
    res= help.content_tag(:ul, res, :class=>:linked_files)
    end

    logins= %w{ town1 town2 town3 town4 town5 }

    logins.each do |login|
    user= User.find_by_login(login)

    eval("
    class OldSiteSection < OldSiteConnect
    set_table_name '#{login}_sections'
    end
    class OldSitePage < OldSiteConnect
    set_table_name '#{login}_pages'
    end
    class OldSiteLinkedFiles < OldSiteConnect
    set_table_name '#{login}_linked_files'
    end
    ")

    #OldSitePage.find:first
    sections= OldSiteSection.find(:all, :order=>"Prev_Id ASC")
    ids_set= Hash.new

    sections.each do |s|
    # Старый id страницы
    old_id= s.Page_Id
    # Старая страница
    basic_page= OldSitePage.find(old_id)

    # Найти файлы если они прикреплены к странице
    files= OldSiteLinkedFiles.find(:all, :conditions => ['Page_Id = ? and Linked = ?', old_id, 1])

    title= basic_page.Description.gsub(">", '>').gsub("<", '<').gsub(""", "'")
    content= basic_page.Content.gsub(">", '>').gsub("<", '<').gsub(""", "'")

    # Добавим список прикрепленных файлов
    (content = content + file_div(files) ) unless files.empty?

    # Поправим все пути
    content= content.gsub("./files/common/", "/uploads/files/#{login}/")
    content= content.gsub("./files/pages/", "/uploads/files/#{login}/")
    content= content.gsub("./files/#{login}/common/", "/uploads/files/#{login}/")
    content= content.gsub("./files/#{login}/pages/", "/uploads/files/#{login}/")

    page= Page.new( :user_id=>user.id,
    :title=>title,
    :content=>content
    )
    page.save
    new_id= page.id
    # Добавить в список соответствий id
    # Если в спискок родителей имеет такой id
    page.move_to_child_of(Page.find(ids_set[s.Prev_Id])) if ids_set[s.Prev_Id]
    ids_set[old_id] = new_id
    end# sections.each do |s|
    end# logins.each do |login|
    end# db:import:start
    end# db:import
    end#:db







    Илья Зыкин aka Killich (Илья Зыкин aka Зайко)

    Учитель школьной информатики, Аспирант, ROR дилетант широкого профиля.

    ruby on rails 2.3.2 winXP, FreeBSD, RSpec, GIT, SVN, MySQL, InstantRails, REE, Passanger, Apache2.2

    Отзывы/предложения/комментарии/вопросы - killich(дикий пёс)mail.ru
    Обучение rails по email,icq, skype и т.д.


    Оригинал статьи для rubyclub.com.ua http://rubyclub.com.ua/messages/show/7312-%D0%9F%D0%B5%D1%80%D0%B5%D0%BD%D0%BE%D1%81%D0%B8%D0%BC-%D0%91%D0%94-%D1%81-PHP-%D0%BD%D0%B0-Rails-%D0%BF%D1%80%D0%B8-%D0%BF%D0%BE%D0%BC%D0%BE%D1%89%D0%B8-Rake
    Рубист с большой буквы Г. Серый кардинал кулинарного блога open-cook.ru
  • phpdude

    Сообщения: 26646 Репутация: N Группа: в ухо

    Spritz Авг. 11, 2009, 8:49 д.п., спустя 7 часов 17 минут 9 секунд

    скоро напишу статью как перенсоить бд с помощью mysqldump
    Сапожник без сапог
  • adw0rd

    Сообщения: 22959 Репутация: N Группа: в ухо

    Spritz Авг. 11, 2009, 1:06 п.п., спустя 4 часа 16 минут 58 секунд


    скоро напишу статью как перенсоить бд с помощью mysqldump
    я уже написал http://adw0rd.ru/2009/mysqldump-and-cheat-sheet/
    Спустя 51 сек.
    killich, ты еще несколько статей по рельсам запостишь? тогда создам раздел по руби и рельсам
    https://smappi.org/ - платформа по созданию API на все случаи жизни
  • Sinkler

    Сообщения: 7958 Репутация: N Группа: в ухо

    Spritz Авг. 11, 2009, 2:17 п.п., спустя 1 час 11 минут 20 секунд

    много красивых букв. не читал, но, killich, +1 =) спасибо

Пожалуйста, авторизуйтесь, чтобы написать комментарий!