Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ(20210201前編)Webpackerのガイドがマージ、RailsはRuby 3でどのぐらい速くなったかほか

こんにちは、hachi8833です。

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsの最新情報などの記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

今回は以下の公式更新情報が出ていました。


つっつきボイス:「今週は新年第2号の公式更新情報出てるんですね」「Railsリポジトリのmaster->mainブランチ変更は既にウォッチでも報じました(ウォッチ20210125)が、公式更新情報で以下のツイートにもリンクしていました↓」「今はブランチ名をmaster->mainに移行するのが世の中の流れですね」

その他は以下のコミットリストのChangelogを中心に見繕いました。

🔗 WebpackerガイドがEdge Guidesに追加される


つっつきボイス:「BPSの社内Slackに流れてた情報ですね」

「Rails Guides(英語)とEdge Guides(英語)を見てみたところ、WebpackerガイドはまだEdge Guidesにしかありませんでした↓」「それはまだmaster、じゃなくてmainブランチにしか入っていないからですね: 通常は次のリリースで正規のRails Guidesに載ることになります」「あ、そうでした」

Edge Guides: Webpacker — Ruby on Rails Guides

Rails Guides: Ruby on Rails Guides

「WebpackerのREADME↓も簡単にチェックしてみたところ、Edge GuidesのWebpackerドキュメントはこれとは違う内容でした」「必要そうなことはWebpacker Guidesにだいたい盛り込まれている感じかな👍」

「Webpackerのドキュメント、ありそうでなかったのか」「そうなんです、Rails 5の頃にこんなつなぎ記事↓を書いたのを思い出しました」「Webpackerがリリースされてから随分経つのに今までなかったとはね...」「このプルリクは"いいね"が多めだったので、それだけ待ち望まれていたということでしょうね」

【保存版】Rails 5 Webpacker公式ドキュメントの歩き方+追加情報

🔗 content_typeメソッドがContent-Typeヘッダーをそのまま返すよう修正


つっつきボイス:「なるほど、変更後はcontent_typeメソッドがtext/csv; header=present; charset=utf-16"の部分を加工せずに丸ごと返してくれるようになったんですね」

参考: Content-Type - HTTP | MDN

「Rails Guides(英語版)の資料も更新されていて、デフォルトの挙動をreturn_only_request_media_type_on_content_typeコンフィグで変更できるようになっている↓」「既存のRailsアプリがcontent_typeメソッドを使っていたら、このコンフィグパラメータで以前の挙動に戻せますね」「なるほど」

# guides/source/configuring.md#1050
#### For '6.2', defaults from previous versions below and:
 - `config.action_view.button_to_generates_button_tag`: `true`
 - `config.action_view.stylesheet_media_default` : `false`
 - `config.active_support.key_generator_hash_digest_class`: `OpenSSL::Digest::SHA256`
 - `config.active_support.hash_digest_class`: `OpenSSL::Digest::SHA256`
+- `config.action_dispatch.return_only_request_media_type_on_content_type`: `false`

参考: Rails アップグレードガイド - Railsガイド

「なるほど、MIMEタイプだけ欲しい場合はActionDispatch::Request#media_typeメソッドで取れるのか」

参考: メディアタイプ - Wikipedia

「以下でmedia_typeメソッドにも修正が入ってますね↓」

🔗 コンフィグファイルで互換性を維持するRails

「この変更では、ライブラリに変更をかけるときに既存のコードに与えるインパクトをできるだけ抑えているのがいいですね: いろいろと参考になります👍」「そうですね」

「Railsは互換性をあんまり気にせずに変えるときは変えるのかなと何となく思ってましたけど、そうでもないんですね」「Railsがデフォルトの挙動を変えることはたまにありますが、そういう場合でも以前の挙動が必要な人はコンフィグで調整可能にする余地を作っておくあたりがRailsらしいと思います」「たしかに!」

「きっとこのコンフィグも、今後既存のRailsアプリでrails app:updateするときに生成されるconfig/initializers/new_framework_defaults.rbに追加されるんでしょうね↓」

