Railsを5.2.4から6.0.4にアップデートした手順をメモ

Railsを5.2.4→6.0.4にアップデートする機会があった。アップデートのために行った手順、手順ごとの調査をメモした。

手順

1. Rails以外のバージョンが古いライブラリをアップデートする

Railsアップデートの影響範囲を小さくするため、Rails以外のライブラリをアップデートしておく。 特にメジャーバージョンが古いライブラリは上げておく。 全部のバージョンをpatchまで最新にしようとするとキリがないので、そこはラフに判断する。

2. gemfileをrails 6.0.4 にして、bundle update rails を実行し失敗の原因となったgemを最新にする

3. rails を6.0.4 にアップデートする

gem 'rails', '6.0.4'

bundle update rails して成功することを確認する。

4. rails app:update する

app:updateとは

Rails アップグレードガイド_1.4 アップデートタスク

Gemfileに記載されているRailsのバージョンを更新後、このコマンドを実行することで、新しいバージョンでのファイル作成や既存ファイルの変更を対話形式で行うことができます。

app:updateを実行する

m(migrate)を打ってエディタで差分を確認したが、今回は修正すべき内容が特に見当たらなかった。

5. RailsガイドのRails 5.2からRails 6.0へのアップグレードを読む

Rails 5.2からRails 6.0へのアップグレード 以下、特に気になった項目を列挙。

npmの全パッケージが@railsスコープに移動

Rails 5.2からRails 6.0へのアップグレード

これまで「actioncable」「activestorage」「rails-ujs」パッケージのいずれかをnpmまたはyarn経由で読み込んでいた場合は、これらを6.0.0にアップグレードする前にそれらの依存関係の名前を以下のように更新しなければなりません。

rails-ujsであれば rails-ujsから@rails/ujsへ変更する

Rails 6のアップデートに先立ってこちらを修正してリリースした。

セキュリティ向上のためpurposeとexpiryメタデータが署名済みおよび暗号化済みCookieに埋め込まれるようになった

cookieを引き続きRails 5.2以前でも読み取れるようにする必要がある場合や、6.0のデプロイを検証中で前のバージョンに戻せるようにしたい場合は、Rails.application.config.action_dispatch.use_cookies_with_metadataにfalseを設定してください。

Rails 6.0から5.2へ巻き戻せるようにしたいので設定する。

purposeについては以下を参考にした。

Rails 6のcookieに「purpose」メタデータが追加(翻訳)|TechRacho by BPS株式会社

autoloaderがclassicからzeitwerkに変更される

アップデートの影響を小さくするため、ひとまずclassicのままRails 6.0へアップデートすることにした。

Rails アップグレードガイド_3.7.15 Rails 6でclassicモードのオートローダーを使う方法

6. 動かして出てきた不具合を修正する

rails sしてログを見たり、自動テストの結果を見たり、アプリケーションを触って出てきた不具合を修正していく。

a. hamlit-rails 周りのDEPRECATION WARNING

