みなさんさようなら、@h3_potetoです。
いよいよRails4アップグレードシリーズもラストです。
最後は、Railsの仕様変更によるアプリの改修です。 これから書くことは、移行前からわかっていて対策していたものもあれば、テストをしたときに発覚したものもあります。 もっとひどいのはリリースした後に発覚したものもあります。 なんて悲しいことなんだ……。
ActiveRecord周り
やはり一番壊れそうなところですから、予想通り壊れましたね……。
絞り込み結果がArrayではなくなる
Rails4では、has_many
関連の中身が ActiveRecord::Associations::CollectionProxy
で返ってきます。 かつては has_many
関連も Array
として扱えていたため、Array
のメソッドを呼んでいたりする場所がいくつかありました。
reject!
とかね!
これらのメソッドが undefined method
になってしまいます。
配列で欲しい場合は、to_a
するとか、target
メソッドを呼ぶと配列がもらえるので、それを使って回避できます。
default_scopeの罠
悪名高きRailsの default_scope
ですが、Rails4へのアップグレードでも名前を聞くとは……。
Rails3の場合、joinsやincludesしたときに、default_scope
が反映されません。
しかし、Rails4になると、joinsやincludesのときにも default_scope
が反映されます。
これ、default_scope
に限った話だけではなくて、scope
を使っていた他のクエリも、結果が変わるものがいくつもありました。
たとえば、こんなモデルが定義されていたとします。
class Poteto < ActiveRecord::Base has_many :poteto_categories end
class PotetoCategory < ActiveRecord::Base default_scope -> { where(remove_flag: false) } end
このときに、
Poteto.joins(:poteto_categories).where(hoge: "fuga").to_sql
みたいなことをしてみると、結果が変わります。
Rails3
SELECT `potetos`.* FROM `potetos` INNER JOIN `poteto_categories` ON `poteto_categories`.`id` = `potetos`.`category_id` WHERE `potetos`.`hoge` = 'fuga'
Rails4
SELECT `potetos`.* FROM `potetos` INNER JOIN `poteto_categories` ON `poteto_categories`.`id` = `potetos`.`category_id` AND `poteto_categories`.`removed_flag` = 0 WHERE `potetos`.`hoge` = 'fuga'
今まで効いていなかった scope
が効くようになったってことは、実はRails4になって、むしろ正しく動くようになったってことです。
今までがダメダメだったということが判明して悲しみ……。
serializeのタイミングが変更になっている
あまり褒められたことではありませんが、ActiveRecordのserializeを使って、JSONとかYAMLをDBに保存している場所がありました。 そして、この部分のバリデーションが落ちるようになりました。
Rails3では、serialize(dumpメソッドの呼出)は値の保存時に行われていました。そのため、バリデーションに入ったときには、値はserialize前の値をチェックしていました。
しかし、Rails4.2より、serializeは値を代入したタイミングで行われるようになり、そのため、バリデーション時には既にserialize済みのデータチェックになってしまいます。
Controller周り
paramsのクラスが変更されている
controllerでパラメータを取得したとき、普段は何も意識せずにハッシュとして扱っています。 実はこれ、Rails3では、 ActiveSupport::HashWithIndifferentAccess
というクラスでした。
しかし、Rails4になるとこれが、ActionController::Parameters
という、ActiveSupport::HashWithIndifferentAccess
のサブクラスになります(StrongParameterが入るにあたって追加されています)。
Class: ActionController::Parameters — Documentation for rails (4.1.7)
普通にハッシュとしてアクセスしている限りは何も困ることはないはずです。
ただ、パラメータをDBに保存していたり、パラメータをキャッシュに保存していたりすると話は違ってきます。 特に、今回のように、Rails3とRails4の共存期間がある場合 には、双方のデータに互換性がある必要があります。
この場合で言うと、Rails4になって新しいクラスができているので、Rails4で保存したパラメータのデータはRails3側からは読み込めなくなります。
そもそも、パラメータをそのままDBに保存するとか、そのままキャッシュに保存するとか、かなりよろしくない仕組みであることは間違いないんですが……。
とりあえずこの互換を保つだけなら、ActionController::Parameters
は ActiveSupport::HashWithIndifferentAccess
のサブクラスなので、ActiveSupport::HashWithIndifferentAccess
に変換してやれば問題なさそうです。 変換したパラメータに関しては、StrongParameter系のメソッドが使えなくなってしまうので、完全にRails4移行して、StrongParameterに書き換えるときまでには、ActionController::Parameters
にしておく必要はありますが……。
params[:poteto].to_h.with_indifferent_access
とかしてやれば、変換できます。
いや、やはりrubyのオブジェクトをそのままDBに保存するとか、memcachedに保存しておくとか、非常に良くないと思うんですよね。
DB周り
まさかActiveRecordより深いところで困るとは……。
Rails4になると、DBとの接続がstrict modeに強制されます。
Rails3時代のCrowdWorksは、かなりゆるゆるにsql_modeを設定していたため、strict modeによるエラーが出ました。
もちろん、sql_modeを別のものに指定することもできます。
ただ、本来であるならエラーになるべきであるものを、握りつぶしている事自体がおかしい。 ならばこれを期にstrict modeにしよう。 というわけで、strict modeは維持して、エラーを潰すことにしました。
Data too longって怒られる
varchar(255)のカラムにやたら長い文字列が入っているような場合があります。 一般的にはバリデーションで弾くべきですが、バリデーションがない!
そのため、DBに300文字くらいを入れようとして、エラーになっていました。 場所にもよりますが、ユーザが入力するような場所の場合には、バリデーションを入れましょう。
Incorrect string valueって怒られる
たいていの場合は絵文字で起こっていました。 DBのテーブルがutf8で定義されている場合に、4バイト文字である絵文字を入れようとするとエラーになります。
今までは、悲しいことにエラーを握りつぶして、絵文字以降の文字列を切り捨てて保存していました。
できれば絵文字も保存できるように、utf8mb4にしたいところですが、これはテーブルによってはかなり難しい……。インデックスのサイズが変更になるので、インデックスを貼っていたりすると一筋縄ではいかない。
もちろんutf8mb4になっている方が、今後嬉しいことが多い予感はしますが、それは今回の対応範囲ではなさそうな大きさです。
なので、ここも一応バリデーションで逃げています。
まとめ
一般的な事象ではまったのはこのくらいですが、これ以外にも、モンキーパッチを作っていたりした部分は盛大にコケたりしていました。 たいてい一番悲しいのはここで、特にActiveRecordの実装に大きく依存しているモンキーパッチを作っていると、直すのはかなり辛くなってきます。
解決策を自分たちで考えなきゃいけない上に、そういうものに限ってテストが書きにくかったり、そもそもテストが無かったり(!!)、絶望的です。
今回はそういったモンキーパッチをかなり消しました。 機能的にまだまだ使わない、長大な機能を提供していたり、そもそも現在ではgemで解決できたり、そういったものの大部分は削除する方向になりました。
おかげでRails4アップグレードによって、ソースが少し綺麗になりました。
ただ、それでも消せない機能は残ってしまいます。だから、テストだけは、独自拡張のテストだけは絶対に書いておきたい。
We're Hiring!
期せずして、アップグレードついでに掃除までやりましたが……。 アップグレードを考えると、こういうことをやっちゃいけないなーというものが結構上がったと思います。
クラウドソーシングのクラウドワークス では、こんなものたちを一緒に掃除をしてくれるエンジニアを募集しています。次回Rails5に上げるときはこんな悲しい思いをしなくて済むといいな!!