MySQL4.1でlatin1なテーブルに格納された日本語データのサルベージ

全回のRails勉強会で相談したネタですが、以前作成したDBでlatin1のテーブルにEUC-JPの日本語を格納してしまい、ダンプツールなどでうまく読み出せなくなってしまっていました。*1
とりあえずRailsからはbinaryで読みだし、日本語として表示できていました。でも、やっぱりUTF-8のほうが楽だよね、ということでMySQL5.0+UTF-8のテーブルに移行するため、データをダンプした際にこの過去のミスがもとでハマった、と。

Rails勉強会の場では「ActiveRecord経由で読み出せば救えるんじゃない?」というアドバイスをいただきまして、その結果うまく救出できたのでまとめをば。

与件

改めて状況をまとめますと、以下の感じでした。

  • MySQL4.1で文字コードlatin1のテーブルにEUC-JPの日本語を詰めてしまっていた。
    • 上記は、binaryで読み出せば正常に表示できていた。
  • MySQL5.0 + UTF-8 なテーブルに移行したい。
  • mysqldump で読み出すと、こちらの問題によるものなのか、ダンプした時点で文字化け。
    • default-character-setを設定しても別の化け方をするばかり。この辺は環境依存だったのかもしれません。

対処内容

ActiveRecord経由でつないでstringなフィールドをkconvでString#toutf8した上でYAMLに保存。ロード時も同じくActiveRecordのオブジェクトを読み込んで保存、と。
ActiveRecordでつなぐあたりはid:secondlifeさんのiar.rbを流用させてもらいました。最下部のIrb.startをコメントアウトしオプションに追加したり、オプションのパース結果をグローバル変数にしたり。

ダンプ側コード

ご参考まで。

# dump_dirとyaml_file_for(table)はそれぞれダンプ先ディレクトリと
# ダンプするテーブル名.ymlなファイルを返すユーティリティメソッド

def dump(opts = {})
  opts = { :excludes => ['schema_info'] }.merge(opts)
  Dir.mkdir dump_dir unless File.directory? dump_dir

  show_tables.each do |table|
    next if opts[:excludes].to_a.include?(table)
    table_class = self.class.const_get(table.classify)
    instances = table_class.find(:all).map do |obj|
      block_given? ?  (yield obj) : obj
    end
    File.open(yaml_file_for(table),"wb") do |dump_file|
      YAML.dump(instances, dump_file)
    end
  end
end

if __FILE__ == $0
  dump do |obj|
    obj.attributes.each do |k,v|
      obj.__send__("#{k}=".to_sym, v.toutf8) if v.is_a? String
    end
    obj
  end
end
ロード時の注意点

ロードするときは、以下で出力したymlファイルを読みこみ、保存しています。ちなみにYAMLからロードしたARオブジェクトのsaveメソッドを呼んでも保存されませんので、YAMLから読み込んだオブジェクトごとに以下のような処理が必要になります。
これは前述のiar.rbで手動でやりました。

  obj = Model.new
  obj.update_attributes(obj_loaded_from_yaml.attributes)
  obj.save!

おまけ

ニーズがあれば*2ロード分も含めてちゃんとスクリプト化して、非対話にできるようにまとめようかなぁ、と思ってます。

*1:我ながらなんでlatin1のまま日本語を入れるなどという暴挙に出たのか不明ですが

*2:もしくはもう一度同じようなことをやる羽目になったら