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

週刊Railsウォッチ: Rails 8マイルストーン、2023年のRails振り返り、Solid Queueほか(20240117前編)

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

本家が先に進んでいるので取り戻しにかかります。

🔗 assert_queriesassert_no_queriesActiveSupport::TestCaseで利用可能になった

動機/背景

作成されたクエリ件数が期待通りであるというアサーションのため、Rails内部ではassert_queriesassert_no_queriesが使われている。これらのアサーションはアプリケーション側でも有用だろう。

これらのアサーションをモジュールに切り出すことで、必要に応じてincludeできるようになる。
Active Recordが定義されると、ActiveSupport::TestCaseにこれらのアサーションが追加される。

Active Storage、Action View、Action Textでも、実装を重複させずにこのモジュールを使うようになった。
Active Recordのテストで内部的に使われるActiveRecord::TestCaseこれらのアサーションを実装した。ただし、これらは少し高度かつ込み入っており、SQLCounterクラスを使っている。とりあえず話を複雑にしないため、この実装は使っていない。

追加情報

これらのアサーションはActiveSupport::TestCaseに追加される。

class ArticleTest < ActiveSupport::TestCase
  test "queries are made" do
     assert_queries(1) { Article.first }
  end
end

同PRより


つっつきボイス:「assert_queriesで実際に実行されたSQLクエリの件数をチェックできるのね: N+1問題を手軽にチェックしたりするのに使えるかも」「assert_no_queriesはクエリ発行件数ゼロ専用のアサーションなんですね」

# activerecord/test/cases/assertions/query_assertions_test.rb#L12
      def test_assert_queries
        assert_queries(1) { Post.first }

        error = assert_raises(Minitest::Assertion) {
          assert_queries(2) { Post.first }
        }
        assert_match(/1 instead of 2 queries/, error.message)

        error = assert_raises(Minitest::Assertion) {
          assert_queries(0) { Post.first }
        }
        assert_match(/1 instead of 0 queries/, error.message)
      end
    end

    def test_assert_no_queries
      assert_no_queries { Post.none }

      error = assert_raises(Minitest::Assertion) {
        assert_no_queries { Post.first }
      }
      assert_match(/1 .* instead of 2/, error.message)
    end

Rails: Bulletで検出されないN+1クエリを解消する

🔗 image/svg+xmlが圧縮可能Content-Typeに追加された

image/svg+xmlActionDispatch::Staticの圧縮可能Content-Typeに追加された。

Georg Ledermann
同Changelogより

動機/背景

このプルリクは、ActionDispatch::Staticの圧縮可能Content-Typeにimage/svg+xmlを追加する。