参考: 1.5 フレームワークのデフォルトを設定する -- Rails アップグレードガイド - Railsガイド

「その場合は古い設定のコンフィグが追加されるんでしょうか?」「Railsのこういうコンフィグは後方互換性を維持する方向で生成されるはずですし、少なくともrails app:updateすると"こういう設定があるからチェックして"みたいに知らせてくれますよ」「え、やったことなくて知らなかった」「rails app:updateは基本的に従来のデフォルトの挙動を使うコードがなるべくそのまま使えるように作られていると思います」「そうだったんですね」


ActionDispatch::Request#content_typeがContent-Typeヘッダーをそのまま返すようになった。
従来のActionDispatch::Request#content_typeが返す値にはcharsetパートが含まれていなかった。この振る舞いを変更して、charsetパートをそのままの形で含むContent-Typeを返すようにした。MIMEタイプだけが欲しい場合はActionDispatch::Request#media_typeを使っていただきたい。

# 変更前
request = ActionDispatch::Request.new("CONTENT_TYPE" => "text/csv; header=present; charset=utf-16", "REQUEST_METHOD" => "GET")
request.content_type #=> "text/csv"
# 変更後
request = ActionDispatch::Request.new("Content-Type" => "text/csv; header=present; charset=utf-16", "REQUEST_METHOD" => "GET")
request.content_type #=> "text/csv; header=present; charset=utf-16"
request.media_type   #=> "text/csv"

Rafael Mendonça França
Changelogより大意

🔗 LogSubscriberを設定したコントローラでthrowしたときのエラーを修正


つっつきボイス:「プルリクメッセージを見ると、コントローラで投げた例外がどこで拾われるかという話になっている」「リグレッションが発生したのを修正したのか: &を追加している↓のを見ても、これは改善ではなく修正でしょうね」「修正前はnil参照みたいになる場合があったということかな」

# actionpack/lib/action_controller/log_subscriber.rb#L26
-       if status.nil? && (exception_class_name = payload[:exception].first)
+       if status.nil? && (exception_class_name = payload[:exception]&.first)
          status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
        end

「LogSubscriberをコントローラに設定し、そのコントローラのアクション内でraiseすると、raiseされた例外がLogSubscriberに吸い込まれて、その後コントローラに投げ返してくれなくなったらしい」

f0fdeaa↓を見ると、firstを呼ぶ前のpresent?による存在確認を省略したことでこのリグレッションが起きたようですね」「なるほど、これですか」「一見問題なさそうな最適化が思わぬところに影響したのか」

# actionpack/lib/action_controller/log_subscriber.rb#L25
        if status.nil? && payload[:exception].present?
          exception_class_name = payload[:exception].first
        if status.nil? && (exception_class_name = payload[:exception].first)
          status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)

コントローラのアクションでthrowが使われていると、マッチするものがRackミドルウェア周辺でキャッチされても:exceptionがイベントペイロードにまったく現れなくなっている。
理由は、ActiveSupport::Notifications::Instrumenter.instrument:exceptionをrescueハンドラー内で設定しているため。しかしrescueは以下のようなthrow/catchシナリオでは決して呼び出されない。

catch(:halt) do
  begin
    throw :halt
  rescue Exception => e
    puts "rescue" # ここに到達することはない
  ensure
    puts "ensure"
  end
end

失われたexceptionは実際にはRails 6.1.0より前のバージョンでは扱えていたが、f0fdeaaの最適化のときに、:exceptionが存在することを前提とする部分が更新された。そのコミットを取り消すのが修正としては最も簡単。そういうわけで本PRはリグレッション修正とみなせる。
なお、この問題はRodauthをRailsで使っていて見つけた。
同PRより大意

jeremyevans/rodauth - GitHub

🔗 media=screenstylesheet_link_tagのデフォルトから削除


つっつきボイス:「stylesheet_link_tagを使うとCSSがmedia=screenになってた、ということ?」「それをデフォルトから外してデフォルトがallになったようですね」

# 変更前

