Timee Product Team Blog

タイミー開発者ブログ

Rails edgeでCIを回し始めました 〜見つけたエラー編〜

こんにちは、マッチング領域でバックエンドエンジニアをしているぽこひで ( @pokohide ) です。

前回はRails edgeでCIを回し始めた話を紹介しました。

tech.timee.co.jp

今回は、実際に弊社でCIをRails edgeで回し始めた事で見つけたエラーの例を紹介していきます。記事公開時点(2023年7月)のバージョンは下記の通りです。

$ ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) +YJIT [aarch64-linux]

$ rails -v
Rails 7.0.6

ActiveRecord::DangerousAttributeError object_id is defined by Active Record

このエラーに関する参考記事はこちらです。

euglena1215.hatenablog.jp

一部のモデルで object_id というカラム名を定義していたため以下のようなエラーが発生し、そのレコードを生成しているテストが軒並み落ちました。

ActiveRecord::DangerousAttributeError:
  object_id is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name.
# /usr/local/bundle/gems/factory_bot-6.2.1/lib/factory_bot/decorator/new_constructor.rb:9:in `new'
# /usr/local/bundle/gems/factory_bot-6.2.1/lib/factory_bot/decorator.rb:16:in `send'
...

Objectクラスで、オーバーライドされると予期せぬ影響を与えうるメソッド名と同じ名前を利用できなくなったからでした。 object_id に限らず dup, freeze, hash, class, clone なども利用できなくなるのでそういったカラム名を使わないようにする必要があります。

このエラーは object_idobject_identifier とリネームする事で対応しました。

Rails7.0.5.1からの create_association の挙動変化によるエラー

巷で話題のcreate_associationの挙動変更によるエラーです。既存レコードが存在する場合にユニーク制約によりバリデーションエラーが発生するといってテストケースがありましたが、そちらのテストが落ちました。Rails edgeを回し始めた頃はRails v7.0.4だったため、このエラーに遭遇しました。

Rails v7.0.5.1から create_association の挙動が「別々のトランザクションでinsertしてからdeleteする」から「同一トランザクションでdeleteしてからinsertする」に変わりました。この影響により、DBのレコードに依存するバリデーション(例: validates_uniquness_of )が効かなくなりました。この挙動変化に関する内容はこちらをご参考ください。

blog.willnet.in

parser gemを利用してcreate_associationの呼び出し箇所を調査し、影響範囲を一つずつ確認、必要な箇所に条件文を追加するなどして対応しました。

既にリリースされている変更ではありますが、Rails edgeでCIを回し始めたことで早期に問題を発見でき、create_association削除する前に検証を挟む新しいオプションの提案をrails/railsへのPR*1を通して行うなどしました。

NoMethodError: undefined method `reading_role' for ActiveRecord::Base:Class

Rails7.1から ActiveRecord::Base.reading_role がなくなるため、この記述を行っていた箇所のテストが落ちました。

irb(main):002:0> ActiveRecord::Base.reading_role
{"severity":"WARN","message":"DEPRECATION WARNING: ActiveRecord::Base.reading_role is deprecated and will be removed in Rails 7.1.\nUse `ActiveRecord.reading_role` instead.\n (called from xxxxx)"}
=> :reading

irb(main):003:0> ActiveRecord::Base.writing_role
{"severity":"WARN","message":"DEPRECATION WARNING: ActiveRecord::Base.writing_role is deprecated and will be removed in Rails 7.1.\nUse `ActiveRecord.writing_role` instead.\n (called from xxxxx)"}
=> :writing

この問題に関しては元々出ていたWarningの内容に従い、ActiveRecord.reading_role を代わりに使う事で対応しました。

partialsにインスタンス変数をlocalsとして渡す挙動が削除されたことによるエラー

弊社では請求書(PDF)を生成するために ActionController::Base#render_to_string を用いてHTML文字列を取得しています。その処理の中でpartialsにインスタンス変数をlocalsとして渡していましたが、そこでエラーが発生しました。

今までWarningが出ていた内容ではありますが、Rails7.1からインスタンス変数をlocalsで渡せなくなったためです*2

インスタンス変数ではなくローカル変数として渡すことで対応を予定しています。

before_type_castの返り値の型が変わったことによるエラー

# create_table :posts, force: true do |t|
#   t.integer :foo, default: 1, null: false
# end
class Post < ActiveRecord::Base
  enum foo: { x: 1, y: 2 }
end

Integer型でEnumを定義しているカラム foo を持つモデルのレコードに対して record.foo_before_type_cast で参照すると、元々の環境ではInteger型で返っていたものが、Rails edge環境下ではString型で返るようになったため落ちているテストケースを見つけました。

record = Post.new(foo: :x)

# 検証当時の環境(Rails v7.0.6)
record.foo_before_type_cast
=> 1

# Rails edge
record.foo_before_type_cast
=> "1"

今回のエラーを再現するコード*3を用意し、 git bisect*4 を利用して二分探索で挙動が変わったをコミットを調査しました。その後のコミットやPRを追ったところ、rails v7.1からDBの型で値を取得する *_for_database が追加されていることに気づいたため、その実装をバックポートして使うように修正することで対応を行いました。

# frozen_string_literal: true

case Rails::VERSION::STRING
when /^7\.0/
  # 以下の定義を読み込むために何もしない。
when /^7\.1/
  # v7.1 には含まれているため読み込まない。
  return
else
  # v7.2 以降で削除し忘れないように例外を投げる。
  raise('Consider removing this patch')
end

module ActiveRecord
  module AttributeMethods
    module BeforeTypeCast
      # refs: https://github.com/rails/rails/pull/46283
      def read_attribute_for_database(attr_name)
        name = attr_name.to_s
        name = self.class.attribute_aliases[name] || name

        attribute_for_database(name)
      end
    end
  end
end

最後に

今回は、Rails edgeでCIを回すことによって見つけた将来動かなくなるコードの早期発見やその対応、原因についての簡単な解説を行いました。また紹介しきれていないですが、エラーだけでなくRails 7.2からの廃止予定を知らせるWarningもいくつか確認できました。

Rails edge導入当初は112個のテストケースが落ちましたが、徐々に対応を行なっていき落ちるテストケースは22件にまで減りました。こういった活動を継続することでRubyRailsのコミュニティの進化に追随できるので引き続き頑張っていこうと思います。

タイミーでは一緒にサービスを成長させていく方を募集しています。もし少しでも興味を持っていただけたら、カジュアル面談受け付けておりますので是非お話ししましょう!

devenable.timee.co.jp