- Ruby / Rails関連
週刊Railsウォッチ(20210607前編)ActiveRecord::Relationのone?とmany?が高速化、RubyKaigi Takeout 2021登壇者募集開始ほか
こんにちは、hachi8833です。RubyKaigi Takeout 2021の登壇者募集が始まりましたね。
CFP for RubyKaigi Takeout 2021 (the online version of RubyKaigi) is now OPEN! https://t.co/VeJ1Tv5iyr #rubykaigi
— RubyKaigi (@rubykaigi) June 2, 2021
🔗Rails: 先週の改修(Rails公式ニュースより)
Rails公式ニュースが久しぶりに更新されたので、その中からこれまで取り上げていなかったものを中心に拾いました。
🔗 ActiveRecord::Relation
のone?
とmany?
を高速化
# activerecord/lib/active_record/relation.rb#L296
def one?
return super if block_given?
- limit_value ? records.one? : size == 1
+ return records.one? if limit_value || loaded?
+ limited_count == 1
end
def many?
return super if block_given?
- limit_value ? records.many? : size > 1
+ return records.many? if limit_value || loaded?
+ limited_count > 1
end
つっつきボイス:「SELECT COUNT(*)
をやめてLIMIT 2
を使って高速化したようですね: 巨大テーブルのCOUNT
は遅いですけど、SELECT 1
したうえでLIMIT 2
にすると速いですよ」「お〜、ベンチマークも1300倍以上になったとありますね」「100M行ならこのぐらい差が出ますよ」「速度が欲しい人たちはこれまで同様の独自メソッドを書いていたかもしれませんね」
「LIMIT 2
で速くなるかどうかは状況にもよるかも: たとえばMySQLでSELECT COUNT(*) FROM table
のようにテーブル内の行をすべて取得するようなクエリを投げると、MySQLのinformation_schemaに既にある値を使うのでLIMIT 2
よりも速くなるかもしれませんね(訂正↓)」「それは速そう!」「LIMIT 2
より速くできる方法は他にもありそうですが、Active Recordでscoped chainされることを考えればこのPRの方法がよさそう👍」
訂正(2021/06/08)
MySQLのエンジンがInnoDBの場合はこのようにならないとのご指摘をいただきました(ツイート)。ありがとうございます!
参考: InnoDBのINFORMATION_SCHEMA TABLES Tableと実データのレコード数比率を出してみた - カイワレの大冒険 Third
「LIMIT 2
に加えてSELECT 1
も速くするのによく使われますね」「1ですか?」「1
というスタティックな値だけを返すという意味ですね: 定数であればよいので1でなくても構いません」「あ〜なるほど」「行データが不要で件数だけ欲しいようなときに使うことがあります」
参考: sql server 2008 - What does "select 1 from" do? - Stack Overflow
ActiveRecord::Relation
のone?
メソッドやmany?
メソッドは、背後でSELECT COUNT(*) FROM posts
のようなクエリを生成してから、結果が1に等しい(または1より大きい)かどうかを比較している。巨大テーブルや複雑な条件ではCOUNT
が非常に遅くなることがあるが、この場合は全レコード件数は完全に不要。それならLIMIT
を追加してSELECT COUNT(count_column) FROM (SELECT 1 AS count_column FROM posts LIMIT 2)
のようにするだけでずっと高速になる。実際any?
などのメソッドではLIMIT
を使っている。
なおこれはすべてのバージョンのRailsで発生している。
100M行のテーブルでいくつかベンチマークを取ってみると、スピードが1300倍以上増加した。
同PRより大意
🔗 ネストしたsecretsにメソッド名でアクセスできるようになった
- PR: Allow access to nested secrets by method calls by ghiculescu · Pull Request #42106 · rails/rails
つっつきボイス:「ネストしたsecrets自体は前からサポートされていた覚えがありますけど、今回はそれにメソッド名でもアクセスできるようにしたようですね」「なるほど」
参考: Rails 5.2 credentials cheat cheat -- ネストした値をフェッチする例が記載されています
Rails.application.credentials
のキーにネステッドアクセスできるようになった従来は
credentials.yml.enc
のトップレベルにあるキーにしかメソッド呼び出しでアクセスできなかったが、任意のキーに対してできるようになった。
たとえば以下のsecretsがあるとする。
aws:
access_key_id: 123
secret_access_key: 345
この改修で、
Rails.application.credentials.aws.access_key_id
がRails.application.credentials.aws[:access_key_id]
と同じものを返すようになった。
Alex Ghiculescu
Changelogより大意
🔗 ActionController::Live#send_stream
が追加
つっつきボイス:「send_stream
も見覚えある: 最近はsend_data
よりもなるべくsend_stream
を使おうみたいな話があったような気がしますね」
「既に生成されてファイルになったものを送信するのであればsend_stream
でもsend_data
でも変わらないんですが、以下のようにeach
で処理を回すものを送信するのであればsend_stream
の方が送信開始を早められます」「あ、send_data
だと処理が完了するまで送信できないのか」「そういうことですね: どちらも最終的に同じことをやれるのであれば基本的にsend_stream
を使う方がいいかなと思いました」「なるほど」
生成済みのストリームをより送信しやすくする
ActionController::Live#send_stream
を追加。
send_stream(filename: "subscribers.csv") do |stream|
stream.write "email_address,updated_at\n"
@subscribers.find_each do |subscriber|
stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
end
end
同PRより大意
🔗 ActiveStorage::Streaming
が切り出された
つっつきボイス:「DHHによる改修です」「streaming.rbがActive Storageから切り出された↓」「リファクタリングっぽいですね」「send_blob_stream
というメソッドもできた」
# streaming.rb
# frozen_string_literal: true
module ActiveStorage::Streaming
DEFAULT_BLOB_STREAMING_DISPOSITION = "inline"
include ActionController::Live
private
# Stream the blob from storage directly to the response. The disposition can be controlled by setting +disposition+.
# The content type and filename is set directly from the +blob+.
def send_blob_stream(blob, disposition: nil) #:doc:
send_stream(
filename: blob.filename.sanitized,
disposition: blob.forced_disposition_for_serving || disposition || DEFAULT_BLOB_STREAMING_DISPOSITION,
type: blob.content_type_for_serving) do |stream|
blob.download do |chunk|
stream.write chunk
end
end
end
end
Active Storageからストリーミングするコントローラを独自に作りたいのであれば、ストリーミングを適切に行えるメソッドがあると便利。そういうものが
ActiveStorage::BaseController
に切り出されていないまま既に存在していた。
同PRより大意
🔗 fresh_when
とstale?
でCache-Control
ヘッダーを上書きできるよう修正
stale(形容詞)新鮮でない、気の抜けた
つっつきボイス:「Cache-Control
ヘッダーにまた修正が入った(ウォッチ20201012、ウォッチ20210412)」
「コントローラのアクションでCache-Control
ヘッダーに積極的に情報を渡す手段が追加されたということみたいですね↓: Cache-Control
を直接書き変えずにfresh_when
メソッドやstale
メソッドのオプション経由で変えられるようにした」「なるほど」
# When overwriting Cache-Control header:
#
# def show
# @article = Article.find(params[:id])
# fresh_when(@article, public: true, cache_control: { no_cache: true })
# end
#
# This will set in the response Cache-Control = public, no-cache.
fresh_when
とstale?
にcache_control: {}
オプションを追加。
これらのメソッドでresponse.cache_control
を設定するショートカットとして使える。
Jacopo Beschi
同Changelogより大意
「クライアントキャッシュの制御はコントローラがやるものと考えれば、そのためのヘルパーメソッドがあるのは理にかなっていそうな気もする」「あ、Cache-Control
ヘッダーが制御するのは一瞬Railsサーバーのページキャッシュのような気がしたけど、クライアントキャッシュの方だったか」
「コントローラでCache-Control
ヘッダーを制御できるようになるのはいいことだと思う一方で、コントローラではサーバーのページキャッシュとクライアントキャッシュのどちらについても気にする場面があるので、どちらを制御しているかに気をつけないといけないかも」「それもそうですね」
参考: Cache-Control
- HTTP | MDN
参考: RailsにおけるCacheの概念と使い方 - Qiita
🔗Rails
🔗 PostgreSQLのRow-Level SecurityをRailsで使う(Ruby Weeklyより)
つっつきボイス:「PostgreSQLに行レベルのセキュリティ機能があるとは」「略してRLS」「ENABLE ROW LEVEL SECURITY;
で有効にできるのか」「どういうふうに使うのかな?」「なるほど、こんな感じでCREATE POLICY
でポリシーを記述するんですね↓」
-- 同記事より
CREATE POLICY transactions_app_user
ON transactions
TO app_user
USING (customer_id = NULLIF(current_setting('rls.customer_id', TRUE), '')::bigint);
参考: PostgreSQL: Documentation: 13: 5.8. Row Security Policies
「記事ではこれをRailsで使ってる↓」「やるな〜」
-- 同記事より
class CreateTransactions < ActiveRecord::Migration[6.1]
def change
create_table :transactions, id: :uuid do |t|
t.bigint :customer_id
t.text :description
t.bigint :amount_cents
t.timestamptz :created_at
end
# Grant application user permissions on the table (this migration should run as the admin user)
reversible do |dir|
dir.up do
execute 'GRANT SELECT, INSERT, UPDATE, DELETE ON transactions TO app_user'
end
dir.down do
execute 'REVOKE SELECT, INSERT, UPDATE, DELETE ON transactions FROM app_user'
end
end
# Define RLS policy
reversible do |dir|
dir.up do
execute 'ALTER TABLE transactions ENABLE ROW LEVEL SECURITY'
execute "CREATE POLICY transactions_app_user ON transactions TO app_user USING (customer_id = NULLIF(current_setting('rls.customer_id', TRUE), '')::bigint)"
end
dir.down do
execute 'DROP POLICY transactions_app_user ON transactions'
execute 'ALTER TABLE transactions DISABLE ROW LEVEL SECURITY'
end
end
end
end
「モデルのコードを見ると、コネクションの部分で'SET rls.customer_id = %s'
のようにRLSを設定してますね↓: PostgreSQLコネクションのセッション変数的な部分にこれを設定することで、クエリが適切かどうかをポリシーでチェックできるようになる感じかな」「複雑になるけどその分安心できそう」
# 同記事より
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
SET_CUSTOMER_ID_SQL = 'SET rls.customer_id = %s'.freeze
RESET_CUSTOMER_ID_SQL = 'RESET rls.customer_id'.freeze
def self.with_customer_id(customer_id, &block)
begin
connection.execute format(SET_CUSTOMER_ID_SQL, connection.quote(customer_id))
block.call
ensure
connection.execute RESET_CUSTOMER_ID_SQL
end
end
end
「RLSをセキュリティ上の防衛という観点から見た場合、PostgreSQLのRLSはあくまで生SQLの中で設定しているので、仮にセッションを乗っ取られて生SQLを実行されたら回避できないでしょうね」「それもそうか」
「自分なら、たとえば今度Rails 7に入る予定のモデル暗号化機能(ウォッチ20210412)みたいな暗号化用gemを使って、暗号化の鍵はアプリケーション側だけで持ち、PostgreSQLには暗号化済みのデータを保存する方が、生SQLされたときの対策としては有効かなと思いました」「あ〜なるほど」「もちろんPostgreSQLのRLSも、有効性の範囲を理解したうえでポリシー設定に使う分にはよいものだろうと思います👍」
「面白い機能ですね」「ぽすぐれならこういう機能があっても不思議じゃない」「記事の最後には、RLSを使う場合はパフォーマンスにも注意しようと書かれてますね」
🔗 Active Recordのnew
にはブロックも渡せる(Ruby Weeklyより)
つっつきボイス:「Active Recordのnew
やcreate
のような作成系メソッドは以前からこういうふうにブロックも渡せますね↓」「あ、そうでしたか」
# 同記事より
u = User.new do |user|
user.first_name = "Jordan"
user.last_name = "Knight"
end
「Rubyだと『こういう書き方、やってみたら動くかな?』と思いついてドキュメントも読まずにやってみると本当に動くことがちょくちょくありますけど、そういうノリでnew
にブロック渡ししてみたらできたという感じでしょうね」「たしかにRubyだとそれよくありますよね」「Ruby名物の『やったらできた』」
追いかけボイス:「同記事の後半では、Rubyのtap
メソッドを使った場合にブロック渡しの挙動が異なることについても触れられていますね↓」
# 同記事より
# tapにブロックを渡す場合
new_user = User.create(first_name: "Jordan", last_name: "Knight").tap do |u|
u.first_name = "Jonathan"
end
new_user.first_name #=> "Jonathan"
new_user.reload.first_name #=> "Jordan"
# createに直接ブロックを渡す場合
new_user = User.create(first_name: "Jordan", last_name: "Knight") do |u|
u.first_name = "Jonathan"
end
new_user.first_name #=> "Jonathan"
new_user.reload.first_name #=> "Jonathan"
参考: Object#tap
(Ruby 3.0.0 リファレンスマニュアル)
🔗 Hotwire記事2本
- 元記事: Hotwire: Reactive Rails with no JavaScript? — Martian Chronicles, Evil Martians’ team blog(Ruby Weeklyより)
- 元記事: まるでフロントエンドの“Rails” Hotwireを使ってJavaScriptの量を最低限に - ログミーTech
つっつきボイス:「1本目は今翻訳中のEvil Martiansの記事で、2本目は少し前のですがwillnetさんのHotwire記事です」「もしかすると自分が本当に欲しかったのはHotwireだったのかもしれない、という気持ちもある一方で、Turboがね...Turboを使うかどうかまだ悩み中なんですよ」「そこですよね」「Evil Martiansの記事は、いい意味で古き良き時代のWeb開発に戻れるよと言いつつ、既存Railsアプリに導入するとそこそこ直しが必要だったようです」「HotwireとRails本体の相性についてはあまり心配していませんが、むしろRails以外のものとHotwireとのインテグレーションが気になっています」「あ、そうか!」「いずれにしろHotwireについてはもう少し調べておかないといけないでしょうね」「たしかに」
🔗 その他Rails
つっつきボイス:「小ネタですが、過去のRails gemがインストールされていれば、こんなふうにバージョン番号の前後にアンダースコア_
を付けるとバージョン指定できるそうです↓」「そうそう、Railsにはこんなオプションが隠れていますね」「たまにとても欲しくなりそうな機能」
# 同記事より
# Create Rails 6.1 project
$ rails _6.1.3_ new rails6_1
# Create Rails 6.0 project
$ rails _6.0.3_ new rails6_0
# Create Rails 5.2 project
$ rails _5.2.3_ new rails5.2
前編は以上です。
バックナンバー(2021年度第2四半期)
週刊Railsウォッチ(20210601後編)Python使いから見たRuby、MySQLのインデックス解説、GitHubが採用したOpenTelemetryほか
- 20210531前編 RailsConf 2021の動画が公開、GraphQLのN+1を自動回避、Ruby 3のJITとRailsほか
- 20210525後編 Rubyのオブジェクトアロケーション改善、RubyKaigi Takeout 2021開催日発表、AWS App Runnerほか
- 20210524前編 Active Supportの知られてなさそうな機能5つ、RSpecの歴史、書籍『Practicing Rails』ほか
- 20210518後編 RubyのGCを深掘りする、Psych gemのbreaking change、11月のRubyConf 2021ほか
- 20210517前編 Bootstrap 5リリース、productionでSQLiteがwarning表示、rails-ujsの舞台裏ほか
- 20210511後編 AWS Lambda関数ハンドラをDSLで書けるyake gem、VPC Peeringが同一AZ転送量無料化ほか
- 20210511後編 AWS Lambda関数ハンドラをDSLで書けるyake gem、VPC Peeringが同一AZ転送量無料化ほか
- 20210510前編 属性メソッドをキャッシュして最適化、Railsのガバナンスに関する声明、bundle install高速化ほか
- 20210427後編 RactorでUDPサーバーを作る、JSONシリアライザalba gem、AppleのAirTagほか
- 20210420後編 ShopifyのJITコンパイラYJIT、PicoRuby、DynamoDBの3つの制約ほか
- 20210419前編 RailsのN+1クエリを定番以外の方法で修正する、GitLabのセキュリティ修正リリースほか
- 20210413後編 RubyMineのRBSサポートとCode With Me、GitHub ActionとDockerレイヤキャッシュほか
- 20210412前編 Active Record属性暗号化機能がRails 7にマージ、RailsNew.ioでrails newオプションを生成ほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)