7年前にRuby 1.8で書かれたサイトをRuby 2.0化する

株式会社wktkの初めての仕事として、2008年2月に開発がスタートした某サイトにおけるRuby 1.8からRuby 2.0への移行案件を受注した。その際行ったことをメモ。

Ruby本体・モジュール 

Debianのパッケージ依存をやめた

当該システムでは、Ruby本体や、DebianのパッケージになっているRubyモジュールはDebianのパッケージを利用し、それ以外のモジュールや、私家製のパッチを当てたモジュールはローカルにtar ballやgemをリポジトリに入れて、シェルスクリプトで導入していた。

基本Debianのモジュールの利用はやめた。ただし、ビルドに必要な各種モジュールと、ruby-buildはDebianのモジュールを利用するようにした。

ruby-buildを使ってRubyをビルドするようにした

「ruby-build -rbenv」で検索するとはかどる。

BundlerとGemfileを使うようにした

モジュールの管理には、BundlerとGemfileを使うようにした。

利用モジュールを変えた

  • tidy => tidy-ext
  • mysql-ruby => mysql2

tidyはメンテナンスされていない。tidy-forkなどのモジュールもあるが、tidy-extを使った。tidy-extにはtidy互換モードがあり、ほとんどそのまま使える。ただし、オプションの値の扱いが違う。

tidyのオプションには、BooleanとAutoBoolの2つのBoolっぽいオプションがある。AutoBoolは実際には3値で、yes/no/autoという値をとるのだ。AutoBoolについて、tidyモジュールではRubyのtrue/falseを指定できるが、tidy-extでは、Rubyのtrue/falseではなく、'auto', ‘yes’, ‘no'などの文字列を渡さなければならない。注意。

mysql2については、生SQL+パラメータbindでクエリを組み立てることができない。id:tagomorisによるブログエントリ「mysql2-cs-bind released!」に紹介されている、mysql2-cs-bindを改造して使っている。

改造ポイントは3つ。

  • 返り値をデフォルトHashではなくArrayとするようにする。
  • 数値を数値としてバインドするようにする。
  • バイナリ文字列をバインドできるようにする。

mysql2は、レコードをHashとして返す。mysql-rubyではレコードをArrayとして返すので、デフォルトの挙動を合わせた。

MySQLで、多くの場合には数値を文字列としてバインドしてもよい。ただし、LIMIT ?, ?をバインドする場合には、数値でないといけない。LIMIT ‘0’, ‘5'ではエラーとなり、LIMIT 0, 5でないといけないのだ。よって、どんな場所であっても、数値は数値としてバインドするようにした。

某サイトの投稿データには、バイナリ情報が含まれることがある。encodingはEncoding:::ASCII_8BITだ。これをそのまま文字列としてバインドされると、エンコーディングが違うとエラーが出る。よって、MySQLの0xaabbccdd…のような16進数でのバイナリ文字列リテラルを使って埋め込むようにした。

diffはこんな感じ。

diff --git a/lib/mysql2-cs-bind.rb b/lib/mysql2-cs-bind.rb
--- a/lib/mysql2-cs-bind.rb
+++ b/lib/mysql2-cs-bind.rb
@@ -8,6 +8,7 @@ class Mysql2::Client
               else
                 {}
               end
+    options[:as] = :array
     if args.size < 1
       query(sql, options)
     else
@@ -33,6 +34,10 @@ class Mysql2::Client
         sql[pos] = 'NULL'
       elsif rawvalue.respond_to?(:strftime)
         sql[pos] = "'" + rawvalue.strftime('%Y-%m-%d %H:%M:%S') + "'"
+      elsif rawvalue.is_a?(Numeric)
+        sql[pos] = rawvalue.to_s
+      elsif rawvalue.is_a?(String) and rawvalue.encoding == Encoding::ASCII_8BIT
+        sql[pos] = '0x' + rawvalue.unpack('C*').map{|b| "%02X" % b}.join('')
       elsif rawvalue.is_a?(Array)
         sql[pos] = rawvalue.map{|v| "'" + Mysql2::Client.escape(v.to_s) + "'" }.join(",")
       else

