- Ruby / Rails関連
週刊Railsウォッチ(20210322前編)Active Recordのstrict loadingの修正、セキュリティリリースのポリシー追加、N+1チェッカーprosopite gemほか
こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
以下のコミットリストから見繕いました。コミットは少なめで、Changelogの変更はありませんでした。
🔗 Active Recordでprecisionなしのnumericalityバリデーションを修正
#32852で追加されたテストケースはActive Modelではパスするが、#38210で
Float::DIG
がBigDecimal.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::DIG
とBigDecimal.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の修正
- PR: Handle false in relation strict loading checks by eileencodes · Pull Request #41688 · rails/rails
つっつきボイス:「strict loadingは最近Railsに入った機能だったかな(ウォッチ20200302)」「strict loadingは昨年3月頃に入ってたんですね」
- PR: Add `strict_loading` mode to optionally prevent lazy loading by eileencodes · Pull Request #37400 · rails/rails
- API:
strict_loading
-- ActiveRecord::QueryMethods
「なるほど、モデル全体に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
つっつきボイス:「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?
を定義しなくて済みますよね」
「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?
を高速化しているんですね」
🔗 RuboCop Performance 1.10がリリース
つっつきボイス:「RuboCopがいくつかに分かれたうちのひとつがRuboCop Performanceですね」
「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クエリチェッカー
つっつきボイス:「プロソパイト?」「造語かと思ったら鉱物の名前でした↓」
「prosopiteは、よく使われているbullet gem↓のようなN+1クエリ検出gemなんですね」
「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
- 元記事: Only My Rails Way
つっつきボイス:「自分はRailsアプリをこう書く、というZennの記事を見つけました」「どれが正解かとかではなく、自分はこういうときにはこう書く、というポリシーをこうやって見えるところに書いておくのはいいですね👍」
「Railsアプリをチームで開発するときに各メンバーの『自分はこういうときにこう書く』という嗜好が事前にわかっていると、それを踏まえた上で設計を議論できるので話が早くなる」「たしかに」「この人はこういう好みがあるから自分のこの設計にはたぶん反対するだろう、そしてそのうえでチームリーダーやプロジェクトの方針に合わせてもらう、というのは普段からよくやっています」
「長年一緒に組んで仕事をしてきたメンバーとならお互いの嗜好もわかっているのでいいんですが、新しく加わったメンバーだと設計の嗜好が最初のうちはなかなか見えてこないんですよ」「そうそう」「そうした部分が明文化されているとそのあたりが多少やりやすくなりそうですね」「こういう試みはもっと行われてもいいと思いました」
前編は以上です。
バックナンバー(2021年度第1四半期)
週刊Railsウォッチ(20210316後編)testdouble/standard gem、DockerfileベストプラクティスとDockerfileのlintツールhadolintほか
- 20210309後編 RubyのIRBに隠れているイースターエッグ、Power Automate Desktop、SQLクエリのありがちなミス6つほか
- 20210303後編 Bundlerのセキュリティ修正、Rubyのガベージコレクション記事、Rubyが2/24に誕生日ほか
- 20210222 ActiveRecord::Relationの新メソッドload_asyncとexcluding、Active Jobのperform_laterの改善ほか
- 20210209後編 Rubyでミニ言語処理系を作る、Kernel#getsの意外な機能、CSSのcontent-visibilityほか
- 20210208前編 Rails次期リリースがバージョン7に決定、thoughtbotのアプリケーションセキュリティガイドほか](/hachi8833/2021_02_08/103801)
- 20210202後編 Ruby 3 irbのmeasureコマンド、テストを関数型言語のマインドセットで考えるほか
- 20210201前編 Webpackerのガイドがマージ、RailsはRuby 3でどのぐらい速くなったかほか
- 20210126後編 Google Cloud FunctionsがRubyをサポート、Ruby 3のパターンマッチングでポーカーゲームほか
- 20210125前編 Railsリポジトリのデフォルトブランチがmainに変更、Rails 6.1はMySQLのENUM型に対応済みほか
- 20210120後編 Ruby 3.0の新機能で遊ぶ、RubyスニペットをJSに変換するRuby2JS、rspec-parameterized gemほか
- 20210113後編 Ruby 3.0 Ractor解説記事、Vercelホスティングサービス、教育用OS xv6ほか
- 20210112前編 Active Recordの範囲指定バリデーション改善、soleとfind_sole_byメソッド、AlgoliaとRailsほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)