Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ(20210322前編)Active Recordのstrict loadingの修正、セキュリティリリースのポリシー追加、N+1チェッカーprosopite gemほか

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

以下のコミットリストから見繕いました。コミットは少なめで、Changelogの変更はありませんでした。

🔗 Active Recordでprecisionなしのnumericalityバリデーションを修正

#32852で追加されたテストケースはActive Modelではパスするが、#38210Float::DIGBigDecimal.double_figに変更されてからパスしなくなっていた。

irb(main):001:0> require 'bigdecimal/util'
=> true
irb(main):002:0> 65.6.to_d
=> 0.656e2
irb(main):003:0> 65.6.to_d(Float::DIG)
=> 0.656e2
irb(main):004:0> 65.6.to_d(BigDecimal.double_fig)
=> 0.6559999999999999e2

同PRより大意


つっつきボイス:「precision(DBの有効桁数)が指定されてないnumericalityバリデーションを修正したのね」「to_dはBigDecimalへの変換で、以前は桁数をFloat::DIGで15桁に指定していたのがBigDecimal.double_figに変わって桁数が違ってしまったのでFloat::DIGに戻したんですね」

# activerecord/lib/active_record/validations/numericality.rb#L4
  module Validations
    class NumericalityValidator < ActiveModel::Validations::NumericalityValidator # :nodoc:
      def validate_each(record, attribute, value, precision: nil, scale: nil)
-       precision = [column_precision_for(record, attribute) || BigDecimal.double_fig, BigDecimal.double_fig].min
+       precision = [column_precision_for(record, attribute) || Float::DIG, Float::DIG].min
        scale     = column_scale_for(record, attribute)
        super(record, attribute, value, precision: precision, scale: scale)
      end

Float::DIGBigDecimal.double_figはFloatの最大桁数の定数だけど、前者は最大の10進桁数で後者はFloatクラスが保持できる有効数字の数だから同じではないということか↓」

Float が表現できる最大の 10 進桁数です。通常はデフォルトで 15 です。

Ruby の Float クラスが保持できる有効数字の数を返します。

# docs.ruby-lang.orgより
require 'bigdecimal'
p BigDecimal::double_fig  # ==> 20 (depends on the CPU etc.)

「お〜、C言語では有効桁数をこうやって計算するのね↓」「これは参考になりそう」

// docs.ruby-lang.orgより
double v = 1.0;
int double_fig = 0;
while (v + 1.0 > 1.0) {
   ++double_fig;
   v /= 10;
}

🔗 Preloader::Association::LoaderQueryが追加


つっつきボイス:「今週はPreloader周りの改修がほかにもいくつかあったんですが、それぞれの関連がよくわからなかったのでこれを代表としてピックアップしてみました」

「このプルリクで引用されている#41385はこれか↓」「サマリーが長いですね...」

# 41385より
# ケース1: 親モデルは異なり、associationは同じ
ActiveRecord::Associations::Preloader.new(records: [book, post], associations: :author).call

# ケース2: 親モデルは同じで、同じテーブルに複数のassociationがある
ActiveRecord::Associations::Preloader.new(records: favorites, associations: [:author, :favorite_author]).call

「従来は以下のように?でプレースホルダ化されるクエリが上のどちらでも同じになっていたけど↓、同じクエリを2回実行するのは冗長なので#41385でクエリをグループ化して少し速くなり、#41597でそのロジックをLoaderQueryで整理したということみたい」「#41385にはベンチマークも貼られていますね」「#41597や#41385は、アプリ開発者が普段それほど意識することはなさそうかな: 普通に使っていれば恩恵を得られそう」

SELECT "authors".* FROM "authors" WHERE "authors"."id" = ?
SELECT "authors".* FROM "authors" WHERE "authors"."id" = ?

#41385のフォローアップ。
このクラスはPreloader::Associationがそのレコードを読み込むのに使うクエリを表す。これは従来のgrouping_keyの概念を置き換えて、Preloader::Associationをクエリごとにグループ化することとそのレコードを読み込むクエリの実行の両方に使われる(これで.firstをうまい具合に回避できる)。
この変更で機能は変わらないはずだが、不要なscope.to_sql呼び出しを回避できるのでほんのわずか速くなるはず。
同PRより大意

🔗 Active Recordのstrict loadingの修正


つっつきボイス:「strict loadingは最近Railsに入った機能だったかな(ウォッチ20200302)」「strict loadingは昨年3月頃に入ってたんですね」

