- Ruby / Rails関連
週刊Railsウォッチ: Evil Martiansが使っているgem、JavaScriptガイドが更新ほか(20230131前編)
こんにちは、hachi8833です。RubyKaigi 2023のCFPは今夜1/31いっぱいが締め切りです。
“about 15 hours left to submit your proposal” 🏯🏔️📝👀💨 #rubykaigi https://t.co/n4CUDDLf6X pic.twitter.com/klXhtyZqpY
— Kakutani Shintaro (@kakutani) January 31, 2023
🔗Rails: 先週の改修(Rails公式ニュースより)
つっつきボイス:「やっと新年の公式更新情報にたどり着きました💦」「ところで最近はRails公式の更新情報び公開ペースが安定していてありがたい」「The Rails Foundationの発足とも関係あるかもしれませんね(ウォッチ20221122)」
🔗 UJSテストランナーがKarmaに置き換えられた
- PR: use Karma as the test runner for the UJS tests by lsylvester · Pull Request #46206 · rails/rails
動機/背景
#45546のUJSビルドでBladeがRollupに置き換わった。
このプルリクはその続きとして、テストのビルドでBladeをRollupに置き換え、Action CableのテストランナーでBladeをKarmaに置き換える。
これによって、RailsをRack 2.xに縛り付けていた依存関係の1つであるBladeの最後の利用が削除される。
同PRより
つっつきボイス:「従来のBladeがKarmaに置き換えられたのか」「そういえばKarmaっていうランナーありましたね」「BladeがCoffeeScriptに依存していたとは↓」
# Gemfile.lock#L143
...
bindex (0.8.1)
- blade (0.7.3)
- activesupport (>= 3.0.0)
- blade-qunit_adapter (>= 2.0.1)
- coffee-script
- coffee-script-source
- curses (>= 1.4.0)
- eventmachine
- faye
- sprockets (>= 3.0)
- thin (>= 1.6.0)
- thor (>= 0.19.1)
- useragent (>= 0.16.7)
- blade-qunit_adapter (2.0.1)
bootsnap (1.9.3)
...
# actionview/RUNNING_UJS_TESTS.rdoc
== Running UJS tests
-Ensure that you can build the project by running:
- rake ujs:server
+Run the tests in headless mode by running:
+
+ rake test:ujs
-Then run the web tests by visiting the following URL in your browser:
+To run the tests in a browser, start the Rails UJS server by running:
- http://localhost:4567
+ rake ujs:server
🔗 productionでPumaのワーカー数をプロセッサ数と同じになるようにする
production環境でホストのプロセッサのパフォーマンスをすべて使うようにする。
同PRより
つっつきボイス:「DHHのプルリクです」「お〜なるほど、production環境のワーカー数をデフォルトで4とかに固定するより、その環境で使えるリソースの最大数に設定するのはいい👍」「複数のRailsサーバーやジョブワーカーが同居しているようなサーバーだと過剰にワーカーが起動してしまうので、明示的にWEB_CONCURRENCY
環境変数を指定するというのもできて良いですね」
# railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt#L14
# Specifies that the worker count should equal the number of processors in production.
+if ENV["RAILS_ENV"] == "production"
+ worker_count = ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count }
+ workers worker_count if worker_count > 1
+end
+
🔗 コールバックのonly
やexcept
オプションで指定したシンボルがない場合にエラーにするようになった
コールバックの
only
とexcept
オプションには、存在しないアクションのシンボルを含めることができてしまう。このブランチでは、そうした例外的なアクション名が実際に利用可能なアクションと対応していない場合にエラーを発生させる。
この変更は、誤字脱字のせいで重要なコールバックが呼び出されるべきときに呼び出されなくなってしまうという事態を防ぐことが目的。before_action :authorize_user, only: [:showww] #typo def show end
あるいは、コールバックで参照されていることに気づかないままアクション名を変更することを防ぐ。
before_action :authorize_user, only: [:admin_console] # 行数が多くて見落としてしまう... def admin_panel # メソッドを最近`admin_console`から変更した end
その他の情報
このチェックをAbstractController::Callbacks::ActionFilter
に置いた理由は、コールバック条件のリストと、インスタンス化されたコントローラが同じ場所にあるから。このチェックはコントローラの初期化時点で行う方がより"クリーン"と思えたが、初期化もコールバックのフィルタもリクエストごとに行われるので、少なくともここでは既に発生しているコールバックチェインのスキャンに相乗りすることが可能。このブランチでは
config.action_controller.raise_on_missing_callback_conditionals
コンフィグオプションを追加してあり、これを音にしたときだけエラーがraiseされる。個人的には常に検出すべきバグだとは思うが(少なくともdevelopmentモードでは)、アプリをアップグレードしやすくするためにオフにできるオプションがあるとよいと思う。
同PRより
つっつきボイス:「プルリクの英語タイトル、プログラミング言語の予約語が名詞として使われていることを知らなかったら走り書きに見えそう」「コールバックのonly:
とかで名前を書き間違えたりしたらちゃんとエラーを出すようになったんですね: これはいい改修👍」「呼んだつもりのコールバックが呼ばれてなかったら怖い」「タイポってどうしても発生しますよね」「タイポを修正するプルリク出すときにちょっとしょんぼりしちゃうけど」
🔗 rubygemの最小バージョンを3.3.13以上にした
- PR: Bump required_rubygems_version to 3.3.13 or higher by yahonda · Pull Request #46817 · rails/rails
動機/背景
このプルリクはrequired_rubygems_version
を3.3.13以上に上げる。理由は#45979がrubygems/rubygems#5486に依存しているため(CHANGELOG.md#3313--2022-05-04に記載されている)。詳細
- RubyGems 3.3.12
Gem.ruby_version
にはパッチレベルが含まれている$ gem -v 3.3.12 $ irb irb(main):001:0> Gem.ruby_version => Gem::Version.new("3.1.3.p185")
- RubyGems 3.3.13
Gem.ruby_version
にはパッチレベルが含まれていない$ gem -v 3.3.13 $ irb rb(main):001:0> Gem.ruby_version => Gem::Version.new("3.1.3") irb(main):002:0>
追加情報
Ruby 1.9.3-p0(#4576)のときに、カレントバージョン1.8.11が更新された。
同PRより
つっつきボイス:「Railsでrubygemの最小バージョンが上げられた」「rubygemはgem
コマンドを提供しているgemですね」「Railsのrubygemの最小バージョンはRuby 1.9.3-p0のときからずっと1.8.11のままだったのか↓」「そんなに長い間だったとは」
# rails.gemspec#L12
s.required_ruby_version = ">= 2.7.0"
- s.required_rubygems_version = ">= 1.8.11"
+ s.required_rubygems_version = ">= 3.3.13"
🔗 assert_raises
アサーションに例外メッセージとマッチさせるオプションが追加された
以下のように書く代わりに
error = assert_raises(ArgumentError) do perform_service(param: 'exception') end assert_match(/incorrect param/i, error.message)
以下のように書ける。
assert_raises(ArgumentError, match: /incorrect param/i) do perform_service(param: 'exception') end
fatkodima
同Changelogより
つっつきボイス:「assert_raises
でmatch: /incorrect param/i
のようにエラーメッセージもチェックできるようになったんですね: assert_raises
はエラーオブジェクトの種別をチェックするものだけど、エラーオブジェクトを使い分けずに共通のエラークラスでエラーを表示している人にはこうやって短く書けるのは嬉しいのかも」
参考: Rails Edge API assert_raises
-- ActiveSupport::Testing::Assertions
「以前はエラーの種別ごとにエラーのクラスを増やすのが何となく億劫だったこともあるんですが、本来であればエラーの種別ごとにエラークラスを定義するべきなので最近はそうするようにしてます」「エラークラスをあまり細かい粒度で増やしたくない気持ち、ちょっとわかります」「名前空間を使うことにはなりますし」「いったんそうすると、今後もエラークラスを際限なく作らなければいけないんだろうかという気持ちになったりする」「わかる」
「たぶんエラーオブジェクトの名前をたくさん考えるのがつらいからなのかも」「たしかに」「名前を考えるのは面倒だけど、かといってStandardErrorだけで処理するのはさすがによくないので、ValidationErrorみたいなクラスを作って、複数の似たようなエラーについては同じエラークラスだけどメッセージの内容が異なるというエラーオブジェクトをraise
させたりすることがありますね」
class CreateUserAccountService
def perform(account_creation_params)
end
end
「たとえば上のようなクラスがあるときに、以下みたいなのを考えているとつらいので、」
class CreateUserAccountService
def perform(account_creation_params)
# コード
raise OAuthTokenError.new
# コード
end
class OAuthTokenError < StandardError; end
class OAuthTokenExpiredError < StandardError; end
class OAuthDoesNotHaveRequiredScopesError < StandardError; end
# ...
end
「以下みたいに複数のエラーをある程度抽象化してまとめることがあったりしますね」「なるほど」
class CreateUserAccountService
def perform(account_creation_params)
# コード
rescue => e
raise OAuthIntegrationError.new(e.message)
end
# コード
end
class OAuthIntegrationError < StandardError; end
# ...
end
「そういう感じでやっている人には、今回のassert_raises
の改修はありがたいかもしれませんね」「なるほど」
参考: class StandardError
(Ruby 3.2 リファレンスマニュアル)
🔗 rails db:prepare
でinternal_metadata
に誤った環境が保存されていたのを修正
修正対象: #46845
#46845のコメントでは以下のような変更を提案した。
--- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1427,7 +1427,7 @@ def migrate_without_lock def record_environment return if down? - @internal_metadata[:environment] = connection.migration_context.current_environment + @internal_metadata[:environment] = connection.pool.db_config.env_name end def ran?(migration)
しかし、database.ymlの
db_config.env_name
が環境変数(トップレベルキー)なのでうまくいかなかった。rakeタスクは基本的にRAILS_ENV
の保存を試みるが、これとは別物なのだろうか?以下を始めとしていくつものテストが失敗した。# rails/activerecord/test/cases/migration_test.rb#691 to 713 in c70a8f7 # Lines 691 to 713 in c70a8f7 def test_internal_metadata_stores_environment current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrations_path = MIGRATIONS_ROOT + "/valid" migrator = ActiveRecord::MigrationContext.new(migrations_path, @schema_migration, @internal_metadata) migrator.up assert_equal current_env, @internal_metadata[:environment] original_rails_env = ENV["RAILS_ENV"] original_rack_env = ENV["RACK_ENV"] ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call assert_not_equal current_env, new_env sleep 1 # mysql by default does not store fractional seconds in the database migrator.up assert_equal new_env, @internal_metadata[:environment] ensure ENV["RAILS_ENV"] = original_rails_env ENV["RACK_ENV"] = original_rack_env migrator.up end
そこで、#46845のコメントの元の提案に立ち返って、Railsアプリケーションの存在に依存しない形で少し拡張した。
エレガントではないが、動くようになった。
cc @eileencodes
同PRより
つっつきボイス:「rails db:prepare
で環境変数を取得するときに、マイグレーションのコンテキストを参照してバグったらしい」「test環境なのにdevelopment環境として保存されていたとは」「マルチプルデータベース環境だとデータベースごとにマイグレーションのコンテキストが変わることになると思うので、そのあたりに関連していそうな気がする」
🔗 設定されていないHostAuthorization
ミドルウェアを使わないようになった
動機/背景
従来は、config.hosts
にエントリがあるかどうかにかかわらずHostAuthorization
ミドルウェアが追加されていた。HostAuthorization
は計算コストの高い処理を行う前にconfig.hosts
をチェックするが結局読み込まれてしまうので、本来なら回避可能なオーバーヘッドが全リクエストで発生するということになりやすい。詳細
コンフィグがある場合にのみHostAuthorization
ミドルウェアを利用する。追加情報
ミドルウェア操作のdeprecation warningは必要だろうか?これについて考える前に、とりあえずこのプルリクをオープンしてどのぐらい関心が寄せられるかを様子見しようと思った。
同PRより
つっつきボイス:「HostAuthorizationはRackミドルウェアなんですね」「HTTP Hostヘッダーの値が指定のホスト名と一致するかどうかをチェックするミドルウェアでしたね: 今までは設定がなくてもミドルウェアが読み込まれていたけど、config.hosts
を指定しない場合は使わないようにするのはいい👍」
参考: Host - HTTP
| MDN
参考: § 3.4.1 ActionDispatch::HostAuthorization
-- Rails アプリケーションを設定する - Railsガイド
Rails.application.config.hosts = [ IPAddr.new("0.0.0.0/0"), # すべてのIPv4アドレス IPAddr.new("::/0"), # すべてのIPv6アドレス "localhost", # localhost予約済みドメイン ENV["RAILS_DEVELOPMENT_HOSTS"] # 開発用の追加ホストリスト(カンマ区切り) ]
🔗 Rails.env.local?
を最適化
- PR: Use precompute optimization for Rails.env.local? by skipkayhil · Pull Request #46830 · rails/rails
動機/背景
09e0372を参照。詳細
このプルリクは、#local?
の振る舞いを、定義済みの環境述語メソッドと同じにする(こちらは事前計算済みのインスタンス変数を即座に返す)。追加情報
ベンチマーク:
dev = ActiveSupport::EnvironmentInquirer.new("development") test = ActiveSupport::EnvironmentInquirer.new("test") prod = ActiveSupport::EnvironmentInquirer.new("production") Benchmark.ips do |x| x.report("dev local?") { dev.local? } x.report("test local?") { test.local? } x.report("prod local?") { prod.local? } x.compare! end
改修前:
Warming up -------------------------------------- dev local? 676.645k i/100ms test local? 627.687k i/100ms prod local? 671.078k i/100ms Calculating ------------------------------------- dev local? 6.785M (± 1.8%) i/s - 34.509M in 5.087679s test local? 6.284M (± 2.0%) i/s - 32.012M in 5.096338s prod local? 6.759M (± 1.8%) i/s - 34.225M in 5.065240s Comparison: dev local?: 6785134.0 i/s prod local?: 6759023.6 i/s - same-ish: difference falls within error test local?: 6283910.1 i/s - 1.08x (± 0.00) slower
改修後:
Warming up -------------------------------------- dev local? 1.076M i/100ms test local? 1.049M i/100ms prod local? 1.028M i/100ms Calculating ------------------------------------- dev local? 10.586M (± 2.3%) i/s - 53.799M in 5.084729s test local? 10.350M (± 2.5%) i/s - 52.457M in 5.071399s prod local? 10.396M (± 2.2%) i/s - 52.422M in 5.045193s Comparison: dev local?: 10586400.1 i/s prod local?: 10395758.6 i/s - same-ish: difference falls within error test local?: 10350328.8 i/s - same-ish: difference falls within error
同PRより
つっつきボイス:「前回DHHが追加したRails.env.local?
(ウォッチ20230125)が改修されていました」「これでRails.env.local?
が高速になる」「EnvironmentInquirer
というクラスがあるとは知らなかった」「2019年からあるみたいですね」「ここでLOCAL_ENVIRONMENTS
を設定しているのか、なるほど」
# activesupport/lib/active_support/environment_inquirer.rb#L6
module ActiveSupport
class EnvironmentInquirer < StringInquirer # :nodoc:
# Optimization for the three default environments, so this inquirer doesn't need to rely on
# the slower delegation through method_missing that StringInquirer would normally entail.
DEFAULT_ENVIRONMENTS = %w[ development test production ]
# Environments that'll respond true for #local?
LOCAL_ENVIRONMENTS = %w[ development test ]
def initialize(env)
raise(ArgumentError, "'local' is a reserved environment name") if env == "local"
super(env)
DEFAULT_ENVIRONMENTS.each do |default|
instance_variable_set :"@#{default}", env == default
end
+ @local = in? LOCAL_ENVIRONMENTS
end
DEFAULT_ENVIRONMENTS.each do |env|
class_eval "def #{env}?; @#{env}; end"
end
# Returns true if we're in the development or test environment.
def local?
- in? LOCAL_ENVIRONMENTS
+ @local
end
end
なお、EnvironmentInquirer
はAPIドキュメントサイトでは検索しても出てきません↓。
🔗Rails
🔗 RailsガイドのJavaScriptガイドがRails 7に合わせて更新
つっつきボイス:「英語版のJavaScriptガイドがRails 7用に全面的に更新されていたので、Railsガイドの方も合わせて更新しました」「お、これでimport mapやTurboの公式情報が読めるようになりますね🎉」「Ajaxという言葉がJavaScriptガイドから消えました」
翻訳後、訳文がCIでリンクエラーになったことで英語版のフォームヘルパーガイドに古い記述があることがわかったので、本家にプルリクを投げてマージしてもらいました↓
参考: [ci-skip] Remove obsolete paragraph from form_helpers.md Guide for Rails 7 by hachi8833 · Pull Request #47069 · rails/rails
参考: Action View フォームヘルパー - Railsガイド
🔗 Evil Martiansが使っているgem
つっつきボイス:「最近"私の好きなgem"的な英語記事にめぼしいものがあまりなかったんですが、Evil Martiansの新しい記事がちょっとよさそうだったのでピックアップしてみました」「gem名を見ればだいたいどんなことをするか見当はつくかな」「Evil Martiansのメンバーが作ったgemや、Evil Martiansがスポンサーになっているgemが目立ちますね」
「schkedの読み方がわからない」「rufus-schedulerのラッパーということはschedule daemonを短縮してもじった感じなのかも」「実行すれば単体でスケジューラが動くようですね: 単体で動かせる軽いスケジューラはありがたそう」
「cronを使うとcrondがない環境で動かせないんですよ、特にコンテナの時代はありがち」「たしかに」「rakeタスクでスケジューラを動かすタイプのツールもあったりするんですけど、rakeタスクだとrakeタスク自体の起動・読み込みでそれなりに時間がかかってしまったりするので、とても単純なことをしたいならRailsやrakeに依存しないスケジューラが欲しいと思うときがあります」
# jmettraux/rufus-schedulerより
require 'rufus-scheduler'
scheduler = Rufus::Scheduler.new
# ...
scheduler.in '10d' do
# do something in 10 days
end
scheduler.at '2030/12/12 23:30:00' do
# do something at a given point in time
end
scheduler.every '3h' do
# do something every 3 hours
end
scheduler.every '3h10m' do
# do something every 3 hours and 10 minutes
end
scheduler.cron '5 0 * * *' do
# do something every day, five minutes after midnight
# (see "man 5 crontab" in your terminal)
end
# ...
「rufus-schedulerは★も多いし、schkedはRailsのジョブをさっと動かせるみたい: 使ってみようかな👍」
「faktoryって何だろうと思ったらSidekiqと同じ会社が出しているんですね」「ほんとだ、Sidekiqとfaktoryが横に並んでる」
「ところで、記事のリンクでRails 7.1にCommon Table Expression(CTE)が入ることになっているのを思い出した」「そういえばそうでしたね」「CTEはぜひ使いたい機能」
参考: MySQL :: MySQL 8.0 Reference Manual :: 13.2.20 WITH
(Common Table Expressions)
「postgresとかpgがつく名前のgemはわかりやすい」「お、scenicも使ってる」「以前データベースVIEWの記事で触れていたgemですね」
# 同記事より
gem 'activerecord-postgres_enum'
gem 'pg_search'
gem 'postgresql_cursor'
gem 'fx'
gem 'scenic'
# or
gem 'pg_trunk'
「action_policyは知らなかった」「pundit gemより手軽に使える認可(authorization)ライブラリらしい」「これもEvil Martians製ですね」「見た感じpunditと使い方は似てそうだけど、少なくとも命名はaction_policyの方がずっとわかりやすい」「たしかに」
参考: Action Policy: authorization framework for Ruby/Rails applications
「ViewComponentは最近Evil Martiansが大プッシュしていますね↓」
「アセット管理のvite_railsは、あのViteのことですよね」
# 同記事より
gem 'vite_rails'
「panko_serializerは、昔からあるActiveModelSerializersと似ていてとても速いらしい」
# 同記事より
gem 'alba'
gem 'oj'
# or
gem 'panko_serializer'
「ログ関連でlogrageはよく使う」「yabedaって初めて見た」
# 同記事より
group :production do
gem 'yabeda-sidekiq', require: false
gem 'yabeda-puma-plugin', require: false
gem 'lograge'
end
「dry-シリーズのgemもありますね」
gem 'anycable-rails'
gem 'feature_toggles'
gem 'redlock'
gem 'anyway_config'
gem 'retriable'
gem 'dry-initializer'
gem 'dry-monads'
gem 'dry-effects'
参考: dry-rb - Home
「思ったよりgemがたくさん紹介されてますね」「最近のgemの動向を知っておくorおさらいしておくのに良さそう👍」
🔗 Ruby APIでFormObjectパターンを使う(RubyFlowより)
# 同記事より
class UsersController < ApplicationController
def create
form = UserForm.new(user_params)
if form.valid?
user = User.create(form.attributes)
render json: user, status: :created
else
render json: form.errors, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password)
end
end
つっつきボイス:「FormObjectは最近あまり見かけないかも」「記事は普通っぽいですが、最後にオチがあります↓」「あら、ChatGPTも使って書いた記事🤣」「ChatGPTはそれっぽい技術記事を生成できちゃいますよね」「基本は自分の経験ベースのようなのでコピペではなさそうかな」
This post was created based on my personal experience and research which I did using chat-gpt tool. So some content might not be 100% unique and is taken from other sources (which just proves the wild spread of the FormObject pattern, btw)
同記事より
🔗 Action Mailerのよい書き方(Ruby Weeklyより)
つっつきボイス:「まずはメールのdisplay_name
を設定しようという話↓」「メールの表示名のことですね」
# 同記事より
ActionMailer::Base.email_address_with_name(user.email, user.display_name)
=> "Matt Swanson <matt@boringrails.com>"
「メールのビューのパスをprepend_view_path
で指定できるのか↓」
# 同記事より
class ApplicationMailer < ActionMailer::Base
prepend_view_path "app/views/mailers"
end
「メール送信ごとにメーラーを作るか、1つのメーラーで複数の種類のメール送信を扱うかについては、記事にもあるように後者でいいと思います: ドメイン的に近い機能は一緒にする方がわかりやすいですね」「たしかに」「なおAction Mailerではmail(to: ...)
の戻り値(Action Mailerのオブジェクト)をメソッドから返すのが大事: そうすることで、非同期ジョブで同じ送信を繰り返したときにインスタンスが異なるものになるので、うっかり同じインスタンスを触って壊れたりしなくなります」「なるほど」
# 同記事より
class CommentReplyMailer < ApplicationMailer
layout "minimal"
def comment_reply_email(user, comment)
# mail(to: ...)
end
end
class UserMentionedMailer < ApplicationMailer
layout "minimal"
def mentioned_email(mentionee, comment)
# mail(to: ...)
end
end
# 同記事より
class NotificationMailer < ApplicationMailer
layout "minimal"
def comment_reply(user, comment)
# mail(to: ...)
end
def mentioned(mentionee, comment)
# mail(to: ...)
end
end
「Action Mailerは普通に使われる枯れた機能なんですが、なかなか新しい記事が出ないので令和の時代に書いてくれるのはありがたい👍」
参考: Action Mailer の基礎 - Railsガイド
前編は以上です。
バックナンバー(2023年度第1四半期)
週刊Railsウォッチ: 2022年のRails振り返り記事、RailsにDocker関連ファイルが追加ほか(20230125前編)
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)