stylesheet_link_tag "style"
#=> <link href="/assets/style.css" media="screen" rel="stylesheet" />

# 変更後

stylesheet_link_tag "style"
#=> <link href="/assets/style.css" rel="stylesheet" />

「プルリクタイトルにlegacyって書いてありますけど、media=screenって古いんでしょうか?」「従来はデフォルトでmedia=screenだったけど、わざわざ指定する意味がないから外したのかな?」

参考: メディアクエリの使用 - CSS: カスケーディングスタイルシート | MDN

「DHHが立てたissue↓にこんなことが書かれてる」

これを修正しよう:

歴史的理由によって、media属性は常にデフォルトで"screen"に設定されるので、すべてのメディアタイプを適用するにはスタイルシートで明示的に"all"を設定しなければならない。

stylesheet_link_tag "http://www.example.com/style.css"
# => <link href="http://www.example.com/style.css" media="screen" rel="stylesheet" />

config.assets.stylesheets.media_default的なものを追加したうえで、既存のアプリではデフォルトを"screen"にすればよいが、新しいアプリでは意味がないので、その後でmedia: "all"による上書きをscaffoldテンプレートから削除しよう(訳注: 本PRで削除されたそうです)。
#41213より(by DHH)

「今のissueに書かれている情報を見た限りではですが、従来のようにデフォルトがmedia=screenだとそうした部分が直感的でないから、デフォルトをallにすることでブラウザ画面用のCSSをデフォルトで印刷にも使うよう修正したんだろうと思います」「たぶんそうでしょうね」

「普段あまり気にしない部分ですが、数年に一度ぐらいはmedia="print"を指定することもあったので、stylesheet_link_tagと書いただけなのにmedia="screen"が付けられるより、その方が自分も嬉しいですね」「たしかに」

🔗 rescue_fromでActive Jobのすべてのエラーを拾えるよう修正

追記(2021/02/12)

ご指摘に基づき、本見出しに「Active Jobの」を追記しました。ありがとうございます🙇


つっつきボイス:「なるほど、rescue_fromで拾う例外が従来StandardErrorだけだったのを、すべての例外を拾えるようにしたようですね↓」

このコミットより前は、rescue_fromハンドラで扱われる例外はStandardErrorだけだった。
この変更によって、rescue句がすべてのExceptionオブジェクトをキャッチするようになり、StandardErrorを継承していないExceptionクラスでrescueハンドラーを定義できるようになる。
つまりStandardErrorの外にあるExceptionをrescueするrescueハンドラーは、今回の変更前に扱えなかった他の例外もrescueできるようになるだろう。
同PRより大意

「たしかRubyの仕様では、改修前のように単にrescue => exceptionと書くとStandardErrorを拾うんですよ」「あ、そうでしたっけ」

# activejob/lib/active_job/execution.rb#L41
    def perform_now
      # Guard against jobs that were persisted before we started counting executions by zeroing out nil counters
      self.executions = (executions || 0) + 1
      deserialize_arguments_if_needed
      run_callbacks :perform do
        perform(*arguments)
      end
-   rescue => exception
+   rescue Exception => exception
      rescue_with_handler(exception) || raise
    end

「Rubyのドキュメントにも書かれてるのを見つけました↓」「ホントだ」「つまりRubyの仕様がそうなってます: 一般にはStandardErrorを継承しますけどね」

参考: 制御構造 (Ruby 3.0.0 リファレンスマニュアル)

error_type が省略された時は StandardError のサブクラスである全ての例外を捕捉します。Rubyの組み込み例外は(SystemExitInterrupt のような脱出を目的としたものを除いて) StandardError のサブクラスです。
docs.ruby-lang.orgより

Exceptionを継承するのって非推奨なんでしょうか?」「非推奨とまではいかなかったと思いますが、Rubyでアプリケーションの例外を扱うときはStandardErrorを継承するのが一般的だったと思います」「なるほど、アプリ開発のお作法的な感じですか」