「なるほど、モデル全体にstrict_loading_by_defaultを設定したうえで、個別のリレーションにstrict_loadingを設定した場合に、前者の設定だけが効いてしまうバグだったのか」「あ、そういうことですか」「詳細度は個別のリレーションに設定するstrict_loadingの方が高いので、モデルにデフォルトを設定するstrict_loading_by_defaultより優先されるべきということなんでしょうね」

「モデルではデフォルトでstrict loadingをオンにするけど特定のリレーションでオフにしたいときは、プルリクのコード例のように書くでしょうね↓」「リレーションのstrict_loading: falseが効くはずが効いてなかったのか、なるほど」


従来は、モデルのstrict_loading_by_defaultをtrueに設定している状態で、strict_loadingをfalseに設定すると、strict loadingに関するraiseやwarningを行うかどうかの決定でこのfalseが考慮されていなかった。

class Dog < ActiveRecord::Base
  self.strict_loading_by_default = true

  has_many :treats, strict_loading: false
end

上の例では、strict_loadingをfalseにしているにもかかわらずdog.treatsがraiseされてしまう。このバグはActive Storage以外にも影響するバグなので、#41461(closed)よりも広範囲に渡るPRにした。自分はこの挙動に少々驚かされたので、この問題はすべてのアプリケーションで解決する必要がある。
#41461のテストと#41453の提案を元にいくつかの追加を行った。
同PRより大意

🔗 ドキュメント: メンテナンスポリシーに追記


つっつきボイス:「Railsガイドの更新です」「Railsのメンテナンスポリシーはちょうどこの間話題にしたような気がしますね」

「なるほど、セキュリティリリースのポリシーのこの部分が明文化されたのか↓」「stableブランチとの関係についても解説されてますね」「Railsを最近始めた人にはありがたい情報🙏」

セキュリティリリースには直接的なセキュリティパッチのみが含まれる。セキュリティパッチに起因する、セキュリティに関連しないバグの修正は、リリースのx-y-stableブランチで公開され、バグ修正ポリシーに基づいて新しいgemとしてのみリリースされる。
同PR更新部分より大意

🔗Rails

🔗 スライド: Ganbaranai wo ganbaru


つっつきボイス:「スライドのタイトルが2つあったので、埋め込みに出ていない短い方を見出しにしてみました」「がんばらないを頑張る」

「リリースを2段階に分けて、本番では通知だけ行って例外は握りつぶす↓、なるほど」「本番で動いているアプリのリリースではこういう感じでリリースすることもありますね」「いろいろ参考になりそう」

「このように本番で運用中のアプリをいかに障害を避けてリリースするかというノウハウは、ある程度運用の経験を積んでくるとありがたみがわかってきますね👍」「たしかに」

🔗 fast_blank: Active SupportのString#blank?を高速化するgem

SamSaffron/fast_blank - GitHub


つっつきボイス:「String#blank?をCで高速化するgemだそうです」「C拡張でやってるのか」「よく見るとfast_rubyを作ったのはSam Saffronさんでした」

# 同リポジトリより抜粋
================== Test String Length: 136 ==================
Calculating -------------------------------------
          Fast Blank   201.772k i/100ms
  Fast ActiveSupport   189.120k i/100ms
          Slow Blank   129.439k i/100ms
      New Slow Blank    90.677k i/100ms
-------------------------------------------------
          Fast Blank     16.718M (± 2.8%) i/s -     83.534M
  Fast ActiveSupport     17.617M (± 3.6%) i/s -     87.941M
          Slow Blank      3.725M (± 3.0%) i/s -     18.639M
      New Slow Blank      1.940M (± 4.8%) i/s -      9.702M

Comparison:
  Fast ActiveSupport: 17616782.1 i/s
          Fast Blank: 16718307.8 i/s - 1.05x slower
          Slow Blank:  3725097.6 i/s - 4.73x slower
      New Slow Blank:  1940271.2 i/s - 9.08x slower

「RubyのJITはこういう高速化に効きそうな気もしますけどね」「String#blank?は、Railsを使っててありがたいと思うRubyらしいメソッドだと思います」「そうですね」「PHPなどのように暗黙の型キャストをあてにして直接比較する方法よりも明示的に書けるのがいい👍」「オレオレblank?を定義しなくて済みますよね」

参考: Ruby 3.0のJIT変更解説 - Qiita

