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

週刊Railsウォッチ: 『RubyとRailsの何が強いのか』、書籍『Ruby on Railsステップアップ』ほか(20221213前編)

こんにちは、hachi8833です。ウォッチ20221129で紹介した"Railsに関わる技術の体系化を目指した本
"が、『Ruby on Railsステップアップ』というタイトルになってKindle Unlimitedで読めるようになりました🎉

参考: Rails初・中級者向けの技術の体系化を目指した本を書きました[追記・宣伝あり]


つっつきボイス:「早くもKindle Unlimitedで読めるようになった🎉」「Kindle Unlimitedはチェックアウトした本が上限に達するとどれかを返却する必要があるんですよね」「そうそう」「自分のKindle Unlimitedの上限を見たら20冊まででした」

参考: Kindle Unlimitedについて - Amazonカスタマーサービス

週刊Railsウォッチについて

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

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

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

🔗 PostgreSQLのcitext型カラムの検索時にlower()を使わないようにした

動機/背景
lower()を使うことで、インデックスが存在していてもインデックスの利用が妨げられてしまう。

詳細
インデックスはuniquenessを持つカラムに存在するのが典型的だが、validates_uniqueness_of ..., case_sensitive: falselower()が追加される。

しかしインデックス自体がlower()ありで定義されていなければ、lower()なしのクエリでインデックスが使われなくなってしまう。
検索エンジンでトップに出てきた資料でも、私たちのドキュメントでも、インデックスにlower()を追加することは推奨されていない。

驚き最小の原則に則り、通常の(lower()のない)インデックスを用いるために、検索クエリでlower()を利用しないことを提案する。

追加情報
インデックス定義にlower()を追加しないとインデックスが効かないことに気づいて実直にlower()を追加した人にとっては、この改修がパフォーマンス上のリグレッションと受け取られるだろう。
同PRより


つっつきボイス:「citextのciって何だろうと思ったら、case insensitive(=大文字小文字を区別しない)の略なのか」「ぽすぐれにこんな型があったとは」「インデックスが作成されているcitext型のカラムを検索する際に本来不要なlower()を使って検索するとインデックスが効かなくなってしまうので、それを修正したんですね👍」

参考: PostgreSQL 14.5文書 F.8. citext
参考: PostgreSQL: Documentation: 15: 9.4. String Functions and Operators -- lower()

🔗 ActiveSupport::Inflector.transliterateのパフォーマンスを改善

動機/背景
ActiveSupport::Inflector.transliterateは、既にASCIIになっている文字列が渡される場合はまったく実行する必要がない。
これはActiveSupport::Inflector.parameterizeにも影響する。
解決されるissue: #46569
詳細
このプルリクは、文字列が既にASCIIの場合に高価な処理の実行を回避するためのチェックをtransliterateに追加する。
追加情報
ベンチマークは以下を示している。
* transliterateが実際に必要な場合(文字列に非ASCII文字がある場合)のパフォーマンスは変わらない
* 普通の長さのASCIIのみの文字列(30文字以内)の場合、現行のコードは20倍遅くなる。
* 非常に長いASCIIのみの文字列(2000文字以内)の場合、現行のコードは670倍遅くなる。
同PRより


つっつきボイス:「ActiveSupport::Inflectorは単数形と複数形の活用を行う機能」「personをpeopleに変換したりするヤツですね」

参考: Rails API ActiveSupport::Inflector

transliterateは?」「調べてみると、üueに変換したりöoeに変換したりするんですって」「思い出した、アクサンやウムラウトみたいな記号を取り除いた文字に変換するメソッドでした」「音訳ということは文字から発音を得るということかな」「この改修では、既にASCIIになっていれば不要なtransliterateをスキップすることで最適化したんですね👍」

# activesupport/lib/active_support/inflector/transliterate.rb#L64
    def transliterate(string, replacement = "?", locale: nil)
-     string = string.dup if string.frozen?
      raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String)
      raise ArgumentError, "Cannot transliterate strings with #{string.encoding} encoding" unless ALLOWED_ENCODINGS_FOR_TRANSLITERATE.include?(string.encoding)

+     string = string.dup if string.frozen?
+     return string if string.ascii_only?
      input_encoding = string.encoding

参考: Rails API transliterate -- ActiveSupport::Inflector
参考: アクセント符号 - Wikipedia
参考: ウムラウト - Wikipedia

transliterate: (…を)字訳する、書き直す、音訳する、(…を)(…と)書き直す
英語「transliterate」の意味・使い方・読み方 | Weblio英和辞書より

Rails: ActiveSupport::Inflectorの便利な活用形メソッド群

🔗 AbstractAdapter#lockをデフォルトでスレッドローカルにする

修正: #45994

Ruby 3.0.2以来割とよくあるのが、トランザクション内でfiberを使うとデッドロックするという問題。

Post.transaction do
 enum =  Enumerator.new do |y|
   y.yield Post.first # ここで詰まる
 end
 enum.next
end

Rubyの#17827の変更によって、Monitorの所有者がThreadではなく、呼び出し元のFiberに変わったのが原因。

また、Active RecordのコネクションプールがThread単位のため、あるFiberが、他のFiberが所有するロックを取得しようとしても解決されないという状況になってしまう。

#46519では、このロックはシステムテストでしか必要とされていなかったので、ロックをオプショナルにした。

このプルリクは、Ruby 2.7までのMonitorのように振る舞う別のロック実装を導入する。ActiveSupport::IsolatedExecutionState.contextがThreadの場合はこちらが使われる。
Fiberの場合は、Fiberベースのstdlibから派生した実装を引き続き利用する。
共著: @wildmaples
FYI: @eileencodes @matthewd @rafaelfranca @ngan @simi
同PRより