「RubyのExceptionのクラス階層↓を見るとわかりますが、ExceptionStandardErrorより上位にあるので、改修後のようにrescue Exception => exceptionと書くとすべての例外を拾えるようになります」「なるほど!」

参考: library _builtin (Ruby 3.0.0 リファレンスマニュアル)


docs.ruby-lang.orgより

Exceptionの直下にあるNoMemoryErrorNotImplementedErrorSecurityErrorのような例外クラスはStandardErrorでは拾えません」「なるほど理解できました」

NoMemoryErrorあたりはアプリで表示する可能性がゼロではないかもしれませんが、SyntaxErrorはコードが解析される時点のエラーなので、まずアプリでは表示されないでしょうね」「アプリが立ち上がる前のエラーだからそうなるのか」

「そういう例外を拾えるように改修したということですね」「なるほど」

「何かの機会に、どうも取れない例外があるなって気づいて修正したのかもしれませんね: たとえばNotImplementedErrorなら、DI(Dependency Injection)を使うつもりでスタブのクラスを書いているときに、まだメソッドが実装されてないのにエラーが出なくておかしいなと思って気づく可能性がそこそこありそう」「ありそうですね」

参考: 依存性の注入 - Wikipedia

🔗 Active Storage向けのfixtureサポートを強化

fixture統合を進めるためのActiveStorage::FixtureSetActiveStorage::FixtureSet.blobが宣言されるようになった。
Changelogより


つっつきボイス:「お、Active Storageのfixtureサポートが増えた」「ActiveStorage::FixtureSetが追加されてActive Storage向けのfixtureが使えるようになったみたい」「Active Storageのテストでfixtureが添付ファイルも扱えるようになったということですね」「これはありがたい」「今まで添付ファイルってどうやってテストしてたんでしょうね」「使えるなら欲しいヤツ、いい機能だと思います👍」

Rails 7 API: ActiveRecord::FixtureSet(翻訳)

🔗Rails

🔗 RailsはRuby 3でどのぐらい速くなったか(Ruby Weeklyより)


つっつきボイス:「RubyやRailsのパフォーマンス記事でお馴染みのNoah Gibbsさんの新しい記事です」

Railsアプリに最適なAWS EC2インスタンスタイプとは(翻訳)

「記事を眺めた感じでは、Ruby 3.0にしたときのパフォーマンスの違いは微差というしかなさそう」「誤差の範囲っぽいですね」

「Ruby自体はRuby 2.0か3.0でら3倍速くなったんですよね」「記事下にあるカラフルなグラフがわかりやすそう↓」「お、なるほど」「グラフを見る限りでは、Ruby 2.7から3.0では実質ほぼ変わらないかな」


同記事より

「3.0に上げて遅くならなければいいと思います👍」「そうですよね」「もし上げて遅くなるとアップグレードしない人が出てくるかもしれませんが、Ruby 3.0でRailsが遅くなったわけではないからそこは大丈夫だと思います」「上げて大丈夫ですよ〜」

🔗 tzinfo-data

「記事ではtzinfo-data↓あたりでいろいろ苦労したみたい」「tzinfo-dataはrails newするとデフォルトで入ってきますね」「そういえば皆さんtzinfo-dataってどうしてます?自分は速攻で削除してますけど」「tzinfo-dataはWindowsでRailsを動かすためのものですよね」「そうなんです、Macで動かすとwarningが出てくる」

tzinfo/tzinfo-data - GitHub

「そのwarningを見るたびに、そもそもWindows環境でRailsを動かすことはほぼないよねという気持ちになります」「Windows環境でRailsを動かす人が少しでもいるうちは残るんでしょうね...」

参考: bundle installする際のtzinfo-dataのwarningがウザい - Qiita


「そういえば、この間ついにRuby 3.0でRails動かしてみましたよ」「お、どうでした?」「まだ動かしてヤッタバンザイしただけ😆」「気持ちわかります」「お気持ちお気持ち」「なおwaringやマイグレーション時にいろいろエラーが出ました」

🔗 ReactとRailsの新しいアーキテクチャ(Ruby Weeklyより)


