クラウドワークス エンジニアブログ

日本最大級のクラウドソーシング「クラウドワークス」の開発の裏側をお届けするエンジニアブログ

Rails4アップグレードのつまずきどころ

みなさんさようなら、@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 を使っていた他のクエリも、結果が変わるものがいくつもありました。

techlife.cookpad.com

github.com

たとえば、こんなモデルが定義されていたとします。

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::ParametersActiveSupport::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を別のものに指定することもできます。

qiita.com

ただ、本来であるならエラーになるべきであるものを、握りつぶしている事自体がおかしい。 ならばこれを期にstrict modeにしよう。 というわけで、strict modeは維持して、エラーを潰すことにしました。

Data too longって怒られる

varchar(255)のカラムにやたら長い文字列が入っているような場合があります。 一般的にはバリデーションで弾くべきですが、バリデーションがない!

そのため、DBに300文字くらいを入れようとして、エラーになっていました。 場所にもよりますが、ユーザが入力するような場所の場合には、バリデーションを入れましょう。

Incorrect string valueって怒られる

たいていの場合は絵文字で起こっていました。 DBのテーブルがutf8で定義されている場合に、4バイト文字である絵文字を入れようとするとエラーになります。

今までは、悲しいことにエラーを握りつぶして、絵文字以降の文字列を切り捨てて保存していました。

できれば絵文字も保存できるように、utf8mb4にしたいところですが、これはテーブルによってはかなり難しい……。インデックスのサイズが変更になるので、インデックスを貼っていたりすると一筋縄ではいかない。

qiita.com

もちろんutf8mb4になっている方が、今後嬉しいことが多い予感はしますが、それは今回の対応範囲ではなさそうな大きさです。

なので、ここも一応バリデーションで逃げています。

まとめ

一般的な事象ではまったのはこのくらいですが、これ以外にも、モンキーパッチを作っていたりした部分は盛大にコケたりしていました。 たいてい一番悲しいのはここで、特にActiveRecordの実装に大きく依存しているモンキーパッチを作っていると、直すのはかなり辛くなってきます。

解決策を自分たちで考えなきゃいけない上に、そういうものに限ってテストが書きにくかったり、そもそもテストが無かったり(!!)、絶望的です。

今回はそういったモンキーパッチをかなり消しました。 機能的にまだまだ使わない、長大な機能を提供していたり、そもそも現在ではgemで解決できたり、そういったものの大部分は削除する方向になりました。

おかげでRails4アップグレードによって、ソースが少し綺麗になりました。

ただ、それでも消せない機能は残ってしまいます。だから、テストだけは、独自拡張のテストだけは絶対に書いておきたい。

We're Hiring!

期せずして、アップグレードついでに掃除までやりましたが……。 アップグレードを考えると、こういうことをやっちゃいけないなーというものが結構上がったと思います。

クラウドソーシングのクラウドワークス では、こんなものたちを一緒に掃除をしてくれるエンジニアを募集しています。次回Rails5に上げるときはこんな悲しい思いをしなくて済むといいな!!

www.wantedly.com

© 2016 CrowdWorks, Inc., All rights reserved.