W, [2021-09-09T10:57:35.117155 #87570]  WARN -- : DEPRECATION WARNING: Single arity template handlers are deprecated. Template handlers must
now accept two parameters, the view object and the source for the view object.
Change:
  >> #<Hamlit::RailsTemplate:0x00007fef2f58f2c0>.call(template)
To:
  >> #<Hamlit::RailsTemplate:0x00007fef2f58f2c0>.call(template, source)

参考: Rails 6 を動かす際に "DEPRECATION WARNING: Single arity template handlers are deprecated." という警告が出た場合の対処

bundle update hamlit-railsを実行し HamlをアップデートしたらDEPRECATION WARNINGは消えた。

こちらはrails6.0アップデートに先立ってリリースした。

b. RailsのInitialize中にautoloadを使ったクラスの読み込みで警告が出る

ログ

W, [2021-09-09T11:11:44.347319 #88314]  WARN -- : DEPRECATION WARNING: Initialization autoloaded the constant FooBar.

Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload FooBar, for example,
the expected changes won't be reflected in that stale Class object.

`config.autoloader` is set to `classic`. This autoloaded constant would have been unloaded if `config.autoloader` had been set to `:zeitwerk`.

Please, check the "Autoloading and Reloading Constants" guide for solutions.

ソース

原因

RailsのInitialize中はautoloadが有効でないため。

Zeitwerkの壊し方 - Qiita

initialiizers実行時はZeitwerkのオートロードが有効になっていません。 Railsは一連のinitializerを実行した最後に、Rails::Application::Finisherを呼び出しますが、ここで初めてZeitwerkが起動する仕組みになっています。

解決策

クラス読み込みが失敗したファイルにrequireを追加すれば解決する。

require './path/to/fobar'

あるいは以下の記事にあるように config.after_initialize を使ってもうまくいく。 STORES Rails アプリを Zeitwerk 有効化するまでの道のり - STORES Tech Blog

ログにある通り、initializerで何らかのクラスを変数にセットしている場合、それらはautoloadしても更新されない(stale Class objectになる) 点に注意する

Reloading does not reboot the application, and therefore code executed during initialization does not run again. So, if you reload FooBar, for example, the expected changes won't be reflected in that stale Class object.

参考: 定数の自動読み込みと再読み込み (Zeitwerk)_6.1 古くなったオブジェクトの再読み込み

c. Rails 6.0から入ったenum negative scope がアプリケーションで定義したscopeをoverrideしている

W, [2021-09-09T12:20:28.272948 #90420] WARN -- : Creating scope :not_deleted. Overwriting existing method FooBar.not_deleted.

ネガティブスコープの影響 https://railsguides.jp/6_0_release_notes.html

すべてのenum値についてネガティブスコープを追加 (Pull Request) https://github.com/rails/rails/pull/35381

動作検証したところ、アプリケーションで定義したFooBar.not_deletedはoverrideされ、enum negative scopeが呼ばれていた。 今回のケースでは、どちらのスコープでも実行されるsqlが全く一緒だったので、アプリケーションで定義したFooBar.not_deletedを削除することで解決した。

d.config_forから返されるハッシュのsliceメソッドの引数にシンボルでなく文字列でアクセスするとエラー

configuration.foo_barのkeyが5.2系ではstringだったのに6.0系ではsymbolになっている。

https://railsguides.jp/6_0_release_notes.html#railties-%E9%9D%9E%E6%8E%A8%E5%A5%A8%E5%8C%96

config_forから返されるハッシュにシンボルでないキーでアクセスすることを非推奨化 (Pull Request)

Hash.[]ならstring keyでもアクセスできるが、Hash.sliceだと非推奨化じゃなくて取得できなかった

6.0のログ

Loading development environment (Rails 6.0.4)
[1] pry(main)> Rails.configuration.foo_bar
=> {:a=>[1, 2], :b=>[3], :c=>[4, 5, 6, 7]}
[2] pry(main)> Rails.configuration.foo_bar.slice('a','b').values.flatten.sort
=> []
[3] pry(main)> Rails.configuration.foo_bar.slice('a','b')
=> {}
[4] pry(main)> Rails.configuration.foo_bar.slice(:a,:b)
=> {:a=>[1, 2], :b=>[3]}

[1] pry(main)> Rails.configuration.foo_bar['a'] # [] のアクセスなら取得できる
=> [1, 2]
[2] pry(main)> Rails.configuration.foo_bar.slice('a') # slice だと取得できない
=> {}

[3] pry(main)> Rails.configuration.foo_bar.class
=> ActiveSupport::OrderedOptions # https://api.rubyonrails.org/v6.0.4/classes/ActiveSupport/OrderedOptions.html string key を symbol に変換する処理あり

# ['string key']['string key'] とすると DEPRECATION WARNINGが出た
[8] pry(main)> Rails.configuration.repro['a']['b']
DEPRECATION WARNING: Accessing hashes returned from config_for by non-symbol keys is deprecated and will be removed in Rails 6.1. Use symbols for access instead. (called from <main> at (pry):8)
W, [3-09-10T12:26:05.948315 #5023]  WARN -- : DEPRECATION WARNING: Accessing hashes returned from config_for by non-symbol keys is deprecated and will be removed in Rails 6.1. Use symbols for access instead. (called from <main> at (pry):8)

5.2のログ

D, [3-09-10T11:01:22.410693 #3695] DEBUG -- :    (8.2ms)  SET NAMES utf8mb4 COLLATE utf8mb4_general_ci,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
Loading development environment (Rails 5.2.6)
[1] pry(main)> Rails.configuration.foo_bar
=> {"a"=>[1, 2], "b"=>[3], "c"=>[4, 5, 6, 7]}
[3] pry(main)> Rails.configuration.foo_bar.slice('a','b').values.flatten.sort
=> [3, 1, 2]

[2] pry(main)> Rails.application.config_for(:foo_bar).class
=> Hash # 6.0.4だとActiveSupport::OrderedOptionsに変わっている

解決策

sliceしている箇所はstring_keyからsymbol_keyにした。 他、怪しそうな箇所がちゃんと動いているか調べた。

d. ActiveRecordインスタンスの配列のキャッシュのデシリアライズに失敗する

Rails.cache.fetch('some_key') do
   FooBar.scope_method.to_a
end

のようにActiveRecordインスタンスの配列をシリアライズして保存していた。 これのでシリアライズに失敗するようになった。

原因

Rails 5 -> 6でシリアライズの形式が変わったためだと思われる。

解決策

cacheのkeyの末尾にバージョンを入れ、Rails 6系の時にRails 5系のキャッシュへのヒットを避けた。

Rails.cache.fetch("some_key_#{Rails.configuration.cache.version}") do
   FooBar.scope_method.to_a
end
採用しなかった解決策
  • versionオプションを使う
    • https://api.rubyonrails.org/v5.2.5/classes/ActiveSupport/Cache/Store.html#method-i-fetch
    • Setting :version verifies the cache stored under name is of the same version. nil is returned on mismatches despite contents. This feature is used to support recyclable cache keys.

    • versionが存在しない場合は別バージョンと認識されなかったため
    • 今回はversionオプションを使わなかったが、メンテを継続するにあたってはversionオプションを利用するのが良いと思う。
      • FooBarにメソッドやインスタンス変数が追加されるとデシリアライズに失敗するリスクがあるので、そういった場合に備えて適切にバージョン管理する仕組みが必要であるため。

感想

自動テストを書いておくことがとても重要。上記に挙げた不具合の一部は、自動テストのおかげで発見できた。

自動テストによる検証が大切なことは、以下にも記述されている。 Rails アップグレードガイド - Railsガイド

アップグレード後にアプリケーションが正常に動作していることを確認する方法としては、良いテストカバレッジをアップグレード前に準備しておくのが最善です。アプリケーションを一気に検査する自動テストがないと、変更点をすべて手動で確認しなければならず膨大な時間がかかってしまいます。Railsのようなアプリケーションの場合、これはアプリケーションのあらゆる機能を一つ残らず確認しなければならないということです。アップグレードの実施は、テストカバレッジをきちんと準備してから行なうよう、くれぐれもお願いします。

activerecord-import の on_duplicate_key_update, on_duplicate_key_ignore 周りを調査

背景

activerecord-importのバージョンを0.15.0から1.2.0(最新)にアップデートしたい。 on_duplicate_key_update周りでBREAKING_CHANGEがある。 DBはMySQLを使用している。

on_duplicate_key_updateとは

MySQL

https://dev.mysql.com/doc/refman/5.6/ja/insert-on-duplicate.html

UNIQUEインデックスまたはPRIMARY KEYが重複するKeyを持つ(つまりduplicate keyな)行が挿入された場合に、どのカラムを更新するか指定できる。

activerecord-import

https://github.com/zdennis/activerecord-import/#duplicate-key-update

MySQL, PostgreSQL (9.5+), and SQLite (3.24.0+) support on duplicate key update (also known as "upsert") which allows you to specify fields whose values should be updated if a primary or unique key constraint is violated.

BREAKING_CHANGE

https://github.com/zdennis/activerecord-import/blob/ee0b95c6a3f17a3d76de340e2a6f4eecc4a7de16/CHANGELOG.md#breaking-changes

Previously :on_duplicate_key_update was enabled by default for MySQL. The update timestamp columns (updated_at, updated_on) would be updated on duplicate key. This was behavior is inconsistent with the other database adapters and could also be considered surprising. Going forward it must be explicitly enabled. See #548.

今まではon_duplicate_key_updateオプションにデフォルトでupdated_atが入っていたが、アップデートでそれが無くなる。

実装

on_duplicate_key_ignoreとは

https://github.com/zdennis/activerecord-import/#duplicate-key-ignore

MySQL, SQLite, and PostgreSQL (9.5+) support on_duplicate_key_ignore which allows you to skip records if a primary or unique key constraint is violated. MySQL it uses INSERT IGNORE

duplicate keyな行が挿入された場合に、レコードの更新をしないためのオプション。

https://dev.mysql.com/doc/refman/5.6/ja/insert.html

IGNORE キーワードを使用した場合、INSERT ステートメントの実行中に発生したエラーは無視されます。たとえば、IGNORE を使用しない場合は、テーブル内の既存の UNIQUE インデックスまたは PRIMARY KEY 値を複製する行によって重複キーエラーが発生し、このステートメントは中止されます。IGNORE を指定すると、その行が破棄され、エラーは発生しません。代わりに、無視されたエラーが警告を生成する可能性がありますが、重複キーエラーは生成しません。

エラーを抑制するオプションで、エラーに気付くづらくなるというデメリットがあるため、基本的には指定しないほうが良さそう。

not null制約に関するエラーも無視するっぽい https://stackoverflow.com/a/548570

Inserting a NULL into a column with a NOT NULL constraint.

調査

duplicate keyなレコードをimportしたときにどのようにupdateされるか調査。

ver 0.15.0

on_duplicate_key_update を指定しない場合

updated_atだけ更新されている。

on_duplicate_key_update[:name] を指定した場合

nameとupdated_atが更新されている。

allオプションを指定した場合

ver 0.15.0では指定できなかった。0.26.0からは使えそう。

https://github.com/zdennis/activerecord-import/blob/ee0b95c6a3f17a3d76de340e2a6f4eecc4a7de16/CHANGELOG.md#new-features-7

ver 1.2.0

on_duplicate_key_update を指定しない場合

Duplicate entryエラーがでた。内部的にはINSERTしているだけだから当然か。

on_duplicate_key_update([:name]) を指定した場合

nameとupdated_atが更新された。 updated_atは指定しなくても更新されるっぽい。

on_duplicate_key_update([:updated_at]) を指定した場合

nameは更新されず、updated_atだけ更新される。

Ralisのstrict validation

Active Record バリデーション - Railsガイド

使ったことなかったが、ユーザー普通にフォームからPOSTしただけでは起きえないエラーのvalidationに良さそう。 POSTされたデータが、普通にフォームをPOSTしただけじゃ起きえないような整合性の崩れ方している場合とかを検出するのに使えそう。

https://api.rubyonrails.org/classes/ActiveModel/StrictValidationFailed.html

Raised when a validation cannot be corrected by end users and are considered exceptional.

公式ドキュメントにもそう書いてあった。

メリット

  • 例外が投げられるので、エラーレポーティングツールに検知される。
    • バグだった場合に気づきやすい。
  • 明らかにおかしいデータの場合にraiseしてくれるので、後続の処理ではそういったおかしいデータを前提にしないで処理が書ける
    • つまりガード節的な使い方ができる

form runの調査

form run とは

フォーム系のSaaS form.run

背景

マッチング系のサービスにおいて 一方が任意の項目を設定する -> もう一方が回答する といったモデルを設計するときに参考になるので調査した。

できることを調査

1. フォームの項目が自由に設定できる

フォームの項目はかなり自由度がある。以下興味深かったものを列挙

  • 選択肢に「その他」を追加
    • その他を選択した時だけ追加でフリーテキストの入力フィールドが表示される
  • 画像付き選択肢
  • 日付選択
  • 条件付き選択肢
    • ある選択をした時だけ追加で出てくる選択肢
    • 例. ECサイトで配送先の選択: 置き配を選択 -> 玄関前か、宅配ボックスかといった選択肢が表示される
  • ページ分割
    • 途中の情報をどこに保存するんだ問題が出てくるので、ここやってくれるのは嬉しい。

2. フォームの項目に回答できる

回答がついた後にフォームの項目を編集、削除したらどうなるか?

フォームの項目と回答は直接は結びついていないっぽい。 回答は データ項目 というものに結びついている。

  • 新しい回答が作られたときに、対応するデータ項目がない場合は新規に作成される。
  • データ項目設定は以下のように編集できる
    • f:id:hayabusa1113:20210719184352p:plain

つまりフォームの項目からデータ項目設定は作られるが、その後フォームに表示する項目を編集、削除してもデータ項目は更新されず、ユーザーが手動で編集できるようにしている。

回答の集計もデータ項目ベースで行うっぽい

考察

  • データ項目を用意したメリット
    • フォーム経由の回答以外の回答もインポートしやすい
      • 既存のデータをインポートして回答の集計に役立てたいとか
    • フォームの項目を物理削除できる
      • 回答は直で紐づいてないので、消そうと思えばフォームの項目を物理削除できる。これによってフォームの項目周りの設計はシンプルになると思われる。
  • データ項目を用意したデメリット
    • 余分な概念が入る分冗長
    • ユーザーはデータ項目という概念を理解し、扱う必要がある

Googleフォームの調査

Googleフォームとは

www.google.com のこと

背景

マッチング系のサービスにおいて 一方が任意の項目を設定する -> もう一方が回答する といったモデルを設計するときに参考になるので調査した。

できることを調査

1. フォームの項目が自由に設定できる

以下興味深かった設定を列挙

a.選択肢にはその他が設定でき、その場合は自由記述のフィールドが追加される

f:id:hayabusa1113:20210719182817p:plain

条件付き選択肢(ある選択肢を選んだときに追加のオプションが出るような機能)の一部として設計することも出来そう

b. 多様な回答の検証がある

f:id:hayabusa1113:20210719182621p:plain

2. フォームに回答できる

回答を受け付ける、受け付けないのSwitchがある

f:id:hayabusa1113:20210719183001p:plain

f:id:hayabusa1113:20210719183013p:plain

フォームの項目を消すとどうなるか

回答も消えてしまう。 これは良くない。 フォームの項目だけ非表示にして、回答は見える状態にしておくべき。

フォームの項目を編集すると、回答側はどうなるか

編集後のフォームの項目に対する回答になってしまう。 これも以下のようなケースが想定されるので良くない。

極端な例 - 同意しますか -> はいをチェック - 項目を 否認しますか に変更 - 回答は 否認しますかはい と回答したように見える

構造が定まってないHashにアクセスする場合は[]よりもfetchのほうが安全

Railsの話。Hashは[]で値が取り出せる

pry(main)> p = {a: {b: :c}}
pry(main)> p[:a][:b]
 => :c

Hashの構造が定まってなくて、求めている構造のときのみ値を取得するケースを考える

例えば信用できない外部APIのレスポンスをパースしたり、ユーザーがPOSTするデータ*1をパースするケースが該当する。

[]だと発生する問題

pry(main)> p = {} #  p = {a: {b: :c}}を期待しているが実際は {} が来た
=> {}

pry(main)> p[:a][:b] # NoMethodError: undefined method `[]' for nil:NilClass 

p.try(:[], :a).try(:[], :b) # tryを使ってNoMethodErrorを回避する
=> nil 

これで良いかと思ったら以下のケースで問題が起きた

pry(main)> p = {a: 'b'} # こういうのが来た
=> {:a=>"b"}

pry(main)> p.try(:[], :a).try(:[], :b)
# TypeError: no implicit conversion of Symbol into Integer
# from /Users/user/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activesupport-5.2.3/lib/active_support/core_ext/object/try.rb:19:in `[]'

何故TypeErrorが起きたか

pry(main)> p = {a: 'b'}
=> {:a=>"b"}

pry(main)> p.try(:[], :a)
=> "b"

なので 文字列に対する[]メソッドが呼ばれる。 ドキュメントを見る限りこのメソッドの引数にsymbolは期待されていないので、無理やりIntegerに変換しようとしてエラーが起きたのだと思われる。

解決策

fetchを使う。 fetchは文字列に対してメソッドが定義されてないので、tryによってnilが返却される

pry(main)> p = {a: 'b'}
=> {:a=>"b"}
pry(main)> p.fetch(:a).try(:fetch, :b)
=> nil

教訓

  • 構造が定まってないHashにアクセスする場合は[]よりもfetchのほうが安全かもしれない。
  • [] はいろんなクラスに定義されているメソッドなので、型が不定な場合に[]を使うことに気をつけた方が良い。

*1:正確にはユーザーがPOSTするデータはHashではなくActionController::Parameters だが、同じようなものとして扱えると思う

『良いウェブサービスを支える 「利用規約」の作り方』を読んだ

良いウェブサービスを支える 「利用規約」の作り方 を読んだ。

利用規約に限らず、ウェブサービスに関する法規制についてのわかりやすいガイドブックだった。

この本を読んだモチベーション

  • 法的な制約によりXXの実装が必要だったことが後から発覚して手戻りが発生する、といったリスクを減らしたいため。
    • 例えば、確認画面を実装する必要があることが後から発覚したり
      • 参照: 通信販売|特定商取引法ガイド
        • 申込みをする際、消費者が申込み内容を容易に確認し、かつ、 訂正できるように措置していないことを「顧客の意に反して売買契約等の申込みをさせようとする行為」として禁止し、行政処分の対象としています。

  • サービスを開発する上でのルールを知ることで、全体最適された解決策を出せるようにしたいため。

個人的に面白かった点

プラットフォームを利用する場合は法律に加えてプラットフォームの規約も意識しなければならないこと

    • 資金決済法の前払式支払手段による規制を避けるため、ポイントの有効期限を180日以内にしようと思っても(a)、プラットフォームのレギュレーションによりApp内課金でポイントを購入する場合はポイントに有効期限を付けられない(b)
      • (a)は法律の範囲だが、(b)はプラットフォームの規約。(b)まで法務がカバーしてない可能性がある。エンジニアが実装のことしか考えてないと、iOSアプリのレビューが却下された段階で(b)が発覚してヤバイことになるので気をつけたい。
      • App Store Reviewガイドライン - Apple Developer
        • App内課金で購入されたクレジットやゲーム内通貨に有効期限を設定することはできません。

事業のことがわかると良い提案ができる

  • サービスの現状や将来のビジョンがわかると、それらを踏まえた上でより良い利用規約が提案できる。
    • しかしそうではなく、むしろそういったネガティブな「リスク」こそ、みなさんから積極的に開示して相談してほしいのです。そうすれば、私たちのような職業に就く者の多くは、そういった実直なリスクコミュニケーションをしてくれる経営者・エンジニアには「アイデアイノベーションの可能性をできるだけ無駄にせずに、しかしリスクは利用規約によってできるだけ最小化しよう」という発想で、対応策を考えます。

  • ここらへんはエンジニアも法務も同じだなと思った。