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

週刊Railsウォッチ: RailsConf 2022の動画が公開、マイクロサービスのテスト戦略ほか(20220725前編)

こんにちは、hachi8833です。Kaigi on Rails 2022のCFPとTRICK 2022のエントリーは共に7月いっぱいで締め切りです。

週刊Railsウォッチについて

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

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

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

コミット差分: Comparing @{2022-07-15}...main@{2022-07-21} · rails/rails

🔗 ActionDispatch::ServerTimingが既存のServer-Timingヘッダーを上書きしないよう修正

修正: #45607
小さな修正。ミドルウェアでServer-Timingヘッダーに何かを追加し、そのミドルウェアの後にActionDispatch::ServerTimingがあると値が上書きされてしまう。このプルリクは、ActionDispatch::ServerTimingの振る舞いを上書きではなく追加に修正する。
同PRより


つっつきボイス:「ServerTimingミドルウェアは昨年追加されたヤツですね(ウォッチ20211011)」「Server-Timingヘッダーを複数回設定したときに上書きせずにカンマ区切りでprependするようにしたんですね↓」

# actionpack/lib/action_dispatch/middleware/server_timing.rb#L13
    def call(env)
      events = []
      subscriber = ActiveSupport::Notifications.subscribe(/.*/) do |*args|
        events << ActiveSupport::Notifications::Event.new(*args)
      end
      status, headers, body = begin
        @app.call(env)
      ensure
        ActiveSupport::Notifications.unsubscribe(subscriber)
      end
      header_info = events.group_by(&:name).map do |event_name, events_collection|
        "#{event_name};dur=#{events_collection.sum(&:duration)}"
      end
+
+     header_info.prepend(headers[SERVER_TIMING_HEADER]) if headers[SERVER_TIMING_HEADER].present?
      headers[SERVER_TIMING_HEADER] = header_info.join(", ")

      [ status, headers, body ]
    end

「ところで、こういうことをやり始めると、これまで何度も話題になったOpenTelemetry(ウォッチ20210601)のような統一的な方法でやりたくなるのではという気がしますね: ちなみにOpenTelemetryはアプリケーションやミドルウェアのトレースログを一括で扱える規格で、あらゆるミドルウェアやアプリケーションから統一的に取得して紐付けて処理できる」

参考: OpenTelemetry
参考: OpenTelemetry とは  |  Google Cloud

🔗 ネストしたSQL関数を安全なSQL文字として許容する

危険なクエリの検出による非推奨メッセージやエラーについて、余分な偽陽性を削減する機会があると思える。#36448と同様に、他の関数を受け取っている関数(length(trim(title))など)を許容するのは妥当と思われる。
同PRより


つっつきボイス:「デンジャラスクエリメソッドって強そうな言葉」「クエリが危険かどうかの判定を少し緩めるということみたい」「length(trim(title))のようなネストしたSQL関数も使っていいと認める↓: たしかにこう書きたくなることもありそう」

# activerecord/test/cases/unsafe_raw_sql_test.rb#183
  test "pluck: allows nested functions" do
    title_lengths_expected = Post.pluck(Arel.sql("length(trim(title))"))

    title_lengths = Post.pluck("length(trim(title))")

    assert_equal title_lengths_expected, title_lengths
  end

🔗 Active Storageのpreviewrepresentationでも定義済みバリアントを使えるようにする

従来の名前付きバリアントは添付ファイルでバリアントメソッドを呼び出すときしか利用できなかった(variable?ではないがpreviewable?であるファイルでは定義済みメソッドを利用できなかった)。
このパッチによって、previewメソッドやrepresentationメソッドでもバリエーション名をシンボルとして渡せるようになる。

class User < ActiveRecord::Base
  has_one_attached :file do |attachable|
    attachable.variant :thumb, resize_to_limit: [100, 100]
  end
end
<%= image_tag user.file.representation(:thumb) %>

同PRより


つっつきボイス:「Active Storageの改修」「previewはActive Storageのプレビューのことなんですね」「今までpreviewrepresentationにバリアントを渡せなかったとは知らなかった」

参考: ActiveStorage::Blob::Representable
参考: §2.2.13.6 :variantsオプション -- レイアウトとレンダリング - Railsガイド

🔗 production向けのcredentialファイル生成にのみsecret_key_baseを含めるよう修正

#45543の続き。
tmp/development_secret.txtsecret_key_baseが保存されて混乱するのを防ぐ。
同PRより

