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

週刊Railsウォッチ: Herokuが無料プラン廃止を発表、Hotwire日本語コミュニティほか(20220905前編)

こんにちは、hachi8833です。いよいよRubyKaigiが今週始まりますね。

週刊Railsウォッチについて

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

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

お知らせ: 来週の週刊Railsウォッチはお休みをいただきます🙇

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

以下の公式更新情報を取り上げました。

また次のが出ていました。なかなか追いつけない…

🔗 ActiveSupport::Cacheの書き込みでexpires_atに過去の日付を渡すとwarningをログ出力するようになった

概要
この変更によって、Rails.cache#fetchwriteを実行するときに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_atexpires_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_ofnilを渡すと、以下の無効な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_ofnilを渡すと動作が不定になりそうなので、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

参考: SQL における NULL との比較


「ちなみに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_ofActiveRecord::QueryMethods
参考: Rails API in_order_ofEnumerable

Rails 7: クエリ結果を任意の順序にできるActiveRecord::QueryMethods#in_order_of

🔗 マルチパートリクエストでRackのEOFErrorrescueするよう修正

以下の箇所でrequest_parametersメソッドがEOFError例外によって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より

「早速うなすけさんが移行先の比較記事を出してくれています🙏↓」

「私もこの記事に載っている選択肢の中から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アプリを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"])

Rails: Webpacker(Shakapacker)とjsbundling-railsの比較(翻訳)

🔗 active_interaction gemでRailsアプリのビジネスロジックを管理する

AaronLasseigne/active_interaction - GitHub


つっつきボイス:「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!"

「こういうふうにarrayhashなどのフィルタが使えるのも便利そう↓」「ベタ書きの手作り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を推している人なんですね」

Rails: Service Objectはもっと使われてもいい(翻訳)

「Service Objectは何でもかんでも無節操に放り込まないようにするのが肝心なので、節度を保ってCommandパターン的に使う分には大丈夫だと思います」「ですよね」

参考: Command パターン – Wikipedia

「Service Objectは個人的にあんまり好きじゃないかも」「でも複数の処理を横断するようなビジネスロジックをconcernsなどに置くとモデルがいくらでも肥大化してしまうので、結局何らかの形でFacade的なものを書く必要は生じると思いますけどね」「そうなんですよ、2つのモデルを行ったり来たりするような処理を片方のモデルに置くわけにはいかないので、どこかでService Object的な何かが必要になる」

参考: Facade パターン – Wikipedia

「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コミュニティ


つっつきボイス:「Hotwire.loveというドメイン名❤️」「管理者にjnchitoさんもいますね」「YassLabの安川さんもいます」「さっそく参加しました」


前編は以上です。

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

週刊Railsウォッチ: RubyKaigi 2022タイムテーブル公開、viewport-extraほか(20220830後編)

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

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

Rails公式ニュース


CONTACT

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