- Ruby / Rails関連
週刊Railsウォッチ(20201214前編)Rails 6.1の直近コミットを見る、RuboCop Rails 2.9リリース、ar_lazy_preload gemほか
こんにちは、hachi8833です。Rails 6.1がリリースされましたね。
- 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
- 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
- お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇
⚓Rails: 先週の改修(Rails公式ニュースより)
今回はいつもと趣向を変えて、6-1-stableブランチの直近のプルリクを中心に見繕いました。したがって、以下のいずれのプルリクも6.1にマージされています。
- 6-1-stableコミットリスト: Commits · rails/rails
⚓ 6.1: バリデーション時のstrict loadingを修正
strict loadingはバリデーション時にエラーを出して欲しくない(バリデーションのためにレコードを読み込む必要があるため)。
今回の変更では、owner.validation_context
をチェックするようにした。これがnil
の場合は、createやupdateでオブジェクトを現在バリデーションしていないことがわかる。それ以外の値が設定されている場合はバリデーション中なので、strict loadingでraise
をスキップしたい。
同PRより大意
つっつきボイス:「validation_context
とは...?」「なるほど、strict loadingを有効にすると、バリデーションの実行中にエラーになったときにraiseされてしまっていたので、バリデーション中はエラーを投げないようにしたということのようですね」
# activerecord/lib/active_record/associations/association.rb#L212
private
def find_target
- if owner.strict_loading?
+ if owner.strict_loading? && owner.validation_context.nil?
Base.strict_loading_violation!(owner: owner.class, association: klass)
end
- if reflection.strict_loading?
+ if reflection.strict_loading? && owner.validation_context.nil?
Base.strict_loading_violation!(owner: owner.class, association: reflection.name)
end
「テストコードを見ると、AuditLogRequiredモデルのrequired: true
がバリデーションに失敗したときにralse
しないことをチェックしている↓」
# activerecord/test/cases/strict_loading_test.rb#L89
def test_strict_loading_is_ignored_in_validation_context
with_strict_loading_by_default(Developer) do
developer = Developer.first
assert_predicate developer, :strict_loading?
assert_nothing_raised do
AuditLogRequired.create! developer_id: developer.id, message: "i am a message"
end
end
end
「このプルリクの日時を見ると、わずか15時間前(つっつき時点)にマージされてたんですね」「Rails 6.1がリリースされる直前じゃないですか」「間に合ってよかった🎉」
⚓ 6.1: I18n.translate
でキーがStringの場合に対応
- PR: Convert translation key to string as necessary by jonathanhefner · Pull Request #40773 · rails/rails
# actionview/lib/action_view/helpers/translation_helper.rb#L70
def translate(key, **options)
return key.map { |k| translate(k, **options) } if key.is_a?(Array)
+ key = key.to_s unless key.is_a?(Symbol)
alternatives = if options.key?(:default)
options[:default].is_a?(Array) ? options.delete(:default).compact : [options.delete(:default)]
end
つっつきボイス:「translate
のキーがシンボルでなくStringの場合も対応するようになったのね」「前はStringでもできてたような気がしたけど、#39989のコメントを見ると、そこでパフォーマンスを改善したときにto_s
を外したことでキーがたとえばInteger
の場合にうまくいかなくなると指摘されているので、それを受けて上の#40773でキーがシンボルでない場合にも対応できるよう修正したという流れのようですね」「なるほど!」「translate
のキーに数値を入れることは実際にはあまりなさそうですけどね」
「要するにシンボルでなかったら数値でも何でも文字列にしとけば大丈夫と」「Rubyのto_s
はObjectクラスにあるので、Objectクラスを継承するオブジェクトは必ずto_s
で文字列に変えられますよね」
ドキュメント: Object#to_s
(Ruby 2.7.0 リファレンスマニュアル)
⚓ 6.1: Ruby 3.0のStringの挙動修正に対応
Ruby 3では、Stringクラスのメソッドがサブクラスのインスタンス上で呼び出されたときにStringクラスのメソッドが常にStringのインスタンスを返すという非互換の変更が導入された。
https://bugs.ruby-lang.org/issues/10845
ruby/ruby#3701これはStringのサブクラスである
ActiveSupport::SafeBuffer
にわずかに影響するので、SafeBuffer#[]
やSafeBuffer#*
でRuby 2の振る舞い(別のSafeBufferインスタンスを返す)をRuby 3でも維持するパッチを用意した。なお、Ruby 3.0で変更されたメソッドのほぼすべては、既にSafeBufferでStringを返すためのオーバーライドが完了しているので、このテストをパスするために必要なパッチはこの2つだけ。
同PRより大意
つっつきボイス:「@amatsudaさんによるプルリクです」「Ruby 3.0の足音が聞こえてきそう」「修正の経緯について#10845に書かれているみたい↓」
「まず、従来のRubyではStringを継承したクラスが返すオブジェクトが以下の*
と+
と%
のように不揃いだった↓のが、Ruby 3.0でStringを返すように統一された」
# 10845より
class MyString < String
end
MyString.new("foo").*(2).class #=> MyString
MyString.new("foo").+("bar").class #=> String
MyString.new("%{foo}").%(foo: "bar").class #=> String
「RailsのSafeBufferは以下のようにStringを継承しているので、Ruby 2.xまではSafeBufferを返していたメソッドがRuby 3.0ではStringを返すように変わってしまったということか」
# activesupport/lib/active_support/core_ext/string/output_safety.rb#133
module ActiveSupport #:nodoc:
class SafeBuffer < String
...
def [](*args)
if html_safe?
- new_safe_buffer = super
+ new_string = super
- if new_safe_buffer
- new_safe_buffer.instance_variable_set :@html_safe, true
- end
+ return unless new_string
+ new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string)
+ new_safe_buffer.instance_variable_set :@html_safe, true
new_safe_buffer
else
to_str[*args]
end
end
...
def *(*)
- new_safe_buffer = super
+ new_string = super
+ new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string)
new_safe_buffer.instance_variable_set(:@html_safe, @html_safe)
new_safe_buffer
end
「それを上のように、返すものがSafeBufferでない場合はSafeBuffer.new
することでRuby 2.xと3.0で挙動が変わらないよう三項演算子で修正したんですね」「あ、なるほど!」「Ruby 2.xではSafeBufferを返すのでこれまでと同じ挙動になる」「Rubyのバージョンをチェックするif
文を書かずに挙動を揃えているのがうまいですね😋」
後で以下の#3701を見ると、Ruby 3.0のStringクラスの#+
と#-
以外のメソッドはすべて、StringのサブクラスのインスタンスではなくStringインスタンスを返すよう統一されるんですね。#3701のコメントの中でもSafeBufferについて言及されていました。
⚓ 6.1: Relation#merge
の利用法をガイドに追加
現在のActive Record クエリインターフェイスガイドの「結合されたテーブルで条件を指定する」には
Relation#merge
の利用法を示すサンプルがない。
既存のサンプルは比較的基本的なjoinedテーブルの条件を生成するにはよいが、Relation#merge
は高度なSQLクエリ生成や既存の名前付きスコープの利用に欠かせない。
同PRより大意
つっつきボイス:「Relation#merge
のドキュメントはたしかに欲しいですね」「こういう情報がガイドに追加されるのはありがたい🙏」
# 同PRのガイドより
class Order < ApplicationRecord
belongs_to :customer
scope :created_in_time_range, ->(time_range) {
where(created_at: time_range)
}
end
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Customer.joins(:orders).merge(Order.created_in_time_range(time_range)).distinct
⚓ 6.1: require_dependency
をhelper
から削除
これだけ今年5月にマージ済みのプルリクです。
プルリクの動機は以下の2本立て:
require_dependency
は現在フレームワークから段階的に削除を進めているconfig.add_autoload_paths_to_load_path
が無効の場合にhelper
が動くようにする
同commitより大意
コントローラの
helper
クラスメソッドは、stringやsymbolで指定されているヘルパーモジュールをrequire_dependency
ではなくString#constantize
で読み込むようになった
Action Pack Changelogより
つっつきボイス:「使わないことになったrequire_dependency
がRailsフレームワークに残っていたのを削除したんですね」「Rails 6.1のアップグレードガイド(edge版)を見て知りました」
参考: 2.6.3 require_dependency
について -- Rails アップグレードガイド - Railsガイド
require_dependency
の既知のユースケースはすべて排除されました。自分のプロジェクトをgrepしてrequire_dependency
を削除してください。
railsguides.jpより
⚓Rails
⚓ RuboCop Rails 2.9がリリース
RuboCop Rails 2.9 has been released that adds two new cops and some bug fixes. Enjoy!https://t.co/ZMSzgXpNbv
— Koichi ITO (@koic) December 8, 2020
つっつきボイス:「@koicさんのツイートでこれを含むさまざまな更新情報を知ることができて助かります🙏」「え、今日ちょうどRuboCopのバージョン上げたところなんですけど😳」「こちらはRuboCop Railsなので本体のRuboCopとは別物ですね」「よかった〜」「RuboCop本体もこの後Rubyのコーナーで取り上げます(明日のウォッチに掲載します)」
RuboCop Rails 2.9.0リリースノートの変更点より:
- RuboCop 0.90以上が必須になる
Rails/SquishedSQLHeredocs
をunsafeに変更
⚓ acts_as_tenant: マルチテナンシーgem(Ruby Weeklyより)
つっつきボイス:「READMEのこのサンプルコード↓ではサブドメインでテナントを分けていますね: サブドメインでマルチテナント化する設計はよく使われていて、Slackなどでも行われています」
# 同リポジトリより
class ApplicationController < ActionController::Base
set_current_tenant_by_subdomain(:account, :subdomain)
end
「ただ、マルチテナントの要件はプロジェクトごとに詳細がいろいろ異なるんですよ: acts_as_tenant gemはまだ使ったことはありませんが、詳細を把握しきっていないgemでマルチテナンシーすることを考えると、おそらく自分ならマルチテナンシーの機能を自分で実装する方を選ぶことが多いかもしれませんね」「あぁ、たしかに!」「gemが案件にフィットするかどうかは案件の成長なども含めて検討しておく必要があるでしょうね: acts_as_tenantはたぶんそういう種類のgemだと思います」
「もちろん、それまで十分使い慣れていて、要件に合致することが確かめられているなら使ってもよいと思います👍」「acts_as_tenant gemは歴史もあるしサポートも継続しているようなので、その点は大丈夫そうですね」
「Railsの認証機能でよく使われるDevise gem↓などもそうなんですが、gemで機能を取り入れるということは、そのgemの機能の範囲で構築せざるを得なくなることでもありますよね」「そうなんですよね...」「gemの詳細や案件との調和をよく調べないうちにDeviseのようなgemを安易に導入すると、後がつらくなるから気をつけようという話もよく目にします」「はい、身に沁みてます😅」
「そういえば、昔のRails向けgemにはacts_as_なんちゃらという名前がよくあったというお話しを以前されてましたね」「昔はそういうネーミングのライブラリが多かったんですが、そういう名前でもライブラリが古いとは限らないでしょうね」「あ、そうか」「このacts_as_tenantは新しいのかな?」「リリースをさかのぼってみると2012年からありますね」「ホントだ」「★はあと少しで1000になるくらいかな」「おそらく今話したみたいに、Railsのマルチテナンシー機能については案件に応じて自前で実装することが多いのかもしれませんね」
追いかけボイス:「後でacts_as_tenantの実装を少し追いかけてみた限りでは、Deviseと比べてだいぶ薄めのgemのようです」「なお、マルチテナンシーだとapartmentというgem↓もあって、もしかするとこちらの方がメジャーかもしれません」
⚓ ar_lazy_preload: GraphQLでも役立つlazy load gem(Ruby Weeklyより)
TechRacho翻訳記事でお世話になっているEvil Martiansがスポンサーになっています。
つっつきボイス:「このgemの#lazy_preload
メソッドを使って読み込んでおくと、たとえば後でmap
してもクエリを1回しか実行しなくなるということか↓」
# 同リポジトリより
users = User.lazy_preload(:posts).limit(10) # => SELECT * FROM users LIMIT 10
users.map(&:first_name)
「コンフィグでlazyなオートプリロードをオンにすることもできる↓」
# 同リポジトリより
ArLazyPreload.config.auto_preload = true
「#preload_associations_lazily
も使える↓」
# 同リポジトリより
posts = User.preload_associations_lazily.flat_map(&:posts)
# => SELECT * FROM users LIMIT 10
# => SELECT * FROM posts WHERE user_id in (...)
「READMEを眺めた限りでは比較的シンプルな機能のgemみたい」「lib/
を覗いてみても、コンフィグも少ないし、比較的シンプルそうですね」「同じコードを自力で書くよりはこういうgemでやる方がよさそう👍」
ArLazyPreloadは、関連付けのlazy loading機能をRailsアプリケーションに導入するgemです。N+1クエリ問題を解決するRails組み込みメソッドはたくさんありますが、プリロードする関連付けのリストが明確でない場合があります。そんなときはこのgemで大半をカバーできます。
- シンプル
- 利用に必要なのは、
#includes
や#eager_load
や#preload
を#lazy_preload
に置き換えることだけです。- 高速
- ベンチマークをご覧ください(
TASK=bench
とTASK=memory
)。- GraphQLとの親和性
- 読み込む関連付けのリストをトップレベルのリゾルバで定義すれば後はこのgemにおまかせ
- オートプリロードのサポート
- 関連付けのリストを指定したくない場合は
ArLazyPreload.config.auto_preload
をtrue
に設定します。
同リポジトリより
「READMEにはGraphQLでも便利と書かれていますね」「以下のような感じで、GraphQLのリゾルバでカラムを取得してからmap
するような操作はGraphQLでよく使うので、たぶんそれを指しているんじゃないかな」
# 同リポジトリより
users = User.lazy_preload(:posts).limit(10) # => SELECT * FROM users LIMIT 10
users.map(&:first_name)
「GraphQLでは、最終的に欲しいカラムをGraphQLのリクエスト側で指定できるんですが、おそらくこのgemの機能を使うと、GraphQLリゾルバの直前まではActive Recordのリレーションのまま加工して、最後の最後でGraphQLからのカラムを渡すと、そのカラムでSELECTするクエリを発行して結果を返す、という感じでクエリ発行が1回で済むようにするのがやりやすくなるんでしょうね」「なるほど!」「なかなかよさそうなgemですね👍」
前編は以上です。
バックナンバー(2020年度第4四半期)
週刊Railsウォッチ(20201209後編)Ractorベンチマーク記事、Railsで複合主キーを使う、AWS re:Invent 2020ほか
- 20201208前編 レガシーRailsアプリを引き継ぐときの6つの作業、サーバーレスプロジェクトをRailsに移行ほか
- 20201202後編 Rails 6.1 RC2リリース、Ruby STMの詳細な解説記事、RSpecのdiffを見やすくするsuper_diff gemほか
- 20201201前編 switch_pointがActive Record 6.0でサポート終了、Rails DBトランザクションの落とし穴ほか
- 20201124 strict loading violationの振る舞いを変更可能に、Railsモデルのアンチパターン、quine-relayとさまざまなクワインほか
- 20201117後編 Rubyのパターンマッチングが3.0で本採用に、AWS Lambdaサイズを縮小する、AppleのM1チップほか
- 20201116前編 6.1のActive Storageでimage_processing gemが必須に、Webアプリ設計の変遷とフロントエンド領域の再定義ほか
- 20201111後編 RubyConf 2020が11/17〜19オンライン開催、GitHub Container Registryベータ開始、スマートロックほか
- 20201110前編 Rails 6.1 RC1がリリース、Railsアプリに最適なEC2インスタンスタイプ、n_plus_one_control gemほか
- 20201028後編 RuboCop 1.0.0 stable版がリリース、Ruby DSLのGUIフレームワークGlimmer、Keycloakほか
- 20201026前編 Shopifyのerb-lint gem、Form Objectを使いやすくするyaaf gem、railsrcの機能追加ほか
- 20201021後編 webpack 5リリースでWebpacker対応開始、AWS Lambda Extensions発表、Pythonにマクロ構文追加提案ほか
- 20201020前編 Percona Toolkitは優秀、Active Admin非公式ガイド、Railsをリアクティブにするガイドほか
- 20201013後編 ruby-type-profilerがtypeprofにリネーム、AWS API Gatewayの実行ログは便利、M5Stackほか
- 20201012前編 Railsの隠し機能routing visualizer、action_args gem、N+1用goldiloader gemほか
- 20201006後編 Rubyの
defined?
キーワード、Ractorベースのジョブスケジューラ、Caddy Webサーバーほか - 20201005前編 Ruby 2.7.2がリリース、Shopifyのモジュラー化gem「packwerk」、stimulus_reflexほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。