こんにちは、hachi8833です。いよいよRubyKaigiが今週始まりますね。
ミナサン〜!
来週はRubyKaigi行きます!
是非会いに来てください!#rubykaigi pic.twitter.com/f8zUyWTzHd— 合同会社イービルマーシャンズ (@evilmartians_jp) August 31, 2022
RubyKaigiまであと1週間ですね! 今年は超絶技巧Rubyプログラミングコンテスト、TRICKの結果発表があります。1日目の最終枠、ご期待ください~。https://t.co/9Xsz4quRBW
— Yusuke Endoh (@mametter) August 31, 2022
お知らせ: 来週の週刊Railsウォッチはお休みをいただきます🙇
🔗Rails: 先週の改修(Rails公式ニュースより)
以下の公式更新情報を取り上げました。
また次のが出ていました。なかなか追いつけない...
🔗 ActiveSupport::Cache
の書き込みでexpires_at
に過去の日付を渡すとwarningをログ出力するようになった
概要
この変更によって、Rails.cache#fetch
やwrite
を実行するときにActiveSupport::Cache
が既に期限切れの場合はwarningをログに出力するようになる。
# 変更前: expires_atが過去の日時なのでキャッシュへの書き込みが無言で失敗する
Rails.cache.write(key, value, expires_at: Time.now.beginning_of_day)
# 変更後: warningをログ出力する
Rails.cache.write(key, value, expires_at: Time.now.beginning_of_day)
その他の情報
expires_at
オプションは#41831
で導入されたもので、これについては既に#45047で言及していた。あるとき、実際は自分のタイプミスに過ぎなかったにもかかわらずデバッグに手こずる問題が生じたことがあった。日時が過去になっている
expires_at
を渡すと、キャッシュのフェッチや書き込みがwarningなしで失敗した(キャッシュエントリは書き込み時点で既に失効していて、書き込みがスキップされたのか、それとも書き込まれたがアクセスできなかっただけなのかがわからなかった)。この問題を踏んだ原因は、
expires_at: Time.now.tomorrow.beginning_of_day
と書くつもりでexpires_at: Time.now.beginning_of_day
と書いてしまったことだった。自分のタイプミスは明らかだが、それに気がつくまで何がおかしかったのかを突き止めるのに手こずってしまった。そういうわけで、この変更は今後デバッグする人にとっても有用だろうと考えた。
なお、この変更によってロガーが起動されるタイミングは
expires_at
がexpires_in
に変更された後なので、expires_at
またはexpires_in
のどちらかを使うとwarningがログ出力されることに注意。
同PRより
つっつきボイス:「キャッシュの書き込みでexpires_at
に過去の日付を指定したら警告を出すのは適切ですね👍」
🔗 legacy_connection_handling=
を呼び出すとエラーを発生するように変更
私は
legacy_connection_handling
のセッターを7.0で非推奨化したが、その後この設定のデフォルト値をfalse
にせずに削除したため、アップグレードしたアプリでこの設定をfalse
にするとエラーが発生する可能性がある。この混乱した動作は、自分たちが業務で扱っているアプリのひとつで発生した。この混乱を避けるため
legacy_connection_handling
のセッターを再定義し、これが呼び出されたら引数エラーを発生するようにした。なおゲッターを再定義しなかったのは、ゲッターはコンフィグに入れるべきではなく、このコンテキストでは無用なためである。
cc/ @paracycle
同PRより
つっつきボイス:「legacy_connection_handling
はRailsにマルチプルデータベース機能が追加された頃の機能だったと思うけど、たしか少し前に削除されていましたね(ウォッチ20220411)」「今の7.0では非推奨で、7.1で削除されるそうです」「そのlegacy_connection_handling
が削除されてfalseに設定しようがなくなったので、呼んだ時点でエラーを出力するようにしたんですね」
参考: §2.1 データベース単位のコネクション切り替え -- Ruby on Rails 6.1 リリースノート - Railsガイド
🔗 ActiveRecord::QueryMethods#in_order_of
のソート対象の値がnilでも動作するよう修正
in_order_of
にnil
を渡すと、以下の無効なSQLクエリが生成される(NULL != NULL
であるため)。
Book.in_order_of(:format, ["hardcover", "ebook", nil]).to_sql
SELECT "books".* FROM "books" WHERE "books"."format" IN ('hardcover', 'ebook', NULL)
ORDER BY CASE "books"."format" WHEN 'hardcover' THEN 1
WHEN 'ebook' THEN 2 WHEN NULL THEN 3 ELSE 4 END ASC
このプルリクでは、MySQLの場合には特殊な順序のフィールド生成(
ORDER BY FIELD
)を削除した。ここではNULL
が使えないためで、デフォルトのORDER BY CASE
が使える(MariaDBとMySQL 5.7以降でテストした)。
cc @kddnewton(本機能の作者)
関連: #42061
同PRより
つっつきボイス:「たしかにin_order_of
にnil
を渡すと動作が不定になりそうなので、nil
かどうかのチェックは必要でしょうね」「言われてみればSQLではNULL != NULL
だった」
# activerecord/lib/active_record/relation/query_methods.rb#L522
+ where_clause =
+ if values.include?(nil)
+ arel_column.in(values.compact).or(arel_column.eq(nil))
+ else
+ arel_column.in(values)
+ end
「ちなみにin_order_of
メソッドはうまくハマればとても気持ちいいですよ」「どんなメソッドでしたっけ?」「引数に渡した項目の順序どおりにソートしてくれるメソッドです↓」「SQLを見ると一目瞭然ですね」
# api.rubyonrails.orgより
User.in_order_of(:id, [1, 5, 3])
# SELECT "users".* FROM "users"
# ORDER BY FIELD("users"."id", 1, 5, 3)
# WHERE "users"."id" IN (1, 5, 3)
「in_order_of
は割と新しいメソッドで、たしか最初はActive SupportのEnumerableに似たような機能が入って、その後ActiveRecord::QueryMethods
にも移植された覚えがあります」「やや、これ知らなかった」「知っておくと便利なメソッド👍」
参考: Rails API in_order_of
-- ActiveRecord::QueryMethods
参考: Rails API in_order_of
-- Enumerable
Rails 7: クエリ結果を任意の順序にできるActiveRecord::QueryMethods#in_order_of
🔗 マルチパートリクエストでRackのEOFError
をrescue
するよう修正
以下の箇所で
request_parameters
メソッドがEOFError
例外によってRackで失敗するマルチパートリクエストには、さまざまなバリエーションがある。
- rack/parser.rb at c9c32b70f9ac98d903ab6361cdba3abbf4fa60ac · rack/rack
- rack/parser.rb at c9c32b70f9ac98d903ab6361cdba3abbf4fa60ac · rack/rack
- rack/parser.rb at c9c32b70f9ac98d903ab6361cdba3abbf4fa60ac · rack/rack
そこで、
BadRequest
例外を発生させるrescue
のリストにEOFError
を追加し、http 500エラーページではなく"bad request"ページを表示するようにした。
Rack 3.0ではこういう場合のためのEmptyContentError
例外が新しく導入されたが、これはEOFError
例外の子なので、Rack 3.0.0.beta1(rack/rack@249dd78)でこのコードをテストしたときも明示的なrescue
は行わなかった。
cc: @rafaelfranca
同PRより
つっつきボイス:「マルチパートリクエストのエラーケースのうちRack内部でEOFError
が発生するケースが正しくrescue
されていなかったということですね: このテストではCONTENT_LENGTH
の値が実際のコンテンツ長さと一致していませんが、これはHTTPリクエストとして不正になります」
# actionpack/test/dispatch/request_test.rb613
# partially mimics https://github.com/rack/rack/blob/249dd785625f0cbe617d3144401de90ecf77025a/test/spec_multipart.rb#L114
test "request_parameters raises BadRequest when content length lower than actual data length for a multipart request" do
request = stub_request(
"CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
"CONTENT_LENGTH" => "9", # lower than data length
"REQUEST_METHOD" => "POST",
"rack.input" => StringIO.new("0123456789")
)
err = assert_raises(ActionController::BadRequest) do
request.request_parameters
end
# original error message is Rack::Multipart::EmptyContentError for rack > 3 otherwise EOFError
assert_match "Invalid request parameters:", err.message
end
test "request_parameters raises BadRequest when content length is higher than actual data length" do
request = stub_request(
"CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
"CONTENT_LENGTH" => "11", # higher than data length
"REQUEST_METHOD" => "POST",
"rack.input" => StringIO.new("0123456789")
)
err = assert_raises(ActionController::BadRequest) do
request.request_parameters
end
assert_equal "Invalid request parameters: bad content body", err.message
end
🔗 ガイド: breaking changeや非推奨化サイクルの記述を追加
つっつきボイス:「ガイドの更新です」「今までbreaking changesなどの記述がなかったとは」「記述だけではなくコードサンプルも追加されているのがいいですね👍」
以下は変更内容を手短にまとめたものです。
- 既存のアプリケーションが壊れる可能性がある変更はbreaking changeとみなす。
- 既存の振る舞いを削除するbreaking changeを行うときは、その前に既存の振る舞いを維持しながら非推奨化警告を追加して表示する期間を設ける必要がある。
- 既存の振る舞いを変更するbreaking changeを行うときは、フレームワークにデフォルトの振る舞いを追加する必要がある。
🔗Rails
🔗 Herokuが無料プランの廃止とストレージ削除を発表
参考: PaaS「Heroku」が無料プラン廃止、11月から 非アクティブなアカウントとストレージも削除 - ITmedia NEWS
つっつきボイス:「Herokuの無料プラン廃止が話題になっていますね」「使われていないアカウントの削除も行われるのは大きい」
なお元記事では、今後「Students and Nonprofit Program」について発表が予定されているとも記されています↓。
Students and Nonprofit Program
We appreciate Heroku’s legacy as a learning platform. Many students have their first experience with deploying an application into the wild on Heroku. Salesforce is committed to providing students with the resources and experiences they need to realize their potential. We will be announcing more on our student program at Dreamforce. For our nonprofit community, we are working closely with our nonprofit team, too.
Heroku’s Next Chapter | Herokuより
「早速うなすけさんが移行先の比較記事を出してくれています🙏↓」
書きました
RailsアプリをHerokuから移行するならどれがいいのか比較する | うなすけとあれこれ https://t.co/7PA1wCjnWk
— うなすけ (@yu_suke1994) May 9, 2022
- Cloud Application Hosting for Developers | Render
- Railway
- Deploy app servers close to your users · Fly
「私もこの記事に載っている選択肢の中からRender.comに移行しようとしているところです: ちなみにRenderの無料プランは90日までです」「今思えばHerokuで1 Dynoを無料で使えたのは大きかったですね」
「自分もHerokuにRailsアプリを何度かデプロイした覚えあります」「私もです」「いわゆるPaaSは一時期広く使われていましたけど、DockerとKubernetesの出現以降はすべてのサービスがコンテナに集約される方向に向かっている印象がありますね」「もちろんPaaSは今でも有用ですし、Heroku CLIも長年洗練されて使いやすくなっていますけど、コンテナの形になっていると何かと取り回しが利きますよね」「今はHerokuでもコンテナが動くようになっていますけどね」
参考: PaaSとは | クラウド・データセンター用語集/IDCフロンティア
参考: Docker によるデプロイ | Heroku Dev Center
「なお、現在Railsチュートリアル™も即座にこの件に対応することを決めて、すごい勢いで新しいデプロイ先の検証を進めました↓」「たしかにRailsチュートリアルはこれまでHeroku環境を前提としていたので重要ですね」
#Railsチュートリアル では本日いくつかのデプロイ先でSample Appが問題なく動くことの検証を完了いたしました (Thx! @himajin315 🙏💖)
コンテンツ部分も合わせて修正していくので、更新箇所を洗い出し、期日までに必要な対応を順次進めていく予定です 📝💨
引き続きご活用頂けると嬉しいです...!! https://t.co/t9xcpmgV02 pic.twitter.com/NITKuzNic9
— Railsチュートリアル 🎓 (@RailsTutorialJP) August 26, 2022
🔗 RailsアプリをWebpackからesbuildに移行してビルドを縮小・高速化
つっつきボイス:「この記事は現在翻訳を進めていて近日公開します」「RailsでWebpackerがデフォルトではなくなってjsbundling-rails経由でesbuildを使えるようになったので、esbuildに移行するのもわかる」
参考: esbuild - An extremely fast JavaScript bundler
参考: Node.js のビルドツール「esbuild」について!
「この記事では、jsbundling-railsを調べた結果jsbundling-railsは使わずに独自のrakeタスクを書くことにしたのがちょっと変わっていると思いました↓」「RailsとJavaScriptバンドラーをどこまで結合させるかは常に悩ましい問題ですね」
# 同記事より
namespace :javascript do
desc "Build your JavaScript bundle"
task :build do
system "yarn install" or raise
system "yarn run build:js" or raise
end
desc "Remove JavaScript builds"
task :clobber do
rm_rf Dir["app/assets/builds/**/[^.]*.{js,js.map}"], verbose: false
end
end
Rake::Task["assets:precompile"].enhance(["javascript:build"])
Rake::Task["test:prepare"].enhance(["javascript:build"])
Rake::Task["assets:clobber"].enhance(["javascript:clobber"])
🔗 active_interaction gemでRailsアプリのビジネスロジックを管理する
つっつきボイス:「active_interactionはどこかで見たような気がする」「リポジトリを見ると、現在はバージョン5で少なくとも7年前からあるようなので、歴史ありそう」
「ActiveInteraction::Base
を継承したクラスにCommandパターン的にビジネスロジックを書いていく感じ↓」「execute
に処理を書くとクラス.run!
で呼び出せるんですね」「run!
の引数をstring :name
のようなDSLで書けるようにする手法はよく使われますね」
# 同リポジトリより
class SayHello < ActiveInteraction::Base
string :name
validates :name,
presence: true
def execute
"Hello, #{name}!"
end
end
# 同リポジトリより
SayHello.run!(name: nil)
# ActiveInteraction::InvalidInteractionError: Name is required
SayHello.run!(name: '')
# ActiveInteraction::InvalidInteractionError: Name can't be blank
SayHello.run!(name: 'Taylor')
# => "Hello, Taylor!"
「こういうふうにarray
やhash
などのフィルタが使えるのも便利そう↓」「ベタ書きの手作りService Objectよりは読みやすく書けそう👍」「リポジトリにも"Railsでシームレスに使えるように設計したService Objectの実装"と書かれていますね」「まさにそういう感じのgem」
# 同リポジトリより
class ArrayInteraction < ActiveInteraction::Base
array :toppings
def execute
toppings.size
end
end
ArrayInteraction.run!(toppings: 'everything')
# ActiveInteraction::InvalidInteractionError: Toppings is not a valid array
ArrayInteraction.run!(toppings: [:cheese, 'pepperoni'])
# => 2
class HashInteraction < ActiveInteraction::Base
hash :preferences do
boolean :newsletter
boolean :sweepstakes
end
def execute
puts 'Thanks for joining the newsletter!' if preferences[:newsletter]
puts 'Good luck in the sweepstakes!' if preferences[:sweepstakes]
end
end
HashInteraction.run!(preferences: 'yes, no')
# ActiveInteraction::InvalidInteractionError: Preferences is not a valid hash
HashInteraction.run!(preferences: { newsletter: true, 'sweepstakes' => false })
# Thanks for joining the newsletter!
# => nil
🔗 Service Objectよもやま話
「このgemと記事の作者のAaron Lasseigneさんについて調べてみたら、以前翻訳した以下の記事の著者で↓、この記事内でも当時からactive_interaction gemについて触れていました」「Service Objectを推している人なんですね」
「Service Objectは何でもかんでも無節操に放り込まないようにするのが肝心なので、節度を保ってCommandパターン的に使う分には大丈夫だと思います」「ですよね」
「Service Objectは個人的にあんまり好きじゃないかも」「でも複数の処理を横断するようなビジネスロジックをconcernsなどに置くとモデルがいくらでも肥大化してしまうので、結局何らかの形でFacade的なものを書く必要は生じると思いますけどね」「そうなんですよ、2つのモデルを行ったり来たりするような処理を片方のモデルに置くわけにはいかないので、どこかでService Object的な何かが必要になる」
「Service Object的に書くメリットのひとつは、テストが書きやすいこと」「そうそう、そのオブジェクトが何をするかがはっきりしますよね」「もちろんService Objectに何でもかんでも放り込んだらダメですけど、処理が1つのコマンドに落とし込まれていれば、テストが落ちたときに何が起きたかわかりやすいので自分は割と好きかも」
「記事の方も、active_interaction gemを使うとこんなふうに治安を保ちながらService Objectを使えますよという感じ↓」
# 同記事より
# ImportEmployeesData: Parent Interaction
class ImportEmployeesData < ActiveInteraction::Base
object :client,
desc: "Third-party service from where we will fetch employees' data"
def execute
employees_data = compose(
FetchEmployeesData,
client: client
)
standard_data = compose(
ConvertEmployeesDataIntoStandardFormat,
data: employees_data
)
compose(
ImportEmployees,
data: standard_data
)
end
end
# 同記事より
# FetchEmployeesData
class FetchEmployeesData < ActiveInteraction::Base
object :client,
desc: "Third-party service from where we will fetch employees' data"
def execute
# API call to the third-party service
client.fetch_employees
end
end
# 同記事より
# ConvertEmployeesDataIntoStandardFormat
class ConvertEmployeesDataIntoStandardFormat < ActiveInteraction::Base
object :data,
desc: "Employee data that will be converted into the standard format"
def execute
# code that converts the employee data into the standard format
end
end
# 同記事より
# ImportEmployees
class ImportEmployees < ActiveInteraction::Base
object :data,
desc: "Standard data that will be imported into our system"
def execute
# code to import employees into our database
end
end
🔗 Hotwire.love -- Hotwireコミュニティ
https://t.co/MOrJRvXVfm 、次回のミートアップは9/8!とお話ししていましたが、諸事情により開催が遅れそうです。1週ずれて9/15とかになるかも。決まり次第告知ページを用意しますのでもうしばらくお待ちください🙏
— Junichi Ito (伊藤淳一) (@jnchito) September 5, 2022
- イベントグループ: Hotwire.love - connpass
つっつきボイス:「Hotwire.loveというドメイン名❤️」「管理者にjnchitoさんもいますね」「YassLabの安川さんもいます」「さっそく参加しました」
前編は以上です。
バックナンバー(2022年度第3四半期)
週刊Railsウォッチ: RubyKaigi 2022タイムテーブル公開、viewport-extraほか(20220830後編)
- 20220829後編 MinitestとRSpecの比較、商用版NGINXの重要機能がオープンソース化ほか
- 20220822前編 週刊Railsウォッチ: ビューテンプレートに渡せるローカル変数をマジックコメントでチェック可能にほか
- 20220802後編 RubyのGVLトレーサーgvl-tracing、casting gemでオブジェクトに振る舞いを追加ほか
- 20220801前編 “リーダブルテストコードについて考えよう”スライド公開、Evil Martiansが日本上陸ほか
- 20220726後編 中高生国際Rubyプログラミングコンテスト2022、W3Cの分散型識別子仕様が勧告にほか
- 20220725前編 RailsConf 2022の動画が公開、マイクロサービスのテスト戦略ほか(
- 20220719 RubyのGCが高速化、RuboCopのストレスを減らす4つの方法、Defensive CSSほか
- 20220711前編 AR::RelationにCTEを利用できるwithメソッドが追加、Propshaftアップグレードガイドほか
- 20220705後編 6月のRubyコア動向、Stack Overflowアンケート結果ほか
- 20220704前編 マイグレーションをStrategyパターンで拡張可能にほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)