# railties/CHANGELOG.md#L13より(編集済み)
+* `development`と`test`を除き、
新規生成する環境ごとのcredentialファイル(`config/credentials/production.yml.enc`など)に
利便性のため(`config/credentials.yml.enc`と同様に)`secret_key_base`を含めるようになった。

つっつきボイス:「先週のcredential改修の続きだそうです(ウォッチ20220719)」「production以外ではsecret_key_baseをデフォルトで含めないようにする、なるほど」「ちなみに個人的にはcredentialより環境変数を使いたい方です: credentialが依存するマスターキーは流出の可能性が付きまとうので」

🔗 ActionView::OutputBufferを高速化

MRIの文字列結合には、結合先がStringの場合にのみ利用できる最適化が多数あるが、Stringのサブクラスを使うとこれらの最適化が無効になってしまう。
この違いは、YJITとVMの両方でString#<<のパフォーマンスが著しく向上しているRuby 3.2でさらに重要性を増す。
そういうわけで、このバッファをStringのサブクラスにしないのが理想。さいわいAction Viewのバッファは内部利用に限定されているので、さほど手間をかけずに継承をコンポジションに置き換えられる。

ベンチマーク:
ActionView::OutputBuffer:   147644.2 i/s
        optimized buffer:   228001.4 i/s - 1.54x  (± 0.00) faster

ソース: https://gist.github.com/casperisfine/7199579a138e268fda71d6a91366af49
注: この50%の高速化はコンテキスト次第で変わる(テンプレートを別のものに変えると大きく変わる)が、常に高速。
同PRより


つっつきボイス:「RubyのStringを継承するサブクラスがStringよりも遅くなるのは昔からよく言われていますね」「なるほど」「Stringを継承しているActiveSupport::SafeBufferを使うと遅いので、継承を避けるためにコンポジションの形でStringクラスを直接使うようにすることで高速化したんですね↓」

# actionview/lib/action_view/buffers.rb#L21
- class OutputBuffer < ActiveSupport::SafeBuffer # :nodoc:
-   def initialize(*)
-     super
-     encode!
+ class OutputBuffer # :nodoc:
+   def initialize(buffer = "")
+     @buffer = String.new(buffer)
+     @buffer.encode!
+   end
+
+   delegate :length, :blank?, :encoding, :encode!, :force_encoding, to: :@buffer
+
+   def to_s
+     @buffer.html_safe
+   end
+   alias_method :html_safe, :to_s
+
+   def to_str
+     @buffer.dup
+   end
+
+   def html_safe?
+     true
+   end

「この改修は、オブジェクト指向プログラミングで一般によく言われる"継承よりコンポジション"に沿っているとも言えるでしょうね: ここでString.new(buffer)のようなコンポジションに変えたのは、RubyではStringクラスを継承するよりも速いというRuby特有の事情に依存していると思いますが」「50%も速くなるのはちょっとびっくり」「おそらくですが、Stringを直接使うとCで実装されたメソッドが使われると思います: 逆に継承を使うと継承パスをたどったりしないといけない」

参考: 継承よりもコンポジションを選ぶのとデザインパターンの話

🔗Rails

🔗 Hanami v2.0.0.beta1リリース(RubyFlowより)

# 同記事より
⚡ tree .
.
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app
│   ├── action.rb
│   └── actions
├── config
│   ├── app.rb
│   ├── routes.rb
│   └── settings.rb
├── config.ru
├── lib
│   ├── bookshelf
│   │   └── types.rb
│   └── tasks
└── spec
    ├── requests
    │   └── root_spec.rb
    ├── spec_helper.rb
    └── support
        ├── requests.rb
        └── rspec.rb

9 directories, 14 files

つっつきボイス:「Hanami v2.0.0.beta1が出た」「Hanamiは使ってないので何とも言えないけど、Rails以外にもRubyのフレームワークがあって着実に進化しているのは健全なことだと思いますね」

🔗 マイクロサービスのテスト戦略


つっつきボイス:「Railsに限定しないテスト記事です」「マイクロサービスのテストは比較的設定しやすい方かなという気もしますけどね」

「こういうコントラクトテストは大事↓」「記事では、2つのサービスがインターフェイスを介して結合するとコントラクトが形成されるとありますね」「大雑把には、サービス間のインターフェイスが満たすべき要件をコントラクトとして記述する感じかな」


同記事より

参考: ソフトウェア テストの手法 - パート 1: モッキング、スタビング、コントラクト テスト

「たしか英国政府のRailsテスト記事にもコントラクトテストの話があった↓」

英国政府によるRailsアプリケーションテストの新標準(翻訳)

