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スコープに移動
これまで「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が有効でないため。
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が存在しない場合は別バージョンと認識されなかったため
- 5.2.4の時点でバージョンがない → 6.0.4でバージョン指定すると、別バージョンと認識されず5.2.4のキャッシュを取り出してしまう。
- 今回はversionオプションを使わなかったが、メンテを継続するにあたってはversionオプションを利用するのが良いと思う。
感想
自動テストを書いておくことがとても重要。上記に挙げた不具合の一部は、自動テストのおかげで発見できた。
自動テストによる検証が大切なことは、以下にも記述されている。 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
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が入っていたが、アップデートでそれが無くなる。
実装
- https://github.com/zdennis/activerecord-import/blob/v0.15.0/lib/activerecord-import/import.rb#L105-L110
- https://github.com/zdennis/activerecord-import/blob/v0.15.0/lib/activerecord-import/import.rb#L653
- https://github.com/zdennis/activerecord-import/blob/v0.15.0/lib/activerecord-import/adapters/mysql_adapter.rb#L72
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からは使えそう。
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 とは
背景
マッチング系のサービスにおいて
一方が任意の項目を設定する -> もう一方が回答する
といったモデルを設計するときに参考になるので調査した。
できることを調査
1. フォームの項目が自由に設定できる
フォームの項目はかなり自由度がある。以下興味深かったものを列挙
- 選択肢に「その他」を追加
- その他を選択した時だけ追加でフリーテキストの入力フィールドが表示される
- 画像付き選択肢
- 日付選択
- 条件付き選択肢
- ページ分割
- 途中の情報をどこに保存するんだ問題が出てくるので、ここやってくれるのは嬉しい。
2. フォームの項目に回答できる
回答がついた後にフォームの項目を編集、削除したらどうなるか?
フォームの項目と回答は直接は結びついていないっぽい。
回答は データ項目
というものに結びついている。
- 新しい回答が作られたときに、対応するデータ項目がない場合は新規に作成される。
- データ項目設定は以下のように編集できる
つまりフォームの項目からデータ項目設定は作られるが、その後フォームに表示する項目を編集、削除してもデータ項目は更新されず、ユーザーが手動で編集できるようにしている。
回答の集計もデータ項目ベースで行うっぽい
考察
- データ項目を用意したメリット
- フォーム経由の回答以外の回答もインポートしやすい
- 既存のデータをインポートして回答の集計に役立てたいとか
- フォームの項目を物理削除できる
- 回答は直で紐づいてないので、消そうと思えばフォームの項目を物理削除できる。これによってフォームの項目周りの設計はシンプルになると思われる。
- フォーム経由の回答以外の回答もインポートしやすい
- データ項目を用意したデメリット
- 余分な概念が入る分冗長
- ユーザーはデータ項目という概念を理解し、扱う必要がある
Googleフォームの調査
Googleフォームとは
www.google.com のこと
背景
マッチング系のサービスにおいて
一方が任意の項目を設定する -> もう一方が回答する
といったモデルを設計するときに参考になるので調査した。
できることを調査
1. フォームの項目が自由に設定できる
以下興味深かった設定を列挙
a.選択肢にはその他が設定でき、その場合は自由記述のフィールドが追加される
条件付き選択肢(ある選択肢を選んだときに追加のオプションが出るような機能)の一部として設計することも出来そう
b. 多様な回答の検証がある
2. フォームに回答できる
回答を受け付ける、受け付けないのSwitchがある
フォームの項目を消すとどうなるか
回答も消えてしまう。 これは良くない。 フォームの項目だけ非表示にして、回答は見える状態にしておくべき。
フォームの項目を編集すると、回答側はどうなるか
編集後のフォームの項目に対する回答になってしまう。 これも以下のようなケースが想定されるので良くない。
極端な例
- 同意しますか
-> はい
をチェック
- 項目を 否認しますか
に変更
- 回答は 否認しますか
に はい
と回答したように見える
構造が定まってないHashにアクセスする場合は[]よりもfetchのほうが安全
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内課金で購入されたクレジットやゲーム内通貨に有効期限を設定することはできません。
- 資金決済法の前払式支払手段による規制を避けるため、ポイントの有効期限を180日以内にしようと思っても(a)、プラットフォームのレギュレーションによりApp内課金でポイントを購入する場合はポイントに有効期限を付けられない(b)