つっつきボイス:「ReactとRailsの共存を進めている開発会社の記事のようです」「この図で見当が付きそう↓」


同記事より

「図を見た限りでは、現在のアーキテクチャ↑ではReact UI ViewでRailsのRESTful APIにアクセスしている: まさに伝統的なRailsアプローチ」

「そして今後目指すアーキテクチャの図↓では、従来のRailsバックエンドとビューもあるけど、React側にAPIライブラリをはさんだうえで、ReactがアクセスするバックエンドをRailsのRESTful APIから徐々にGraphQLに移行しようとしているようですね」「へ〜」「全部移行するのかもしれませんし、RailsのRestAPIも残すのかもしれませんが、いずれにしろRailsを一度引き剥がしてやり直したい感じがしますね」


同記事より

「このようなアーキテクチャ移行は最近よく見かけるやつですね」

🔗 GraphQLよもやま話

「今日ちょうどWebチームミーティングでRailsとGraphQLについての発表があったんですよ」「お、GraphQLやってるんですね」「自分もGraphQLに関わってみた感想としては、ReactとGraphQLの組み合わせはたしかに便利: でも便利であるがゆえに、GraphQLのエンドポイントで行う仕事が肥大化してくるんですよ」「何となくわかります」

「たとえばさっきの図で言うと、federated GraphQLの下に置くマイクロサービスを1個にするのか、それとも複数のマイクロサービスを扱うのかという選択肢があります」「ふむふむ」「そして後者を選ぶと、ファットなRailsモノリスの代わりにファットなfederated GraphQLができる、そういう未来をちょっと感じているところです」「自分もまさにそう思っています」

「具体的には、このfederated GraphQLで何もかもやろうとするとGraphQLのスキーマが巨大になるんですよ」「やっぱり育っちゃうんですね..」

「Railsの場合は、ルーティングにresourcesと1行書くだけでRESTfulなルーティングが使えますが、GraphQLはQueryとMutationがあって、しかもRestfulのCREATEやUPDATEなどもMutationで定義することになります」「ふむふむ」「それをやっていくと、GraphQLのフラットな名前空間に大量のGraphQLメソッドができてくるんですよ」「ありゃ〜」「ネストすることもできるんですが、いずれにしろ、GraphQLのスキーマはちょっと大きすぎじゃないかなと感じているところです」「うーむ」

参考: クエリのクエリとミューテーションの実行 - AWS AppSync
参考: GraphQLのクエリを基礎から整理してみた - Qiita

「そうやってGraphQLスキーマが育ちすぎたら当然エンドポイントを分割しようという話が出てきますけど、今度は分割されたエンドポイント同士のセッション管理をどうするかという問題も出てくるんですよ」「エンドポイントごとのログイン状態のようなstateをフロントエンド側のコードで判断するのは大変そう...」

「そうなったらAWSのAPI Gateway的な方法を使うことになってくるでしょうね: Railsウォッチでも何度か話題にしたEnvoy(ウォッチ20190212)も、そういう流れで出てきたんだろうと思っています」「なるほど」「きれいにやろうと思ったら、さっきの図で言うReactのAPI Libraryの層の下にもうひとつAPI Gateway的な層も必要になってくるんじゃないかと思ったりしますね」「また層が増えるのか〜」「というようなことを最近GraphQLを使いながら思いました」

参考: Amazon API Gateway(規模に応じた API の作成、維持、保護)| AWS
参考: Envoy Proxy - Home


「質問です: 図のfederated GraphQのfederatedは、ここではどういう状態を指すんでしょうか?」「技術用語ではfederationという言葉がよく使われますが↓、ここで使われているfederated GraphQLは複数のマイクロサービスを取りまとめているGraphQL、という程度の意味じゃないかと思います」「なるほど、辞書的には連合とかそういう意味でしたね」

参考: フェデレーション (federation)とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典


前編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210126後編)Google Cloud FunctionsがRubyをサポート、Ruby 3のパターンマッチングでポーカーゲームほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。