pull requestを出そうかと思ったのだが、どれも仕様変更で微妙なのでナシ。バイナリだけは有用性あるかもしれないが、unpackはちょっと汚いね。

トランザクション時におけるbegin/revert/commitも、それぞれのクエリを実行するラッパをかました。

lastinsertidの取得方法も変えた。st.insertidはdbh.lastidとなる。

ソースコード

$KCODE撲滅

$KCODE = 'u'としているところを、全て置換した。 shebangにある-wオプションも削除した。

カレントディレクトリをモジュール読み込み先としてくれない問題を修正

$:もしくは$LOAD_PATHにカレントディレクトリが含まれなくなっている。

$: << '.'

と追加したり、

require 'application'

require './application'
と修正したりした。

force_encodingをいくつか付与する

POSTのパラメータの一部などはバイナリなので、String#force_encoding('ASCII-8BIT’)を付与した。他にも、外部のC拡張とのやりとりにおいて(ex. libxml-ruby)、encodingを強制的に指定したりすることがあった。

バイナリを用いた文字境界にマッチしない正規表現を文字境界にマッチする正規表現にする

タイトルだけでは、何を言っているのか全く通じない。よって、以下のコードを見てもらいたい。

以下のように、文字列すべてがひらがなかどうか、カタカナかどうかを調べるメソッドにおいて、文字をバイト列として扱わないように修正した。

   def self.all_hiragana?(str)
     str.gsub!('-', 'ー')
-    not (str =~ /\A(?:\xE3\x81[\x81-\xBF]|\xE3\x82[\x80-\x93]|\xE3\x83\xBC)+\z/
).nil?
+    not (str =~ /\A(?:[\xE3\x81\x81-\xE3\x81\xBF]|[\xE3\x82\x80-\xE3\x82\x93]|\xE3\x83\xBC)+\z/).nil?
   end

   def self.all_katakana?(str)
     str.gsub!('-', 'ー')
-    not (str =~ /\A(?:\xE3\x82[\xA1-\xBF]|\xE3\x83[\x80-\xB6]|\xE3\x83\xBC)+\z/).nil?
+    not (str =~ /\A(?:[\xE3\x82\xA1-\xE3\x82\xBF]|[\xE3\x83\x80-\xE3\x83\xB6]|\xE3\x83\xBC)+\z/).nil?
   end

String#jsize -> String#size変換

日本語としての文字列の長さを返すString#jsizeはなくなっているので、単にString#sizeに変更した。

String#length -> String#bytesize変換

Ruby 1.8では、String#lengthはバイト列としての長さだが、Ruby 1.9以降では文字列長である。

Ruby 1.8.7が出たあとは、String#bytesizeをなるべく使うようにしていたが、何点か漏れがあった。

jcodeとuconvモジュールを廃止した

nkfモジュールはそのまま残した。場合によっては、それもRuby標準のエンコーディング変換に置換してもよいが、未定義のコードポイントや不正なバイト列の場合の挙動が違うので注意。

Integer(nil)の挙動が変わったのを追従

Integer(nil)がnilのとき0を返さなくなった変更に追従。 なぜto_iを使っていなかったかというと、空文字列を変換するときにエラーになって欲しかったから。

irb(main):001:0> Integer(nil)
=> 0
irb(main):001:0> Integer(nil)
TypeError: can't convert nil into Integer
from (irb):1:in `Integer'
from (irb):1
from /www/ruby/bin/irb:12:in `
'

条件文の末尾に付けられるコロンの削除

Ruby 1.8ではifなどの末尾にコロンをつけられたが、1.9以降ではNG。

case xxx
when 'a':
  puts 'ababa'
end

1個だけあったので消した。

まとめ

  • mysql-rubyからmysql2への移行はちょっと面倒。
  • 文字列エンコーディングまわりについては、上記の注意点を踏まえれば、それほど大変ではない。
  • それ以外にも細かい変更点はあるが、大ハマリはしなかった。ただし、細かい変更点はググりにくい。Ruby 1.8とRuby 2.0の両環境を整えて、挙動の違いを逐一確認していったほうがよい。
  • 記事を書いたあとで、クックパッドを Ruby 2.0.0 に対応させた話のエントリを発見してへコんでいる。