データ量が少ない(百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 で呼び出されるメソッドのチェインは以下のとおり
- SerializationHelper::Base.new(helper).load("db/data", "yml")
- YamlDb::Load.load() # SerializationHelper::Load.load()をそのまま継承
- YamlDb::Load.load_documents() # lib/yaml_db.rbで定義
- YamlDb::Load.load_table() # SerializationHelper::Load.load_table()をそのまま継承
- YamlDb::Load.truncate_table() # SerializationHelper::Load.truncate_table() をそのまま継承
- YamlDb::Load.load_records() # SerializationHelper::Load.load_records() をそのまま継承
YamlDb::Load.load_documents()の働き
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ファイルを読み込んだハッシュ変数の値をいじくれば、スキーマ変更前と後のデータベースでデータの移行を行うことができる。
手順
- RAILS_ROOT/lib/tasks に移行用タスクファイルを作成
- RAILS_ROOT/lib/removal_yaml_db を用意し、そこに removal_yaml_db.rb を置く
- RAILS_ROOT/config/application.rb で autoloadを有効にする→参考:INOHILOG: Rails3でautoloadはデフォルトで無効になっている
- 現在のデータベースの中見をダンプ「rake db:data:load」
- db/data.yml をコピー「cp db/data.yml db/data.yml.org」
- 新規スキーマ追加「rails generate migration Addコラム名Toテーブル名 追加コラム」等
- マイグレーション「rake db:migration」
- 新しいデータベースにデータ移行「rake db:data:removal」
- 確認のため新しいデータベースをダンプ「rake db:data:load」
- 確認「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