ぱろっと・すたじお

技術メモなどをまったりと / my site : http://parrot-studio.com/

Rails4.2からRails5.0(RC1)に移行する際に修正したポイント

昨年の秋あたりから、お仕事の関係で監視していたRailsの開発状況ですが、
お仕事が関係なくなっても、なんとなく毎日チェックしておりまして

どうしてもβを採用するのは怖いので、(仕様が固まる)RC版を待っていたところ、
先日RC版がリリースされました(`・ω・´)

Riding Rails: This Week In Rails 💯: RailsConf recap & Rails 5.0 RC 1 is out!

ということで、手持ちの「チェンクロパーティーシミュレーター」を、
Rails4.2からRails5.0RC1に移行したわけです

ccpts.parrot-studio.com

移行する際に書き換えたコードがこれになりますが・・・

Rails5(RC1)に移行 / 入手先検索のバグを修正 / viewのコードを修正 / データの誤りを修正 · parrot-studio/cc-pt-viewer@9f3e715 · GitHub

・・・全体を見直したことによるバグfixも含むとはいえ、
なんとなくどこが変わったのかはつかめるんじゃないかと

以下、引っかかった点や変更点をざっくりと...φ(・ω・`)

modelをApplicationRecordからの継承に変更

以前は「ActiveRecord::Base」から直に継承していたmodelですが、
Rails5から「ApplicationRecord」というアプリ層クラスを経由するようになりました
この変更はとてもいいので、Rails4.2で進めている今のお仕事でも取り込んでいます(`・ω・´) b *1

例えば、複数のmodelを触るトランザクションを張る場合に、以前だと・・・

# Railsのクラスを直に触っている
ActiveRecord::Base.transaction do
  # なにか更新処理
end

# これが嫌なら、自分のクラスを経由するけど、なにか気持ち悪い(´-ω-)
Arcana.transaction do
  # 実際はArcana以外のmodelも更新している
end

・・・こんな感じで書くしかありませんでした

しかし、一つクラスをはさんだことで・・・

ApplicationRecord.transaction do
  # 明確な親クラスでトランザクションを使える
end

・・・こう書くことができて、とてもすっきりしますヽ(`・ω・´)ノ

トランザクション以外にも、アプリ全体に処理を仕込みたい場合、
全部「ApplicationRecord」に書けばいいので、ルールとしてわかりやすいです

Relationの仕様変更にはまる

Railsのバージョンアップをする際、だいたい鬼門になるのはActiveRecordです
今回も見事に仕様変更の罠を踏み抜きました(lll゚Д゚)

チェンクロのアルカナ情報を表すクラスとしてArcanaクラスを用意して、
その下に細かい情報クラスをぶら下げておりました

# Rails4.2のコード
class Arcana < ActiveRecord::Base
  # 中略
  belongs_to :first_skill,    class_name: 'Skill'
  belongs_to :second_skill,   class_name: 'Skill'
  belongs_to :third_skill,    class_name: 'Skill'
  belongs_to :first_ability,  class_name: 'Ability'
  belongs_to :second_ability, class_name: 'Ability'
  belongs_to :weapon_ability, class_name: 'Ability'
  # 後略
end