(なお、これは@ledermannによる#42407をrebaseして再送信したものであり、2021年に@zzakがapprove済み)

詳細

元の説明文:

ActionDispatch::Staticの圧縮可能Content-Typeにimage/svg+xmlを追加することでGZIPやBrotliを利用可能にする。

同PRより


つっつきボイス:「image/svg+xmlというContent-Typeが圧縮可能になったんですね」「テスト用にRailsロゴのSVG画像も追加されてる」「お、fixturesの下に公共/gzipというディレクトリがありますね↓」「日本語だ」「意図があるとすれば、ディレクトリ名のエンコードが非ASCIIでも正常に動作するかどうかをチェックするとかかな」「それありそうですね」


同PRより

後で調べると、2013年のコミットでfixtureに日本語のディレクトリ名やファイル名が追加されていました↓。

参考: Fix Encoding::CompatibilityError when public path is UTF-8 · rails/rails@436ed51

🔗 TrilogyAdapterでDATABASE_URLにUNIXソケットを指定可能になった

TrilogyAdapterでsocketパラメータが設定済みの場合はhostを無視するようになった。

これにより、DATABASE_URL経由でUNIXソケット上にコネクションを設定可能になる。

DATABASE_URL=trilogy://does-not-matter/my_db_production?socket=/var/run/mysql.sock

Jean Boussier
同Changelogより


つっつきボイス:「?socket=/var/run/mysql.sockというクエリをDATABASE_URLで指定可能になったんですね」「ソケットを使うならURLのhost部に何があっても無関係でしょうね」

参考: UNIXドメインソケット - Wikipedia

🔗 Action MailboxとAction Textでもモデルのテーブル名にActive Recordのプレフィックス/サフィックス設定が効くよう修正


つっつきボイス:「昨年末に#50167でActive Recordのプレフィックス/サフィックス設定がActive Storageモデルのテーブル名で効くように修正されていましたね(ウォッチ20231222)」「上の2つはAction MailboxとAction Textについても同じ修正を行った、なるほど」

🔗 Railsのランナーに--skip-executorオプションが追加された

bin/rails runner --skip-executorオプションを指定することで、ランナースクリプトをExecutorでラップしないようにできるようになった。

Ben Sheldon
同Changelogより

動機/背景

#44999でRailsのランナースクリプトがRailsのExecutorでラップされるようになった。

これは素晴らしいことだが、実行に時間のかかるスクリプトやループするスクリプトやデーモン的なスクリプトではそうではない。そのような場合は、スクリプト内で個別の作業単位をExecutorで意図的にラップできるようにする方がよさそう。

詳細

このプルリクは、Railsスクリプトランナーに--skip-executorを渡せるようにすることで、Executorのラップを条件付きでスクリプトに追加「しない」ようにする。

別のオプション: Executorを削除する代わりに、#43550でExecutorに導入されたリエントラント可能な#performでランナーをラップすべきかもしれない。

同PRより


つっつきボイス:「#44999はRails 7.1でマージされてる」「ランナースクリプトを自動的にExecutorでラップしたのは、エラーレポートやクエリキャッシュが使えるようにするためだったのね: たしかに場合によってはExecutorでラップしたくないときもありそう」

参考: Rails API ActionDispatch::Executor
参考: §2.5 bin/rails runner -- コマンドラインツール - Railsガイド

🔗 SQLite3アダプタに生成カラム(generated columns)のサポートを追加

生成カラムは(storedおよびdynamicの両方について)SQLite 3.31.0以降でサポートされている。
このプルリクは、SQLite3アダプタにそうしたサポートを追加する。

create_table :users do |t|
  t.string :name
  t.virtual :name_upper, type: :string, as: 'UPPER(name)'
  t.virtual :name_lower, type: :string, as: 'LOWER(name)', stored: true
end

Stephen Margheim
同Changelogより


つっつきボイス:「SQLiteでも2020年から生成カラムが使えるようになってたとは知らなかった」「SQLiteのサポートがまた強くなった」

参考: 第150回 Generated Columnを利用してみる | gihyo.jp
参考: Generated Columns -- SQLite Documents

🔗 ActionController::Live#send_streamにinstrumentationが追加された

ActionController::Live#send_stream用のinstrumentationを追加。

send_streamイベントへのサブスクライブが可能になる。このイベントのペイロードにはfilenamedispositiontypeが含まれる。

Hannah Ramadan
同Changelogより


つっつきボイス:「これはシンプルなinstrumentation APIの追加ですね」

参考: Active Support Instrumentation で計測 - Railsガイド

🔗 MySQLでArelのnulls_firstnulls_lastが使えるようになった

MySQL向けにnulls_lastを追加し、desc.nulls_firstを修正した。

Tristan Fellows
同Changelogより

動機/背景

修正: #50078

このプルリクを作成した理由は、MySQLデータベースで、あるASCカラムを「null last」でソートする必要が生じたが、そのためにSQLフラグメントの利用を避けたかったため。ArelはMySQLを除いてこの機能をサポートしていることを発見した。MySQLの実装を調べてみたところ、実装が一貫していないことに気づいた。

元々は動作を一貫させるだけのつもりだったが、足りない機能も実装してみた。

詳細

このプルリクはMySQLデータベースにおける.nulls_first().nulls_last()の実装を変更する。これにより、これらのメソッドが設計通りに動作するようになる。
同PRより


つっつきボイス:「Arelのdesc.nulls_firstがMySQLだと期待通り動いていなかったそうです」「悲しい」「ソートしたときにNULLを冒頭に配置するか末尾に配置するかを指定するメソッドなんですね」「お〜、.asc.nulls_last.asc.nulls_first.reverseみたいな書き方ができるのか、よさそう😋」「これ今まで生SQLで書いてた」

# activerecord/test/cases/arel/visitors/mysql_test.rb#L156
      describe "Nodes::Ordering" do
-       it "should no-op ascending nulls first" do
        it "should handle nulls first" do
          test = Table.new(:users)[:first_name].asc.nulls_first
          _(compile(test)).must_be_like %{
-           "users"."first_name" ASC
+           "users"."first_name" IS NOT NULL, "users"."first_name" ASC
+         }
+       end
+
+       it "should handle nulls last" do
+         test = Table.new(:users)[:first_name].asc.nulls_last
+         _(compile(test)).must_be_like %{
+           "users"."first_name" IS NULL, "users"."first_name" ASC
+         }
+       end
+
+       it "should handle nulls first reversed" do
+         test = Table.new(:users)[:first_name].asc.nulls_first.reverse
+         _(compile(test)).must_be_like %{
+           "users"."first_name" IS NULL, "users"."first_name" DESC
+         }
+       end
+
+       it "should handle nulls last reversed" do
+         test = Table.new(:users)[:first_name].asc.nulls_last.reverse
+         _(compile(test)).must_be_like %{
+           "users"."first_name" IS NOT NULL, "users"."first_name" DESC
          }
        end
      end

Arelのススメ -- Arelを使ってみよう

🔗Rails

🔗 2023年のRails振り返りとRails 8情報(Rails公式ニュースより)


つっつきボイス:「Rails公式が振り返る2023年と、Rails Foundation創立に関する記事です」「こうして見るとRails 7.1でいろんなものが追加されましたね」

以下は週刊Railsウォッチでの対応する見出しです。


「続いてRails 8のマイルストーンもできました↓」「お、ここをチェックしておけばRails 8の様子がある程度わかりますね」「後で読もう」

参考: 8.0.0 Milestone

なお、以下の記事でもRails 8を先行紹介しています。

参考: Sneak Peek on Rails 8 | lucas.dohmen.ioRuby Weeklyより)


