- Ruby / Rails関連
週刊Railsウォッチ(20210419前編)RailsのN+1クエリを定番以外の方法で修正する、GitLabのセキュリティ修正リリースほか
こんにちは、hachi8833です。RailsConf 2021をすっかり見逃してました😇。
We must and will do better in the future. pic.twitter.com/qYTF7ljUX5
— RailsConf (@railsconf) April 9, 2021
つっつきボイス:「RailsConf 2021、そういえば開催時期だったか」「つっつき中の今日(04/15)がラス日でした🙇」「キーノートのページにコミットでよく見かける顔が並んでますね」
- サイト: RailsConf 2021
- セッション: Sessions | RailsConf 2021 - 60件
- ワークショップ: Workshops | RailsConf 2021 -- 11件
「RailsConf 2021のfaqを見た感じでは有料イベントのようですね」「あ、そうでしたか」「参加者は後で動画を見られるそうですが、一般公開するとは書かれてない: たしかに有料でしかもリモート開催のイベントの動画を後で無料公開したら有料で登録する人がほとんどいなくなってしまいますよね」「それもそうか...」「スライドだけでも見られればと思ったけどしょうがないですね」
以下はつっつき後に見つけたツイートです。
Omg! I got a selfie with @tenderlove at #RailsConf!!! pic.twitter.com/8CHdDelvYZ
— Aaron Patterson (@tenderlove) April 15, 2021
🔗Rails: 先週の改修(Rails公式ニュースより)
今回は以下のコミットリストのChangelogを中心に見繕いました。
🔗 エラーページのCSSとアクセシビリティを改善
つっつきボイス:「developmentモードで表示されるエラーページのスタイルをインライン(style=
による直書き)ではなくちゃんとCSSのクラスで書き直したのか↓」「開発中によく見かける赤いエラーページですね」
# actionpack/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb#L13
<% corrections.each do |correction| %>
- <li style="list-style-type: none"><%= h correction %></li>
+ <li class="correction"><%= h correction %></li>
<% end %>
「<div>
タグも、意味を表せるHTML5の<main>
タグに置き換えてる↓」「考えてみれば、developmentモードのエラー画面をスクリーンリーダーで読む人がいる可能性もありそう」「だからアクセシビリティ改善なんですね」
# actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb#L4
-<div id="container">
+<main role="main" id="container">
<h2>To allow requests to <%= @host %>, add the following to your environment configuration:</h2>
<pre>config.hosts << "<%= @host %>"</pre>
-</div>
+</main>
参考: HTML: アクセシビリティの基礎 - ウェブ開発を学ぶ | MDN
「ところで、accessibilityをa11yって略さなくてもいいんじゃないかしら」「i18n(internationalization)やm17n(multilingualization)ぐらい長ければ略すのもやむなしな気もしますけど」「Kubernetesも大して長くないけどK8sと略されたりしますね」
🔗 ActiveSupport::Duration#parts
のハッシュをfreeze
つっつきボイス:「parts
が返すハッシュをfreeze
して返すことで高速化したらしい」「ベンチマークでも速くなってますね」「ライター系メソッドも削除したのか」「freeze
したんだからライター系メソッドはあったらマズいでしょうね」「あ、たしかに」
# activesupport/lib/active_support/duration.rb#L212
- def initialize(value, parts) #:nodoc:
+ def initialize(value, parts, variable = nil) #:nodoc:
@value, @parts = value, parts
@parts.reject! { |k, v| v.zero? } unless value == 0
+ @parts.freeze
+ @variable = variable
+
+ if @variable.nil?
+ @variable = @parts.any? { |part, _| VARIABLE_PARTS.include?(part) }
+ end
+ end
+
+ # Returns a copy of the parts hash that defines the duration
+ def parts
+ @parts.dup
end
「Active SupportのDurationは処理によっては利用頻度が高そうなので、高速化されるのはありがたい🙏」
「DateやTimeの差からDurationを得られるのは便利ですよね」「他の言語だといったんmsecに変換するとかいろいろ面倒になりがちですが、ActiveSupport::Duration.build(time1 - time2)
のようにするとDurationが得られますね」
- Rails API:
ActiveSupport::Duration
「もっとも、たとえばActiveSupport::Duration.build(time1.beginning_of_day - time2.beginning_of_day)
がサマータイム切り替わりをはさんだときに常に日単位になってくれるかどうかは解釈によっては難しいですよね: 1日が23時間や25時間でも1日と解釈するのか、それともあくまで1日=24時間として解釈するのか、とか」「う、そうですね😅」「日付は難しい...」
DurationはValue Objectであり、ミューテーションされるべきではない。
この変更によって、Durationが変数であるかどうかにかかわらずキャッシュできるようになり、ActiveSupport::Duration
の+
メソッドや-
メソッドのパフォーマンスが改善される。さらにany?
やインラインのarray定義を避けられるので、arrayが2個アロケーションされるのも回避できる。アプリ全体のパフォーマンスが著しく改善されるとまではいかないが、duration_of_variable_length?
メソッドは2.5倍速くなる。
Warming up --------------------------------------
current 325.663k i/100ms
new 828.177k i/100ms
Calculating -------------------------------------
current 3.274M (± 2.9%) i/s - 16.609M in 5.076744s
new 8.337M (± 1.9%) i/s - 42.237M in 5.067887s
Comparison:
new: 8337279.2 i/s
current: 3274297.7 i/s - 2.55x (± 0.00) slower
parts=
メソッドとvalue=
メソッドはDurationで決して使うべきではないので、削除した。さらに
parts
がfrozen hashのコピーを返すようにし、内部でdup
を使わずにpartsにアクセスできるよう_parts
リーダーも追加した。
同PRより大意
🔗 ActiveSupport::TimeZone#utc_to_local
を修正
utc_to_local_returns_utc_offset_times
がfalseでtimeインスタンスに小数の秒がある場合、Time.utc
コンストラクタが小数秒の値ではなくusec(マイクロ秒)の値を取るため、新しいUTC timeインスタンスが1,000,000のファクターでずれていた。
Changelogより大意
つっつきボイス:「usecって何だったかなと思ったらマイクロ秒(μsec)の簡易表記なんですね」「そうそう」
「ローカルのシステムはマイクロ秒も扱えたと思うので、手元のWSLのコンソール(Ubuntu 20)でdate
コマンドやってみよう」(しばし操作)「date
にナノ秒の書式(%N
)があったのでナノ秒まで出せた↓(マイクロ秒の書式は見当たらず)」
$ date +"%Y-%m-%d %H:%M:%S.%N"
2021-04-15 20:20:15.172406400
「で、従来はTime.utc
で小数点以下の値が小数秒として扱われていなかったので* 1_000_000
を追加して修正したようですね↓」「このバグよく見つけたな〜」
# activesupport/lib/active_support/values/time_zone.rb#L513
def utc_to_local(time)
tzinfo.utc_to_local(time).yield_self do |t|
ActiveSupport.utc_to_local_returns_utc_offset_times ?
- t : Time.utc(t.year, t.month, t.day, t.hour, t.min, t.sec, t.sec_fraction)
+ t : Time.utc(t.year, t.month, t.day, t.hour, t.min, t.sec, t.sec_fraction * 1_000_000)
end
end
🔗 Rack::Runtimeミドルウェアが非推奨化
- PR: Remove Rack::Runtime and deprecate referencing it by SkipKayhil · Pull Request #41935 · rails/rails
Rack::Runtime
はFakeRuntime
に置き換えられる。FakeRuntime
はリクエストを渡すだけのダミーのミドルウェアで、ミドルウェア操作では利用できない。- ミドルウェア操作(相対的な
insert
やmove
など)でRack::Runtime
を使うとFakeRuntime
を使うようdeprecation warningを表示する。- アプリケーションが(
use
やunshift
などで)Rack::Runtime
を明示的に追加する場合はdeprecation warningは表示されず、FakeRuntime
は無視される。- Railsガイドは
Rack::Runtime
を参照しないよう更新される。
同PRより大意
# actionpack/lib/action_dispatch/middleware/stack.rb#L6
module ActionDispatch
class MiddlewareStack
+ class FakeRuntime
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ @app.call(env)
+ end
+ end
+
つっつきボイス:「Rack::Runtime
って何をするんだろう?」「このissue↓を見ると、HTTPのX-RUNTIME
ヘッダーを追加するミドルウェアみたいですね」「X-RUNTIME
でググると結構古い記事が出てきた」「元々デバッグ用なのかも?」
「X-RUNTIME
はサーバーの処理にかかった時間を返すんですね」「今まではRack::Runtime
でX-RUNTIME
を返してたけど、X-RUNTIME
が必要な人はほとんどいなさそうだし、セキュリティ上の懸念もあるので削除しようという流れっぽい」「こんなふうにRack::Runtime
に依存したコンフィグを書いてる↓人がいる可能性もあるので、いったんdeprecationをはさんでから削除するんでしょうね」
# #38412コメントより
config.middleware.insert_before(::Rack::Runtime, SomeCoolMiddleware.new)
参考: x-runtime は消すべきなのか - Qiita
「このプルリクを見なかったらRack::Runtime
を知ることは一生なかったかも😆」「X-RUNTIME
を使う人って相当マニアックな感じしますね」「New RelicのようなアプリケーションメトリクスツールならX-RUNTIME
ヘッダーを使うことがあるかもしれませんね」「あ、ありそう!」
参考: New Relic | パフォーマンス分析プラットフォーム
🔗 番外: Webpackerがいつの間にかv6.0.0.betaに
つっつきボイス:「Evil Martiansの以下の記事↓を翻訳しながらチェックしていたら、Webpacker 6の開発が思ったより進んでいることに気づいて、今記事を書きかけています(公開済み)」
「Webpacker 5がリリースされたのは昨年ぐらいでしたっけ?」「リリースタグを見ると2020年3月だから1年前ぐらいですね」「早いな〜」「Webpacker 5から6への移行がそんなに手間でなければ大丈夫かなと思いますけど」「でも現時点の6.0アップグレードガイド↓を見ると割と変更点あるようです」「あ〜、source_path
とかも変わるのか」「コンフィグ洗い替えが必要っぽいですね」「Webpacker 5を頑張って導入してカスタマイズしていたら大変そう...」「容赦なき変更」
つっつき後、以下の記事に現状をメモしました↓。
🔗Rails
🔗 マネーフォワードのマイクロサービス化インタビュー記事
つっつきボイス:「マネーフォワードの中の人へのインタビューが本当に生々しかったので取り上げてみました」「なるほど、現在はリクエストの途中にRailsをはさんでいるのを将来Goで直接処理しようとしてるんですね」
中出: まず前提として先ほど申し上げたように、まずRuby on Railsでできた巨大なSaaSがありまして、それを今マイクロサービスに置き換えている最中です。現在のアーキテクチャとしては、ユーザーのリクエストがRailsに届き、それがGoでできたマイクロサービスを呼び出すという形になっています。将来的には、Railsを通さずに直接Goのマイクロサービスを呼び出す形に移行していくことを考えています。
同記事より
「マイクロサービス化はまだ始まったばかりだそうです」「マネーフォーワードのようにお金に関連するサービスだと大変そう」「マネーフォワードのサービス内容を考えると、周辺機能はともかくコア機能はマイクロサービスにしにくそうな気がしますね: 機能を横断しないと取れないものはマイクロサービス化が難しくなる」「そうですよね...」
「ところで、ひと頃のマイクロサービス化しようぜみたいな話も最近だいぶ落ち着いてきた感じはありますね」「言われてみれば前より静かになったかも」「表に出ていない部分も含めて、マイクロサービス化に挑戦して失敗したところは結構あるんじゃないかな」
「もしかすると本当に欲しかったのはマイクロサービスじゃなくて『多少モジュラライズされたモノリス』だったのかも」「気持ちわかります」「巨大モノリスのままだとつらいのは確かなので、モノリスにマイクロサービスのパラダイムを一部取り入れることで、モノリスを適切に分割するときの設計上の参考にするのはありだと思いますし、上の記事はそういうときに参考になりそうですね👍」
つっつきの後で、以下の「シタデルアーキテクチャ」をやっと思い出しました↓。
🔗 rollout gemで「フィーチャーゲート」を実現する
つっつきボイス:「Engine Yardの記事です」「フィーチャーゲートはいわゆるフィーチャーフラグ的な機能かな: この機能はいろんな呼び方があるんですよ」「あ、そうなんですね」
「ちなみに、ちょうどこの構成によく似た案件をこれから始めるところです↓」「へ〜」「RailsのAPIをURLベースでカレントから新しいものに切り替える感じで、よく行われる作業ですね」
「そういえば自分も今Rails 4の案件やってます」「Rails 4ぐらい古いものをアップグレードする場合、小さなアプリだったら頑張って少しずつRails 6までアップグレードするより、rails new
してそっちにコードを移す方が早いんじゃないかって思いますね」「そうそう、少しずつアップグレードするのって意外としんどいですよね」「その途中でテスト系のgemがつかえたりするとアプリ本来の挙動と関係ないところで力を使わないといけなくなってつらい」
「でもアプリが大きいとなかなかそうもいかないので、順当に少しずつアップグレードすることになりますけど」「手間はかかりますけど、ステップバイステップでアップグレードする方が明らかに安全ですよね」「進捗も出しやすいですし」「rails new
してコードをコピペする方式だと、完全に移し終わるまで全体をテストできないんですよね」「コードベースが大きいとリスクが高すぎる」「そういうことがありうるので巨大モノリスにするのは避けたい気持ちがあります」
🔗 .count
でRailsアプリが落ちることがある(Ruby Weeklyより)
つっつきボイス:「.count
でそんなに遅くなるものかな?」「あ、テーブルの行数がでかいのか」「30秒経過してもクエリが返ってこないのはつらそう」「クエリが返るまで数分かかるRedmineの対応したことならありますよ」「お〜」「なかなか最適化しがいのあるクエリでした😆」
「特定のクエリだけがものすごく遅い場合なら、頑張って最適化すればたいてい速くできますし、結果の整合性も取りやすいですね」「お〜」「逆に、複雑にテーブルをまたぐようなN+1クエリを手動で定数回に最適化する方がしんどい: 数百msecのクエリが数百件とか」「たしかに」「N+1の最適化は油断すると結果を変えてしまうことがあるんですよ」
「記事の結論は、.count > 0
や.count.positive?
をもっと速いメソッドに置き換えましょうということか」「遅いクエリの最適化は頑張りが大事」「たしかActiveRecord::Relation
のempty?
はレコードが1件あるかどうかがわかればいいので、COUNT()を使わずLIMIT 1を使った実装になっていて速かった覚えがありますね」「なるほど」
参考: 【Ruby on Rails】ActiveRecordのexists?の逆はempty?を使う - Qiita
🔗 N+1クエリを定番以外の方法で修正する
つっつきボイス:「ちょうどN+1クエリの記事です」「non standardなN+1クエリ修正ですか」
「ちなみに最初の見出し『N+1 queries 101』の101は、英語圏では『入門』の意味ですね」「そうそう、海外の大学のシラバスだと最初に履修する授業の番号が101になってることが多い」「へ〜、101ってそんな意味なんですね」
# 同記事より
# app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
def author_name
user.name
end
end
# app/models/user.rb
class User < ApplicationRecord
has_many :posts
end
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
@posts = Post.published
end
end
「入門の次は、Scout APM↓を使ってメトリクスを取ってる」「APM(Application Performance Monitoring)ツールはN+1クエリの検出には有効ですね: N+1クエリが確実に発生していて、かつ実際に遅くなっていることを確かめてから対策する方がいい👍」「そうですね」「N+1クエリが発生していても実用上問題ない速度が出ていれば、結果が変わってしまう可能性のある更新を行わずに済むときもあります」
「方法その1は、Active Recordのキャッシュを作って手動でヒットさせる方式↓」「たしかにRailsだとnon standard」「この方法が有効なケースは、限定的ですがたしかにあります: SQLでJOINを駆使してもきれいに取れないケースがたまにあるんですが、そういうときはこのように展開済みの一時的なハッシュを作ったりしますね」「なるほど!」「これをRedisに入れられればリクエストをまたいで共有できるので便利です」
# 同記事より
class PostsController < ApplicationController
def index
@posts = Post.published.includes(:comments)
@users = User.active
@users_cache = @users.reduce({}) do |agg, user|
agg[user.id] = user
agg
end
end
end
「Rails標準の方法だとeager loadingするんですが、それが効かないケースにもこういう書き方をすることがありますね」
「方法2は、引数や戻り値をオブジェクトではなくプリミティブな値にする ↓: スコープでpost
オブジェクトを渡すとpost.user.id
の部分でクエリが発生するので、生のuser_id
を受け取る方がいい、たしかに」「あ〜、なるほど」「効くかどうかは内部の処理にもよりますし、呼び出し方も変えないといけないので、効果を確かめてから使う方がいいと思います」
# 同記事より
class Post < ApplicationRecord
scope :by_author, -> (user_id) {
where(user_id: user_id)
}
end
「方法3はリレーションシップのショートカットを作る」「この手法もときどき使いますね: リレーションを普通にたどるとuser.account.activity.last_active_at
のように長くなりますけど、Userのid
をActivityにも置くことで以下のようにactivity.last_active_at
と書けるようにする↓」
# 同記事より
class Post < ApplicationRecord
belongs_to :user
belongs_to :activity
def author_last_active_at
activity.last_active_at
end
end
「正規化するならuser.account
をたどればUserのid
を取れるのでActivetyにUserのid
を置く必要はないんですが、置くことで参照の距離を縮めるという発想ですね」「お〜、なるほど!」
「マルチテナントのアプリケーションなどでこの方法を使うことがあります: テナントのid
は利用頻度が高いんですが、きれいに正規化すると距離が離れてしまってJOINを何度も行わないと取れなくなるので、テナントのid
を途中のテーブルにも持たせたりします」「記事には『やりすぎ注意』とありますね」「そう、この方法だと正規化を崩すことになるので、データ不整合が発生したら悲惨なことになります😇」「たしかに」「あと、外部キーを付けすぎるとINSERTが重くなる可能性もあります: いずれにしろ十分検討してからにしましょう」
「方法4の『リレーションシップのデータ複製』もよく使いますね: マテリアライズドビューあたりがこれに近い方法かな」「なるほど」「テーブルパーティショニングもこの一種でしょうね: 直近1か月のデータだけ別テーブルに切り出してすぐ取り出せるようにするとか」「ログデータなんかもそうやったりしますね」
参考: マテリアライズドビュー(マテビュー)とは - IT用語辞典 e-Words
参考: 5.11. テーブルのパーティショニング
「記事タイトルはnon standard wayとあるけど、これはRailsの標準ではないというだけで、Railsに限らない一般的なN+1クエリの解決ではどれも普通に使われる方法だと思います」「あ、そういうことでしたか」「むしろRailsでN+1解決というとすぐeager loadingの話になる方が一般的でないんじゃないかな」「まあたしかに😅」「記事で紹介されている方法は少なくともバッドプラクティスではありませんね: いい記事👍」
🔗 GitLabのセキュリティ修正がリリース
つっつきボイス:「さっきHacklinesに流れてたので取り上げました」「あ、ログインユーザーが画像アップロードしてコード実行できるのは結構エグい!社内GitLabサーバーにも赤ランプ点灯してるし↓、週末に適用しなきゃ」
つっつき後の日曜日に無事アップデート完了しました。
前編は以上です。
バックナンバー(2021年度第1四半期)
週刊Railsウォッチ(20210413後編)RubyMineのRBSサポートとCode With Me、GitHub ActionとDockerレイヤキャッシュほか
- 20210412前編 Active Record属性暗号化機能がRails 7にマージ、RailsNew.ioでrails newオプションを生成ほか
- 20210407後編 エイプリルフールのRuby構文プロポーザル、AWSのVPC Reachability Analyzerほか
- 20210406前編 GitHubが修正したRailsセッションハンドリングの競合、erb/haml/slimの速度比較ほか
- 20210330後編 Active Recordモデル属性暗号化が標準で入る可能性、Flipper Cloud、awesome_printほか
- 20210329前編 特集: Rails更新版の臨時リリースとmimemagic gemのGPL問題
- 20210323後編 GitHub Actionsで使えるruby/setup-ruby、中高生国際Rubyプログラミングコンテスト2020ほか
- 20210322前編 Active Recordのstrict loadingの修正、セキュリティリリースのポリシー追加、N+1チェッカーprosopite gemほか
- 20210316後編 testdouble/standard gem、DockerfileベストプラクティスとDockerfileのlintツールhadolintほか
- 20210315前編 Active Recordのenum関連改修、Active SupportのEnumerableでpluckが使えるほか
- 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ウォッチタグ)