String#blank?はActive Supportのメソッドですが、Ruby本体に取り込まれてもよさそうな気がしました」「今のところRuby本体には取り込まれてないようですね」「Active SupportのメソッドがRuby本体に取り込まれることがたまにありますよね」「Active Supportのpresent?ならもうRubyに入っているかなと思ったけど入っていなかった」

後で調べると、to_procはActive SupportからRubyに取り込まれたそうです↓。

参考: 開発コアメンバが語るRubyの今とこれから(前編) - @IT

「ちなみにfast_blank gemは以下のgemから参照されていて知りました↓」「こちらはRustでString#blank?を高速化しているんですね」

malept/rusty_blank - GitHub

🔗 RuboCop Performance 1.10がリリース


つっつきボイス:「RuboCopがいくつかに分かれたうちのひとつがRuboCop Performanceですね」

rubocop/rubocop-performance - GitHub

Performance/RedundantSplitRegexpArgumentというcop(RuboCopのルール)が追加されてる↓」「区切り文字を,と指定するのに正規表現で/,/と書く必要はない、たしかに」「gsubなどのような文字列でも正規表現でも渡せるメソッドで、しかも完全一致を使うなら正規表現で書かなくてもいいんですよね」「そういえばgsubで無駄に正規表現を書いてしまったことありました😅」「文字列の方が正規表現よりも高速になるというのはRubyだとちょくちょくあります」

# 同記事より
# bad
'a,b,c'.split(/,/)

# good
'a,b,c'.split(',')

Performance/RedundantEqualityComparisonBlockというcopも@kamipoさんのリクエストで追加されていますね」「こちらは安全ではないとあるので-aオプションで自動修正されないヤツか」

# 同記事より
# bad
items.all? { |item| pattern === item }
items.all? { |item| item == other }
items.all? { |item| item.is_a?(Klass) }
items.all? { |item| item.kind_of?(Klass) }

# good
items.all?(pattern)

🔗 prosopite: false positive/negativeゼロをうたうN+1クエリチェッカー

charkost/prosopite - GitHub


つっつきボイス:「プロソパイト?」「造語かと思ったら鉱物の名前でした↓」

参考: Prosopite(プロソパイト)

「prosopiteは、よく使われているbullet gem↓のようなN+1クエリ検出gemなんですね」

flyerhzm/bullet - GitHub

「READMEに偽陽性と偽陰性がゼロって書かれてますけど、そんなことができるんですね」「N+1クエリが確実に発生している場合だけを検出するということは、bulletと検出方式が違うんでしょうね: 実際に発行されるSQLから検出するか、Rubyのコードから検出するかなど、方法はいくつか考えられます」「bulletでは検出できないN+1クエリも検出できるとも書かれてる」「確実に発生しているN+1クエリだけを検出できるならなかなかよさそう👍」

# 同リポジトリより: レコード作成で発生するN+1
FactoryBot.create_list(:leg, 10)

Leg.last(10).each do |l|
  l.chair
end

prosopiteはActive Support instrumentationを用いてすべてのSQLクエリを監視し、すべてのN+1クエリケースに存在する以下のパターンを検出します。
「同じコールスタックと同じクエリフィンガープリントが複数のクエリに存在する場合」
同リポジトリより

参考: Active Support の Instrumentation 機能 - Railsガイド


Rails: Bulletで検出されないN+1クエリを解消する

🔗 その他Rails


つっつきボイス:「自分はRailsアプリをこう書く、というZennの記事を見つけました」「どれが正解かとかではなく、自分はこういうときにはこう書く、というポリシーをこうやって見えるところに書いておくのはいいですね👍」

「Railsアプリをチームで開発するときに各メンバーの『自分はこういうときにこう書く』という嗜好が事前にわかっていると、それを踏まえた上で設計を議論できるので話が早くなる」「たしかに」「この人はこういう好みがあるから自分のこの設計にはたぶん反対するだろう、そしてそのうえでチームリーダーやプロジェクトの方針に合わせてもらう、というのは普段からよくやっています」

「長年一緒に組んで仕事をしてきたメンバーとならお互いの嗜好もわかっているのでいいんですが、新しく加わったメンバーだと設計の嗜好が最初のうちはなかなか見えてこないんですよ」「そうそう」「そうした部分が明文化されているとそのあたりが多少やりやすくなりそうですね」「こういう試みはもっと行われてもいいと思いました」


前編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210316後編)testdouble/standard gem、DockerfileベストプラクティスとDockerfileのlintツールhadolintほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。