データ量が少ない(百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