「記事は統合テストの話に続いて、インプロセスとアウトプロセスのコンポーネントテストのような具体的な話もある↓」「要点を押さえたまっとうな記事👍」



同記事より

🔗 RailsConf 2022の動画が公開


つっつきボイス:「2日ぐらい前(編注: 7/21のつっつき時点)に一気に公開されていましたね」「キーノートスピーチ↓も含まれていたし、アップロードは終わっているようですね」「YouTubeの通知がこれで埋まりまし た 」「自動の字幕ももうついているのがありがたい🙏」

🔗 その他Rails

つっつきボイス:「7/26(火)の開催だそうです」「10年続くRailsアプリ開発か」「Railsを長く使っている会社が増えてきたので、こうしたノウハウが蓄積されているのを感じますね」


「クラスメソッドとインスタンスメソッドの違いですか」「Rubyの世界ではクラスもオブジェクトだから、その視点で見れば実は両者に違いはないとも言える」「まあそうですけど😆」「もちろん設計として両者をどう使い分けるかは依然として問題ですけどね」

「基本的にはインスタンスメソッドにする、たしかに」「パターン1のようなユーティリティ関数的なものは自分もクラスメソッドでいいかなという気持ち↓」

# 同記事より
# 日付として有効であればtrue、そうでなければfalseを返す
Date.valid_date?(2022, 6, 30)  #=> true
Date.valid_date?(2022, 6, 31)  #=> false

「パターン2のようなファクトリーメソッド的なものも基本的にはクラスメソッドにするでしょうね↓」「単純なものならクラスメソッドにするけど、テンプレートから生成するようなときはインスタンスメソッドにすることもあるかな」

# 同記事より
# 文字列をパースしてDateオブジェクトを生成する
Date.parse("2022-06-30") #=> #<Date: 2022-06-30 ((2459761j,0s,0n),+0s,2299161j)>

「パターン3は"そのクラスの複数のインスタンスを扱うメソッド"↓」「export_as_xlsxは説明用の架空のメソッドなんですね」「こういうのって、そもそもそのクラスのクラスメソッドにすべきなのかどうかという観点もあるかも」「それ同じこと思いました」

# 同記事より
# carsのデータをExcelファイルに出力する(架空のメソッド)
Car.export_as_xlsx(cars)

「この種のメソッドをクラスメソッドにすると経験上バグの元になりやすかった覚えがある: exportするクラスメソッドが同時に複数呼ばれるとtmpファイルが競合して壊れるとか」「あるある」「これはパターン1の関数的なメソッドに分類できそうな気もしますね」


「ところで、こういう議論を随分長いことやっていると、初心者がどういうところでつまづくかを思い出せなくなってくると実感します」「後からもう一度初心者の気持ちになるのは難しいですよね」

「最近学生からこんな相談を受けたんですよ: iOSアプリからデータを定期的に受け取って保存したものをビジュアル表示しようと思っていたらサーバーが必要だろうと言われたけど、サーバーで何かしたことがなくてイメージが湧かないのでどうアプローチしたらいいかって」「言われてみるとサーバーサイドを最初から説明するのって大変そうですね」「iOSの書籍ではほとんどサーバーサイドに触れていないし、その学生にしてみればサーバーは枝葉であって本質的ではないんですよ」「たしかに」「とりあえずFirebaseのデータストアあたりを使ってみてはどうかと回答しましたけど」

「そのとき調べていて気づいたんですけど、サーバーでJSONデータを受け取ってDBに保存して、所定のURLにアクセスするとDBから読み出して表示するだけみたいなシンプルなチュートリアルって、意外と今の世の中にないんですよ」「オススメするならRailsチュートリアルが王道だと思うけどボリュームありますもんね」「サーバーサイド未経験でiOSで何かしたい気持ちが高まっている学生に、RailsやLaravelみたいな本格的なものをがっつり学ばせるのはちょっと忍びなかった」「自分たちのときはたいていPerl CGIで掲示板なんかを作ったりした経験がありましたけど、言われてみると今のサーバーサイドでそれに相当する汎用性の高い経験を手軽に得られる感じではないですね」

参考: CGIの概要 - とほほのWWW入門


前編は以上です。

バックナンバー(2022年度第3四半期)

週刊Railsウォッチ: Active Modelのパターンマッチングがいったん取り消し、Ruby技術者認定試験が10月3日から3.xに対応ほか(20220719)

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

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

Rails公式ニュース

RubyFlow

160928_1638_XvIP4h


CONTACT

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