yaml_dbを用いたデータベースのスキーマ変更を行ったときのデータ移行

データ量が少ない(百MBぐらいまで)場合。開発をつづけるうちに公開版と開発版でデータベースのスキーマが違ってしまったとき、公開版のデータを消すことなく、新しいスキーマになったデータベースに移行したいことがあった。SQLをいじればよいのだけれども、自信がないのでデータベースの中見をYAML形式でダンプできるyaml_dbスクリプトをちょっと変更して対応することにした。

環境

yaml_dbの動き

以下のコマンドを打つとconfig/database.yml で指定されたデータベースの中見をYAML形式で db/data.yml として吐き出す。

% rake db:data:dump

以下のコマンドを打つとconfig/database.yml で指定されたデータベースに db/data.yml のデータを格納する。

% rake db:data:load

yaml_db-0.2.3/lib/serialization_helper.rb を見ると、最終的にはINSERT文を生SQLで吐き出しているので、yaml_dbによって、idやcreated_at, updated_atなどの値は変更されない。

db:data:load の流れ

rake db:data:load で呼び出されるメソッドのチェインは以下のとおり

  1. SerializationHelper::Base.new(helper).load("db/data", "yml")
  2. YamlDb::Load.load() # SerializationHelper::Load.load()をそのまま継承
  3. YamlDb::Load.load_documents() # lib/yaml_db.rbで定義
  4. YamlDb::Load.load_table() # SerializationHelper::Load.load_table()をそのまま継承
    1. YamlDb::Load.truncate_table() # SerializationHelper::Load.truncate_table() をそのまま継承
    2. YamlDb::Load.load_records() # SerializationHelper::Load.load_records() をそのまま継承

YamlDb::Load.load_documents()の働き

  • このメソッドで、YAMLファイルの解析を行う。
  • 現在のテーブル情報との照らし合わせはない。
  • 各テーブルごとにハッシュの形で、 YamlDb::Load.load_table() に引き渡す

YamlDb::Load.load_table()の働き

  • 渡されたハッシュからコラム名(columns)を取得する。
  • YamlDb::Load.truncate_table() を呼び出し、テーブルの中身を空にする
  • 続いて、YamlDb::Load.load_records() に テーブル名(文字列)、コラム名(配列)、レコード(配列)を引き渡す

YamlDb::Load.load_records()の働き

  • データベースにアクセスし、現テーブルと引数として渡されたコラム名を照らし合わせる(一致しない場合にどうなるかは読み取れなかった)。
  • 引数として渡されたコラム名をサニタイズし、文字列にする(INSERT用)
  • テーブル名をクォーティングする
  • 1レコードごとに
    • 現在のテーブルに存在するコラム名とレコードの値を順序対にして文字列にする(INSERT用)
    • SQLでINSERTする。このため id, updated_at, created_at もそのまま格納される

なので、コラム名の変更や、コラム間の値の移動などを行う場合は、YamlDb::Load.load_documents()において、YamlDb::Load.load_table()が呼び出される前に、YAMLファイルを読み込んだハッシュ変数の値をいじくれば、スキーマ変更前と後のデータベースでデータの移行を行うことができる。

手順

  1. RAILS_ROOT/lib/tasks に移行用タスクファイルを作成
  2. RAILS_ROOT/lib/removal_yaml_db を用意し、そこに removal_yaml_db.rb を置く
  3. RAILS_ROOT/config/application.rb で autoloadを有効にする→参考:INOHILOG: Rails3でautoloadはデフォルトで無効になっている
  4. 現在のデータベースの中見をダンプ「rake db:data:load」
  5. db/data.yml をコピー「cp db/data.yml db/data.yml.org」
  6. 新規スキーマ追加「rails generate migration Addコラム名Toテーブル名 追加コラム」等
  7. マイグレーション「rake db:migration」
  8. 新しいデータベースにデータ移行「rake db:data:removal」
  9. 確認のため新しいデータベースをダンプ「rake db:data:load」
  10. 確認「diff db/data.yml.org db/data.yml」

移行用タスクファイル

yaml_db-0.2.3/lib/tasks/yaml_db_tasks.rake をコピーし、RAILS_ROOT/lib/tasks/removal_yaml_db_tasks.rake とした。編集内容は以下のとおり。

namespace :db do
  namespace :data do
    def db_dump_data_file (extension = "yml")
      "#{dump_dir}/data.#{extension}"
    end

    def dump_dir(dir = "")
      "#{Rails.root}/db#{dir}"
    end

    desc "Change contents of old db with contents of new db, and store those into new db"
    task :remove => :environment do
      format_class = ENV['class'] || "RemovalYamlDb::Helper"
      helper = format_class.constantize
      SerializationHelper::Base.new(helper).load(db_dump_data_file helper.extension)
    end

  end
end

移行スクリプト

yaml_db-0.2.3/lib/yaml_db.rb をほぼそのままに RAILS_ROOT/lib/removal_yaml_db/removal_yaml_db.rb を作成する。self.load_documents() が、load_table()を呼び出す前に、YAMLファイルのデータをハッシュ化した ydoc の中見を弄りたいので、load_table()呼び出しの直前に、removal_data()という自作メソッドを実行する。

ydocの構造は以下のとおり。各テーブルは、値がカラム名の一覧である配列 columns と値が各レコードである配列 records で構成されている。各レコードは配列であり、レコードの値の順番は columnsの順番と一致している。

ydoc = 
{ "table_name1" => 
  {"columns" => [colum1, column2, ..., columnN],
   "records" => [record1Array, record2Array, ...]},
   "table_name2" => 
  {"columns" => [colum1, column2, ..., columnN],
   "records" => [record1Array, record2Array, ...]},
   }  

removal_yaml_db.rb はこんな感じになる(9割 yaml_db-0.2.3/lib/yaml_db.rb のコピー)。

require 'rubygems'
require 'yaml'
require 'active_record'
require 'serialization_helper'
require 'yaml_db'
require 'active_support/core_ext/kernel/reporting'
require 'rails/railtie'

module RemovalYamlDb
  module Helper
    def self.loader
      RemovalYamlDb::Load
    end

    def self.dumper
      RemovalYamlDb::Dump
    end

    def self.extension
      "yml"
    end
  end


  module Utils
    def self.chunk_records(records)
      yaml = [ records ].to_yaml
      yaml.sub!(/---\s\n|---\n/, '')
      yaml.sub!('- - -', '  - -')
      yaml
    end
  end

  class Dump < YamlDb::Dump
  end

  class Load < YamlDb::Load
    def self.load_documents(io, truncate = true)
      YAML.load_documents(io) do |ydoc|
        ydoc.keys.each do |table_name|
          next if ydoc[table_name].nil?
          removal_data(table_name, ydoc)
          load_table(table_name, ydoc[table_name], truncate)
        end
      end
    end
    
    # Describe a program to move old db data to new db data.
    def self.removal_data(table_name, ydoc)
     # このメソッド内でydocをいじる。
    end
  end

end