しかし、Rails5に移行したら、データのimport処理で引っかかりまくりまして、
吐き出されたエラーメッセージを読んだところ、
「結びついたデータがないぞ(#゚Д゚)」といったことが書いてありました

以前はこれで動いていたわけで、なにかおかしい・・・と思ったところ、
Relationの存在チェックするバリデーションが、Rails5からデフォルトで有効になっていたようです

関連を定義したのだから、その先の情報もチェックするのが当然・・・という思想はわかるのですが、
全てのアルカナが二つ以上スキルを持っているわけでもなく、
アビリティに至っては持ってないケースもあるので、これは困ります(´-ω-)

そこで、このようにチェックを無効化する必要があったのです

# Rails5のコード
class Arcana < ApplicationRecord
  # 中略
  belongs_to :first_skill,    class_name: 'Skill'
  belongs_to :second_skill,   class_name: 'Skill',   optional: true
  belongs_to :third_skill,    class_name: 'Skill',   optional: true
  belongs_to :first_ability,  class_name: 'Ability', optional: true
  belongs_to :second_ability, class_name: 'Ability', optional: true
  belongs_to :weapon_ability, class_name: 'Ability', optional: true
  # 後略
end

これで以前と同じ動作になります(`・ω・´)
(first_skillだけは必ず持っているはずなので、逆にチェックを増やした状態になってます)

schema.rbがすっきり&MariaDB関連のパッチが採用

移行するついでに、migrationを統合してしまおうと思いまして、
schema.rbの内容をそのままコピペしたmigrationを作ろうとしたのですが、
だいぶいろいろ変わっておりました

ActiveRecord::Schema.define(version: 20160507003347) do

  create_table "abilities", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin" do |t|
    t.string   "name",        limit: 100, null: false
    t.string   "explanation", limit: 500
    t.string   "weapon_name", limit: 100
    t.datetime "created_at",              null: false
    t.datetime "updated_at",              null: false
    t.index ["name"], name: "index_abilities_on_name", unique: true, using: :btree
  end

# ...

end

まず、create_tableのオプションに、MariaDBMySQL)固有のオプション情報が付加されています
あと、indexの生成が「add_index」ではなく、「t.index」という形でcreate_tableに統合されています
後者はとてもいいですね(`・ω・´) b

しかも、migration関連の処理がえらい速くなりました
単にmigrationを統合したから、という感じではない速度です

デフォルトAPサーバがpumaに変更されたが・・・

ActionCableが追加され、WebSocketが採用されたために、
Webrickからpumaに変更されたのですが、
これがいろいろ問題を引き起こしているようで・・・(´-ω-)

configレベルで誤りがあって起動しないって場合に、
Webrickなら例外を吐いて止まってくれるのですが、
pumaだと落ちずにずっとフリーズしたままで、他のコンソールからkillしないといけません

特にコンソールに例外を吐いてくれないのが困ります
どこを直せばいいのかわかりませんし(´・ω・`)

他にもreload周りのissueが挙がっているようですし、
まだまだ不安定な感じがしますね・・・
まあ、本番で使うわけではないですし、コードの本体をいじる際には問題ないのですが

ArelでOR文の書き方変更

Rails5より前だと、ActiveRecordでOR文を書く際、
やや面倒な書き方をしなければいけませんでした(´-ω-)

  def skill_search(category, cost, sub, ef)
    return [] if (category.blank? && cost.blank?)

    arel = SkillEffect.all
    arel.where!(category: category) unless category.blank?
    arel.where!(subcategory: sub) unless sub.blank?
    arel = arel.joins(:skill).where(skills: { cost: cost }) unless cost.blank?
      unless ef.blank?
        efs = [ef].flatten.uniq.compact
        arel.where!(
          SkillEffect.where(
            subeffect1: efs
          ).where(
            subeffect2: efs
          ).where(
            subeffect3: efs
          ).where(
            subeffect4: efs
          ).where(
            subeffect5: efs
          ).where_values.reduce(:or)
       )
    end

    arel.pluck(:skill_id)
  end

「where_values.reduce(:or)」って書き方は美しくないので、
「or(条件)」という書き方が採用されております(`・ω・´) b

そこまではいいのですが、ANDとORが混在する場合のルールがいまいちよくわからなくて、
whereとorの順番を変えるだけで、いろいろ変わってしまいまして・・・

先にwhereを書いてからorを書くと、whareの条件と混線してしまうので、
orを先に書いてしまってからwhereを書くことでなんとか

  def skill_search(category, cost, sub, ef)
    return [] if (category.blank? && cost.blank?)

    arel = SkillEffect.all
    # 先にOR条件を構築
    unless ef.blank?
      efs = [ef].flatten.uniq.compact
      arel = arel.where(subeffect1: efs)
        .or(SkillEffect.where(subeffect2: efs))
        .or(SkillEffect.where(subeffect3: efs))
        .or(SkillEffect.where(subeffect4: efs))
        .or(SkillEffect.where(subeffect5: efs))
    end

    # そのあとAND条件を付加
    arel = arel.where(category: category) unless category.blank?
    arel = arel.where(subcategory: sub) unless sub.blank?
    arel = arel.joins(:skill).where(skills: { cost: cost }) unless cost.blank?
    arel.pluck(:skill_id)
  end

このあたりは生成されるSQLを眺めて慎重に進めるしかないのですが、
やはりドキュメントが欲しいですね(´-ω-)

キャッシュの制御がフレームワーク

処理効率化のため、Rails.cacheを使うってのはよくあるはなしですが、
開発環境では邪魔だし、かといってテストもできないと困るので、
以前は自前のconfigにON/OFFフラグを記述して制御していました

   def with_cache(name, &b)
      return unless (name && b)
      # configの情報を見て利用の有無を制御
      return b.call unless ServerSettings.use_cache?
      Rails.cache.fetch(name, &b)
   end

これがRails5では不要になりまして、
「tmp/caching-dev.txt」の有無で制御できるようになってます
(実際の操作は「rails dev:cache」か「rake dev:cache」するだけです)

# config/environments/development.rb
  # Enable/disable caching. By default caching is disabled.
  if Rails.root.join('tmp/caching-dev.txt').exist?
    config.action_controller.perform_caching = true

    config.cache_store = :memory_store
    config.public_file_server.headers = {
      'Cache-Control' => 'public, max-age=172800'
    }
  else
    config.action_controller.perform_caching = false

    config.cache_store = :null_store
  end

こういうコードがdevelopment.rbに自動生成されるおかげなので、
他のバージョンでもこれをコピペすれば同じことができます(`・ω・´) b

まとめ

ということでざっくりと修正した点を見てきましたが、
思ったよりも少なかったかな・・・という印象です
まあ、アプリの規模が小さいってのはありますが(´-ω-)

Railsの思想から逸脱したコードを量産しない限りは、
わりと移行しやすいんじゃないかと思いますので、
やはりRailsの基本的な仕組みは理解しておいた方がいいですね

これを書いている時点の最新はRC1のままで、
リリース版になったらまた修正が入るかもしれないので、
その際はまた書いてみます...φ(・ω・`)

*1:rails g model」した後に、いちいちクラス定義を書き換える手間はありますが、それを上回るすっきり感がありますし(`・ω・´)