参考: A writer's RubyRuby Weeklyより)

「上のDHHの記事によると、Rails 8でRubocopがデフォルトで入ってくるそうです」「このrubocop-rails-omakaseがそれか↓」「ついに入ってくるんですね」「なかなか大きな変更」「現代のRailsプロジェクトでRubocopを使わないことはまずないでしょうね」「このrubocop-rails-omakaseの設定が議論を呼びそうな気がする」

rails/rubocop-rails-omakase - GitHub

「あとBrakemanもRails 8でデフォルトになるそうです」「Brakemanは大好き❤️」

presidentbeef/brakeman - GitHub

🔗 Solid Queue: 37signalsによるActive Job向けDBベースのジョブバックエンド(Ruby Weeklyより)

basecamp/solid_queue - GitHub


つっつきボイス:「37signalsが作ったSolid Queue、今のRails 8マイルストーンにもありますね」「Rails 8ではこれがデフォルトのジョブキューバックエンドになって(#50442)、PostgreSQLとMySQLとSQLite3を使えるらしい: ジョブキューのためにわざわざRedisサーバーを立てなくても済む方向を目指しているんでしょうね」「DBでのジョブキューで有用なSKIP LOCKEDがMySQL 8.0.1でも入っていたのね↓」

--同記事より
SELECT ... FOR UPDATE SKIP LOCKED

参考: MySQL :: MySQL 8.0.1: Using SKIP LOCKED and NOWAIT to handle hot rows

🔗 superglue: thoughtbotによるRails向けReact Reduxライブラリ(Ruby Weeklyより)

thoughtbot/superglue - GitHub

// 同記事より
import React from 'react'
import { useSelector } from 'react-redux'
import { Drawer, Header, Footer, ProductList, ProductFilter } from './components'

export default function FooBar({
  header,
  products = [],
  productFilter,
  rightDrawer,
  footer
}) {
  const flash = useSelector((state) => state.flash)

  return (
    <>
      <p id="notice">{flash && flash.notice}</p>
      <Header {...header}>
        <Drawer {...rightDrawer} />
      </Header>

      <ProductList {...products}>
        <ProductFilter {...productFilter} />
      </ProductList>

      <Footer {...footer} />
    </>
  )
}

つっつきボイス:「thoughtbotがRailsでReactを使うためのsuperglueというのを作ったそうです」「ドキュメントを眺めてみると、クライアントサイドルーティングはしないらしい↓」

参考: Document -- Superglue

「基本的にRailsのビューを使う形になっていて.jsを置くとReactページコンポーネントになり、.jsonを置くとデータだけを表示するのね↓」「PropsTemplateではjbuilderっぽい書式でDSLを書けるのか」


同ドキュメントより

「どうなんだろう?管理画面なんかを作るにはいいのかもしれないけど」「近年フロントエンドを切り離してバックエンドと別に開発するようになってきたのは、フロントエンドのモデルとバックエンドのモデルが一致しないからという面もあるんですが、superglueのディレクトリ構成などを見ると両者が同じモデルを使うという前提になっているので、両者が密結合するわけですよね」「それもそうか」「その意味ではReactをフロントエンドフレームワークではなく、単なるビューとして使うことになるんじゃないかな: 少なくともフロントエンド指向の開発ではないと思います」

「superglueを入れてまでReactを使いたいかどうかですよね」「あるとすれば、Rails開発者がHotwireのStimulusとかを新たに勉強するよりは、既に知っているReactを使いたい場合とかかな🤔」


前編は以上です。

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

週刊Railsウォッチ: Ruby 3.4から暗黙の"it"ブロック変数が導入されるほか(20231222)

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

Rails公式ニュース

Ruby Weekly


CONTACT

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