つっつきボイス:「先週に続いてスレッド絡みの改修ですね」「トランザクションでRubyのFiberがデッドロックすることがあったらしい」「あ〜、その処理がThreadで動いているのかFiberで動いているのかを区別してロック方法を分けないといけないのか↓」「言われてみればたしかに」

# activerecord/lib/active_record/connection_adapters/abstract_adapter.rb#L175
-     def synchronized=(synchronized) # :nodoc:
-       @lock = if synchronized
+     def lock_thread=(lock_thread) # :nodoc:
+       @lock =
+       case lock_thread
+       when Thread
+         ActiveSupport::Concurrency::ThreadLoadInterlockAwareMonitor.new
+       when Fiber
          ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
        else
          ActiveSupport::Concurrency::NullLock
        end
      end

参考: class Fiber (Ruby 3.1 リファレンスマニュアル)

Ruby 3: FiberやRactorでHTTPサーバーを手作りする(翻訳)

🔗 globalidのセキュリティ修正(現在オープン)

GlobalIDとSignedGlobalIDは、それら自身に.findメソッドがあるので、モデルとして指定できる。
SignedGlobalIDは内部でMarshalを使っているので、secret_key_baseが漏洩すると想定外のRCE(リモートコード実行)が発生する危険がある。
このプルリクは、GlobalIDとSignedGlobalIDが標的にされないようにする。
同PRより


つっつきボイス:「これはruby-jp Slackのセキュリティ関連チャンネルで見かけたプルリクです」「SignedGlobalIDの内部でMarshalが使われているので、#findに対して想定外の任意文字列を渡せるようなコードを書いていると、そのキーの解決時にmarshalが展開されてRCEになるということのようですね」「今のところ特に動きはないようです」「この問題を実際に悪用して攻撃するには、おそらくGlobalIDに相当する部分に危険なコードを渡したうえで呼び出せるようにする必要がありそうなので、攻撃の難易度はかなり高いんじゃないかな」「なるほど」

# lib/global_id/global_id.rb#L53
  def model_class
-   model_name.constantize
+   model =  model_name.constantize
+   if model <= GlobalID
+     raise ArgumentError, "GlobalID and SignedGlobalID cannot be used as model_class."
+   end
+   model
  end

参考: module Marshal (Ruby 3.1 リファレンスマニュアル)

rails/globalid - GitHub

🔗Rails

🔗 『RubyとRailsの何が強いのか』


つっつきボイス:「@knuさんが公開したスライドです」「RubyとRailsの強みと弱みについてまとまっていますね👍: 縛りが少ない分、制約の強制力も弱いというのもそのとおりだと思います↓」

「Railsは中身を詳しく知らなくても動かせるけど、自分の思い通りに正しくカスタマイズしようとすると、やはりそれなりに難しい」「そうですよね」

「"RuboCopはデフォルト設定だとつらい"は同意」「先週話題にしたMethodLengthなんかがそうですね(ウォッチ20221207)」

参考: Metrics/MethodLength :: RuboCop Docs

「このページ↓はBPS社内Slackでも他の言語の開発者から"これが鉄板構成なんですか?"という質問がありましたね」「そうそう、自分はこの構成が鉄板かどうかは人それぞれだと思います」

「@joker1007さんのツイートも納得↓」


つっつきの後で、『RubyとRailsの何が強いのか』は以下の「Qiita Night 〜Ruby〜」の発表だと社内で教わりました。

参考: Qiita Night~Ruby~ - connpass

さらに、その次の@QUANONさんによる発表で「Railsの情報をキャッチアップする方法」にTechRachoもリストアップいただいたことも教わりました(29:52)🎉。ありがとうございます!

🔗 RSpec記事2本


つっつきボイス:「いつの頃からか、Controller specよりRequest specの方が推奨になりましたね」「そういえばRequest specとController specの違いが当初よくわからなかった」「Controller specはコントローラ呼び出しのタイミングからテストが始まるけど、Request specは内部的に実際にリクエストを投げてテストするので、前後にミドルウェアが存在する場合もより現実に近い形でテストが動くはず」「なるほど」

参考: 「EverydayRails」のcontrollersのテストをrequest specで書き換える

書籍でも触れられていますが、RailsのControllerをテストする際にはRails4系統の時にはController specが用いられていましたが、現在ではそれらのメソッドは非奨励となりRequest specを用いたテストが奨励されているそうです。
「EverydayRails」のcontrollersのテストをrequest specで書き換えるより

「Controller specはHTTP 200かどうかのテストぐらいならさらっと書けるけど、それ以外のテストを書こうとすると結局request spec相当のテストの仕方をしないとちゃんと意味のあるテストになりづらいかなと思います: コントローラにセットされたVIEWに渡されるインスタンス変数のチェックなどはできるけど、それならレスポンスHTML自体をexpectした方が安心できますね」

参考: 200 OK - HTTP | MDN


「こちらはjnchitoさんのRSpec記事です」「遅いテストを普段はスキップする」「アプリの規模がある程度以上大きくなると、何らかの形でやりたくなりますね」

🔗 その他Rails


つっつきボイス:「The Rails Foundationのエグゼクティブディレクターですか」「読んでみたら普通に雇用募集だった」「報酬は年135,000ドル」

参考: Executive Director - The Rails Foundation -- apply.workable.com


前編は以上です。

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

週刊Railsウォッチ: JRubyが9.4.0.0でRuby 3.1に対応、IRB v1.5.0リリースほか(20221207後編)

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

Rails公式ニュース


CONTACT

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