Этот сайт не наркоманов. Это сайт программистов.

Добро пожаловать на Пыху!

Логин:
Пароль:
 

Нет прописки? Зарегистрируйся!

Новости

Пыха переехала на новый сервер, ура!

Краснодарское время: 11 Сентябрь, 2010, 12:12:52

Страниц: [1]
Печать
Автор Тема: Killich: Переносим БД с PHP на Rails при помощи Rake  (Прочитано 932 раз)
0 Пользователей и 1 Гость смотрят эту тему.
killich    ↓ 
16 Август, 2009, 01:31:57
НЕ ХУЕТА! ХУЕТА!

Группа: Санитары

Карма: 18
Сообщений: 215

Приветствую, дорогой товариСч! =)
 
Расскажу я тебе не о красивых обнаженных женских телах,
прекрасный вид которых давно утонул в твоем сознании под
давлением воспаленного виртуальностью разума, и даже не о том,
как прекрасно в теплый летний вечер лежать на берегу небольшого озера и
наслаждаться теплом лучей медленно заходящего за горизонт раскаленного шара..
 
..зачем.. ведь этого все равно нет в твоей жизни =)
 
..а расскажу я тебе о том, как потратил несколько часов своей замечательной молодой жизни
на написание rake задачи, организующей перенос данных из одной БД на в другую.
 
И так:
 
Постановка задачи:
 
Имеем:
 
Локальная машина с двумя MySQL БД
 
БД1 => взята из 2х летнего php проекта, который переносится на рельсы
БД2 => Новая БД на рельсах с совершенно другой структурой данных
 
БД1 - вмещает в себе несколько десятков таблиц. БД1 использовалась несколькими экземплярами одного движка.
Таблицы каждого экземпляра имеют уникальный префикс перед именем, который можно считать Логином администратора сайта.
Основное содержимое БД1 - контентные страницы и файлы связанные со страницами.
В БД1 организована кривая древовидная структура - Страницы хранятся в отдельной таблице - Информация о дереве в другой.
 
БД2 - БД под многопользовательскую Rails систему. Обладает таблицей страниц, с привязкой к конкретному пользователю и полями,
обеспечивающими функционал дерева (На основе awesome_nested_set)
 
Основная цель - перенести деревья страниц в новый движок и обеспечить, что информация о файлах,
прикрепленных к страницам не терялась
(ранее была программная отрисовка прикрепленных файлов - мы заменим ее на простой html поскольку
новый движок не предполагает автоматической отрисоки прикрепленных файлов)
 
Начинаем:
 
1. Создаем файл для rake и его основу:
 
Ruby
lib\tasks\import.rake

Ruby
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
 
Ruby
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.
 
У меня в старой базе три таблицы с которыми мне придетсф работать:
 
Ruby
  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

 
Ruby
set_table_name "#{login}_sections"

Функция set_table_name позволила мне изменить имя таблицы связанной с Моделью,
поскольку у меня ИМЯ МОДЕЛИ и ИМЯ ТАБЛИЦЫ БД не совпадали.
Пришлось немного обмануть ожидания ActiveRecord
 
Внутри класса моя внешняя переменная login интерпретироваться не захотела и я
не став долго думать запихнул код в eval
 
=) ну да да.. бросайте в меня камни =) подумаешь один eval =) а шуму то, шуму =)
 
Итого получаем так:
 
Ruby
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

И так - мы видим список логинов пользователей.
Для каждого пользователя будут переопределяться классы Модели для доступа к нужным таблицам старой БД.
 
Видите?! Видите?!
 
Ruby
sections= OldSiteSection.find(:all,  :order=>"Prev_Id ASC")

Я уже обращаюсь к старой базе и получаю от туда структуру дерева страниц (там оно было названо sections)
 
По этой структуре sections надо выбрать страницы.
Страницы надо перенести в новую БД и построить из них дерево.
Дерево в новой БД будет строится по новым ID страниц, и не будет опираться на старые ID.
Точнее старые ID будут определять дерево, а по новым ID нужно это дерево уже строить.
 
Что бы привести все это в какое то соответствие  - я делаю ассоциативный массив.
Ключ ассоциативной пары - это старый ID страницы (в старой БД он назывался Prev_Id)
Значение ассоциативной пары - новый ID страницы в новой БД.
 
При создании каждой страницы я буду сохранять во внешнем хеше пары - старый ID => Новый ID
И при необходимости буду вызывать функцию перемещения страницы к предку, если в ассоциативной паре есть нужная информация.
 
На самом деле объяснить на словах довольно трудно - была бы у вас такая задачка - сами бы поняли что к чему.
 
Ruby
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 эквиваленты.
Кроме того, нужно было заменить все пути у ссылок, ведущих на локальные файлы,
в новом движке все файлы хранятся централизовано =)
 
Ruby
  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}/")

А еще я вспомнил, что в старом движке к странице программно прикреплялись файлы.
Я не буду заниматься написанием аналогичного программного функционала - а просто найду
все прикрепленные к странице файлы и в конце страницы сделаю список со ссылками.
 
Для каждой страницы я буду вызывать
 
Ruby
  # Найти файлы если они прикреплены к странице
  files= OldSiteLinkedFiles.find(:all, :conditions => ['Page_Id = ? and Linked = ?', old_id, 1])
  
Page_Id в старом движке означал ID страницы, а Linked - это флаг того, что файл отображается,
при просмотре страницы.
 
Ruby
  content= basic_page.Content.gsub(">", '>').gsub("<", '<').gsub(""", "'")
 
  # если массив найденных файлов не пуст то к содержимому страницы присоединяю
  # HTML список со ссылками на файлы
  (content = content + file_div(files) ) unless files.empty?

Функция file_div(files) должна генерировать HTML
 
Мне не хотелось использовать чистый HTML код и я хотел использовать хелперы из ACTION VIEW
 
Ruby
  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 работать не захотело.
 
Я сделал так:
 
Ruby
  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
от которого и вызвал нужные функции.
 
Уж не знаю на сколько криво я это сделал - но оно работает и это главное =)
Ruby

  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

Ну и собственно получил
 
Ruby
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
« Последнее редактирование: 16 Август, 2009, 01:31:57 от adw0rd » Записан
phpdude    ↓ 
11 Август, 2009, 08:49:06 , спустя
НЕ ХУЕТА! ХУЕТА!

я - ЭМО
Группа: в ухо

Карма: 253
Сообщений: 13788

скоро напишу статью как перенсоить бд с помощью mysqldump
Записан

Маг-Волшебник 20го левела. Снятие порчи, ддос, настройка cron, проклятие серверов конкурентов. php магистр, тролль, клоун
забанен. могу забанить других, пишите в личку
Нас невозможно сбить с пути - нам похую, куда идти :-
adw0rd    ↓ 
11 Август, 2009, 01:06:04 , спустя 4 часа 16 минут 58 секунд
НЕ ХУЕТА! ХУЕТА!

эдво
Группа: в ухо

Карма: 251
Сообщений: 15393


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

Sinkler    ↓ 
11 Август, 2009, 02:17:24 , спустя 1 час 11 минут 20 секунд
НЕ ХУЕТА! ХУЕТА!

Группа: Адекваты

Карма: 31
Сообщений: 2540

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

/home/sinkler/Tree behavior и callbacks в CakePHP 1.3.3
pyha@conference.jabber.ru - эта комната жива, хоть и без бота, мать его...
Страниц: [1]
Печать
 

Перейти в: