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

Rails: Evil Martiansが使って選び抜いた夢のgem -- 2024年度版(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。また、gemごとにGitHubリポジトリへのリンクカードも追加してあります。

参考: 週刊Railsウォッチ20230131: Evil Martiansが使っているgem

なおEvil Martiansの記事では、自社メンバーをしばしば火星人(Martians)と表現しています。


Rails: Evil Martiansが使って選び抜いた夢のgem -- 2024年度版(翻訳)

はじめに

太古の昔から、Evil Martiansは毎年多くのRuby on Railsプロジェクトを手がけてきました。当然ながら、その過程で多くのRuby gemが関わってきます。ピカピカの最新機能を使いたい(さもなければ自分たちで作りたい)という私たちの願望を反映したgemもあれば、弊社のほとんどのプロジェクトで使われているほど柔軟性の高いgemもあります。私たちが使っている火星人印のgemには、私たちの開発哲学、プログラミング習慣、そして魂が込められているのです。これらのgemを何らかの形で1つのGemfileに集約したらどうなるでしょう。火星人たちが理想とするGemfileとは一体どんなものか、外宇宙からやってきたRailsエンジニアたちの道具箱には果たして何が詰まっているのでしょうか?

原注: 本記事原文は、Railsフレームワークの変更を反映するため恒常的に更新されます。詳しくは記事末尾のChangelogをご覧ください。


Railsのエコシステムは実に広大です。あらゆる典型的な作業(のほとんど)を支援する多くのライブラリが出動を心待ちにしています。その中からどうやって正しいライブラリを選べばよいでしょうか?

GitHubスターやダウンロード数といった指標に頼る方法もあります(これについてはThe Ruby Toolboxサイトで貴重な洞察を得られます)。あるいは過去の経験に頼る手もあるでしょう。経験則はライブラリを選ぶうえで有効な手段ですが、新しいgemを極端に敬遠する保守主義におちいる可能性もあります。

追加
gemfile.directoryサイトには、実際に使われているさまざまなGemfileが誰でも参照できる形で掲載されていますので、ここからヒントを得ることも可能です。
サンプルとして、AnyCable+で使われている最新Gemfileをご覧ください。皆さんも自分のGemfileをここで公開してみませんか?

我がEvil Martiansでは、gemを選ぶときやgemを新たに構築するかどうかを決定するときは、集団がつちかってきた経験と異星人の本能の両方に賭けています。集合知としての経験のメリットは、さまざまな人々のマインドが長年に渡って蓄積されていることです。私たちは、どうすれば勝利を繰り返せるか、どうすれば失敗を回避できるかというおびただしい知見を積み上げています。この知見をスーパー賢いチャットAIに仕立て上げることは(まだ)できていませんが、本記事でその一端を紹介したいと思います。

なお、ここで言う「異星人の本能」は、要するに新しく登場したライブラリの品質を評価する枠組みのことです。未経験のライブラリであれば、静的コードの特徴(APIやアーキテクチャ)に頼るしかなく、人気の指標はあまりあてにできません。Evil MartiansのGem Checkプロジェクトは、そうした枠組みに特化した実装です。

ここからは、RailsアプリケーションのGemfile構造に似せた順序で進めます。以下のセクションがあります。

🔗 Railsの基本部分

最初に、Rails Webアプリケーションの根幹となる要素、すなわちWebサーバーとデータベース(RDBMS)を設定する必要があります。


自分たちのチームでもっと先進的なことをやりたい方は、私たちがこれまで手がけてきたRailsスタートアップスタックをご覧ください。このスタートアップスタックは本記事から大いにヒントを得ています。

私たちはデータベースにPostgreSQLを選び、WebサーバーにはPumaを選ぶようにしています。
つまり、Gemfileの冒頭部分は以下のような感じになります。

gem 'rails', '~> 7.2'
gem 'pg'
gem 'puma'

rails/rails - GitHub
ged/ruby-pg - GitHub
puma/puma - GitHub

基本部分に付け加える話はそれほどないので、Ruby on Railsアプリケーションの第3の柱であるバックグラウンドジョブエンジンを見ていきましょう。

🔗 バックグラウンドジョブ

Webアプリケーションのパフォーマンスにおける重要な特性は、スループット(throughput: 一定時間に処理できるリクエスト数)です。Ruby(MRI)のコンカレンシー制限、より正確にはGVL(Global Virtual Machine Lock)1によって、パラレルに配信可能なリクエスト数に上限が加わります。

私たちはスループットを改善するために、リクエストを最短時間で処理できるよう努力を重ねています。それには、処理をできるだけバックグラウンドに逃がすのがベストです。ここで登場するのが、バックグラウンドジョブを処理するエンジンです。

Railsは、バックグラウンドジョブを定義する抽象化機能をActive Jobとして提供しています。しかし、実際にバックグラウンドジョブを処理するジョブエンジンはユーザーの選択に任されています。私たちはSidekiqを使っています。

gem 'sidekiq'

mperham/sidekiq - GitHub


Sidekiqには、バックグラウンドジョブの実行ロジックをきめ細かく制御できるアドオンが多数あります(オフィシャルなPro版やEnterprise版も含む)。その中でも、火星人たちのプロジェクトで最も多用されている2つのプラグインを2つ紹介します。

gem 'sidekiq-grouping'
gem 'sidekiq-limit_fetch'

gzigzigzeo/sidekiq-grouping - GitHub

sidekiq-grouping gemは、キューに入れられる(enqueued)ジョブをバッファリングして、まとめてバッチ処理できます。ジョブのenqueue率が高いが即座に処理しなくてもいい場合に有用です。典型的なユースケースは、モデルの再インデックスや、モデルへのライブアップデートをブロードキャストするなどです。

deanpcmad/sidekiq-limit_fetch - GitHub

sidekiq-limit_fetch gemは、重たいジョブ(あるいはメモリ使用量を肥大化させるジョブ)を制御するために使っています。たとえば、巨大な単一XSLXファイルは同時に1個しかエクスポートできないようにするには、以下のコンフィグでキューに制限をかけます。

# sidekiq.yml
---
:queues:
  - default
  # ...
  - exports
:process_limits:
  exports: 1

追加 有料のSidekiq ProやSidekiq Enterpriseを選ぶことでサードパーティのソリューションが提供する全機能や強化された安定性を今すぐ手に入れるという選択肢も、もちろんありです。そうすればgemリストは間違いなく大幅に短くなります(その分財布も涼しくなりますが、それだけの価値があります)。

ただしSidekiqには、有料無料を問わずいくつかのトレードオフが生じます。

1つ目は、Redisが必要になる点です。つまり、インフラストラクチャでメンテナンス(もしくは課金)すべきコンポーネントが1つ増えることになります。

2つ目は、Redisをジョブの保存に利用すると、トランザクション整合性エラーが発生しやすくなる点です(この問題と解決方法については後述しますので、このままお読みください)。そういうわけで、トランザクションに対応し、依存が生じないタイプのバックグラウンドジョブエンジンを求める動きが近年盛んになっています。現在は、以下の2つのgemのどちらかが使われていることがよくあります。

gem 'good_job'

# または
gem 'solid_queue'

bensheldon/good_job - GitHub

追加 GoodJobはバックエンドにPostgreSQLを採用しているので、PostgreSQLを既に導入しているアプリケーション(つまり業務で使うほとんどのアプリケーション)と統合しやすい点が特徴です。GoodJobは一般的なRailsアプリケーション向けの機能を十分備えており、現場で長年に渡る実績を積んでいるので、真剣に検討する価値があります。

rails/solid_queue - GitHub

追加 GoodJobの精神的な継承者であるSolidQueueは、データベースの種類を問わない(ただしSQLには依存する)ソリューションであり、Rails 8以後はRailsが提供するデフォルトのバックグラウンドジョブとなることを意図していますが、私たちとしてはSolidQueueが十分成熟するまで待ちたいと思います。

GoodJobやSolidQueueを利用するもうひとつのメリットは、(cronライクな)定期ジョブ実行が組み込みでサポートされていることです。Enterprise版以外のSidekiqで定期ジョブ実行機能を利用するには、それ用のサードパーティ製ツールを導入する必要があります。
よく使われているサードパーティ製ツールをいくつか紹介します。

gem 'schked'

bibendi/schked - GitHub

jmettraux/rufus-scheduler - GitHub

よく実行するジョブについては、Schkedという軽量なソリューションを使っています。これはrufus-schedulerという有名なスケジューラをラップして、テストのサポートやRailsエンジン対応などの機能を追加したものです。

最後に、Railsの枠を少々超えるgemを紹介して本セクションを締めくくることにします。

gem 'faktory_worker_ruby'

contribsys/faktory_worker_ruby - GitHub

私たちの多言語プロジェクトでは、さまざまなサービスから受け取ったジョブをエンキューして処理するFaktoryを用いています。たとえば、Pythonで書かれたMLサービスがあるとすると、そこから分析結果をRailsアプリケーションに送信することも、逆にRailsアプリからPythonアプリに送信することも可能です。

🔗 Active Record拡張

本セクションの見出しから想像がつくかと思いますが、私たちはRails Wayから"脱線"するようなことはしません。Active Recordは、ビジネスロジックのモデリングとデータベースとのやりとりに用いています。

Active RecordはRailsのサブフレームワークの中でも最も多機能で、RailsがリリースされるたびにActive RecordのAPIが増えています(Rails 7.1には待ちに待ったCTE(Common Table Expressions)サポートも追加されました!)。
それでもActive Recordに追加したい機能はまだあるので、私たちは多くのプラグインを追加していきます。

私たちはPostgreSQLに特化しているので、PostgreSQL固有の機能を多用しています。個人的にはJSONBが好みです。JSONBは、合理的に使えば生産性が爆伸びします(スキーマやマイグレーションを気にしなくてよくなるなど)。デフォルトのRailsは、JSONBの値を普通のRubyハッシュに変換しますが、このままではビジネスロジックをモデリングするうえでやや力不足です。そういうわけで、以下のライブラリのどちらか1つを用いて、構造化されていないフィールドを強化しています。

gem 'store_attribute'
gem 'store_model'

palkan/store_attribute - GitHub
DmitryTsepelev/store_model - GitHub

store_attribute gemは、Active Record組み込みのstore_accessor機能を拡張して型キャストをサポートします。これにより、JSONの値を通常の属性として扱えるようになります。

参考: Wrapping JSON-based ActiveRecord attributes with classes—Martian Chronicles, Evil Martians’ team blog

Store Model gemはそこからさらに一歩踏み込んで、JSON属性を備えたモデルクラスを定義可能にします。


その他によく使っているデータベース機能(PostgreSQL固有でないSQL固有の機能)トップNといえば、データベースVIEW、トリガー、全文検索などです。それぞれの機能に対応したgemは以下のとおりです。

gem 'pg_search'
gem 'postgresql_cursor'

gem 'fx'
gem 'scenic'
# または
gem 'pg_trunk'

Casecommons/pg_search - GitHub
afair/postgresql_cursor - GitHub
teoljungberg/fx - GitHub
scenic-views/scenic - GitHub
nepalez/pg_trunk - GitHub

PostgreSQLのenumやそのメリットについて知りたい方は、Test DoubleのJustin Searlsによるブログ記事『Enumerate your enums』をどうぞ。

特徴的な拡張についても、ほんのいくつか触れておきたいと思います。

# Soft-deletion
gem 'discard'
# 期間でグループ化するヘルパー
gem 'groupdate'

jhawthorn/discard - GitHub

Discardは、最小限のソフト削除(soft-deletion)機能を提供します(default_scopeをアタッチしません)。機能が最小限に絞られているので、用途に応じてカスタマイズしやすく、ソフト削除を用いるほとんどのアプリケーションで使われています。

参考: Soft deletion with PostgreSQL: but with logic on the database!—Martian Chronicles, Evil Martians’ team blog

ankane/groupdate - GitHub

Groupdate gemは、時系列集計レポートを扱う(かつ、TimescaleDBなどの時系列データベースにまだ移行していない)プロジェクトになくてはならないものです。

🔗 認証と認可

私がこの5年間に付き合ってきたアプリケーションのほとんどには、以下の1行がありました。

gem 'devise'

heartcombo/devise - GitHub

きっと本記事読者も、10人のうち9人はDeviseをバンドルしているに違いありません。それが現実です(が、それでも私は夢を見たいのです)。

機会は少ないのですが、Railsアプリケーションをゼロから構築する場合は、最初にDevise以外の認証ライブラリを検討してみると決めています。さて、その筆頭は...(ドラムロール)、何とRails自身なのです!has_secure_password2機能を使えば、かなりの程度までやれます。

追加 実は、Rails 8ではこのあたりをさらに使いやすくする柔軟なジェネレータを搭載する計画があるのです(#50446)!(訳注: その後#52328でauthenticationジェネレータがマージされました)

諦めてDeviseに立ち返る前に、他にもいくつか候補があります。

gem 'sorcery'
gem 'jwt_sessions'

Sorcery/sorcery - GitHub

Sorceryは歴史の長いgemです。Deviseよりもマジックが控えめで(READMEには逆のことが書かれていますが)、Deviseほど癖が強くなく、プラガブルアーキテクチャを備えています(つまり必要な部分だけを手軽に選べます)。

tuwukee/jwt_sessions - GitHub

JWT Sessionsライブラリは、トークンベース認証の構築に必要なものをすべて提供しています。モバイルやSPAフロントエンドを持つAPI専用のRailsアプリの構築に最適です。


さて、OAuthドリブンの認証ソリューションについてはまるっと割愛して(これだけでたっぷり一本の記事が書けます)、認可機能の話に移ることにしましょう。

「あれ、認証も認可も同じようなものなんじゃないの?」いえいえ、認証(authentication)と認可(authorization)は全然違います。両者の根本的な違いは、どんな問いに回答するかの違いに基づいています。
認証は「この人は誰ですか?」という問いに回答する機能ですが、認可は「私がこれを行うことは許されますか?」という問いに回答する機能です。すなわち、認可ではロール(role: 役割)と権限(permission)を扱うことになります。

認可機能は、「認可モデル」と「認可(適用)レイヤ」という2つのコンポーネントに分けられます。前者の認可モデルは、パーミッションの背後に置かれるビジネスロジック(ロールや細かなパーミッションなどを使うかどうかなど)を表します。後者の認可レイヤは、認可ルールを実際に適用する方法を定義します。

参考: Access denied: the missing guide to authorization in Rails

認可モデルはアプリケーションに強く特化しすぎていて一般化が難しいのですが、認可レイヤの実装についてはgemに同梱されている一般的な手法が使えます。
私たちは長年Punditを認可の強制適用に用いてきましたが、今ならもっとよい選択肢があります。

gem 'action_policy'

palkan/action_policy - GitHub

Action PolicyはPunditの強化版です。このgemのコンセプト(ポリシー)はPunditと同じですが、パフォーマンスや開発エクスペリエンスを重視する優れた機能を提供します。

varvet/pundit - GitHub

🔗 HTMLビュー

古典的なRails Wayでは、「HTMLファースト」が想定されていました。サーバーはMVC(Model-View-Controller)パラダイムの3つの要素である"M"、"V"、"C"について責務を負います。API専用Railsアプリという暗黒時代を経て後、最近の私たちはHTML-over-the-wireというルネッサンスの時代を今まさに目撃しています。こうして、今の私たちは再びビューテンプレートを書くようになっているのです!

しかし2020年代ともなれば、ビューテンプレートの書き方も変わります。

gem 'view_component'
gem 'view_component-contrib'
gem 'lookbook', require: false

gem 'turbo-rails'

viewcomponent/view_component - GitHub
palkan/view_component-contrib - GitHub
allmarkedup/lookbook - GitHub
hotwired/turbo-rails - GitHub

HotwireはHTMLベースのアプリケーションをインタラクティブかつリアクティブにし、ViewComponentはテンプレートとそのロジックを整理するのに役立ちます。

実践ViewComponent(1): 現代的なRailsフロントエンド構築の心得(翻訳)

実践ViewComponent(2): コンポーネントを徹底的に強化する(翻訳)

🔗 アセット管理

Rails 5でWebpackerが導入されて以来、Railsでアセットを扱うのが面倒でした。今ではRails 7でこの問題に対する公式のソリューション(importmap-rails、jsbundling-rails、cssbundling-rails)が使えるようになりました。しかし私たち火星人は別の方法にしました。

# 訳注: gemのリポジトリ名は`vite_ruby`です
gem 'vite_rails'

ElMassimo/vite_ruby - GitHub

Viteを利用するアセット管理は、バックエンド指向とフロントエンド指向の中間に位置します。HotwireアプリケーションとReact SPAのどちらについてもシンプルに利用可能です(さらに私たちのセットアップは両者のハイブリッドになることもしばしばです)。vite_ruby gemは、それらに加えて五つ星級の開発エクスペリエンスを提供してくれます。

参考: Vite-lizing Rails: get live reload and hot replacement with Vite Ruby—Martian Chronicles, Evil Martians’ team blog


私たちのGemileには、他にもアセット関連gemがいくつか追加されています。

gem 'imgproxy-rails'
gem 'premailer-rails'

imgproxy/imgproxy-rails - GitHub

ユーザーが生成した大量のコンテンツを扱う必要が生じたときに、Rubyサーバーのリソースを画像変換のために浪費したくないですよね?そんなときは専用のプロキシサーバーに重い処理を任せましょう。もちろん、私たちは独自のimgproxyをそれ用の連携gemとともに使っているので、Railsアプリで使うのは朝飯前です。

fphilipe/premailer-rails - GitHub

メールに書式を設定するなら、premailer-rails gemが逸品中の逸品です。自分たちのCSSクラスを自由に用いてメールに書式を設定すれば、メールテンプレートのレンダリング中にPremailerがそれらを解きほぐしてスタイル属性にしてくれます。

🔗 API(とGraphQL)の作成

HTML-over-the-wireによる手法が人気を盛り返しつつあるとはいえ、長年に渡るJS支配はそう簡単にはゆるがないでしょう。そういうわけで、APIファーストのRailsアプリケーションも当分安泰だろうと思われます。

私たちがJSON APIを作成するときは、たいてい以下のツールを利用します。

gem 'alba'
gem 'oj'
# または
gem 'panko_serializer'

okuramasafumi/alba - GitHub
ohler55/oj - GitHub
panko-serializer/panko_serializer - GitHub

AlbaPankoは、どちらもデータシリアライズのパフォーマンスに注力しており、Active Model Serializerに似た使いやすいインターフェイスを提供しています。PankoはC拡張を使っていることもあり、active_model_serializersに近いAPIを提供している現在もメンテ中のライブラリの中では最速のベンチマーク結果を叩き出しているので、ライブラリの乗り換え先候補としてもよいでしょう。一方、Albaはもっと機能が豊富で、しかもかなり高速です(JSONシリアライズドライバにOjを使う場合)。


skryukov/skooma - GitHub

追加REST APIのドキュメントとフロントエンド側を常に同期する作業も、Rails開発者につきものの課題です。これについて私たちは、「ドキュメントファースト」の手法が非常にうまくいくことを見出しました。OpenAPIスキーマを手動またはAI支援で(今は2024年ですよね🤖)作成することで、実装が常にスキーマと一致するようにしています。

gem "skooma", group: :test

参考: Let there be docs! A documentation-first approach to Rails API development—Martian Chronicles, Evil Martians’ team blog


今もRailsコミュニティでかなり人気の高いGraphQLを利用しているのであれば、以下の他に何も追加しなくてもドキュメントと型情報を常に同期できます。

gem 'graphql', '~> 2.3'

# コネクションでカーソルベースのページネーションを行う
gem 'graphql-connections'
# GraphQLのキャッシュを適切にする
gem 'graphql-fragment_cache'
# Apolloの永続化クエリをサポート
gem 'graphql-persisted_queries'

# はい、Action PolicyはGraphQLで公式に統合されています
gem 'action_policy-graphql'

gem 'graphql-schema_comparator', group: :development

# N+1クエリ問題を(ずぼらな)プロのように解決!
gem 'ar_lazy_preload'

rmosolgo/graphql-ruby - GitHub
bibendi/graphql-connections - GitHub
DmitryTsepelev/graphql-ruby-fragment_cache - GitHub
palkan/action_policy-graphql - GitHub
xuorig/graphql-schema_comparator - GitHub
DmitryTsepelev/ar_lazy_preload - GitHub

私たちが使っているGraphQLライブラリはパフォーマンス指向です(そして私たち火星人が作っています...これは偶然?)。graphql-schema_comparator gemをDangerというツールと併用することで、スキーマを変更するプルリクが来たら警告するようになっています(おかげでスキーマ変更を見落とさずに済みます)。

GraphQLの永続化クエリやフラグメントキャッシュについて詳しくは、以下の別記事をどうぞ。

ところで、GraphQLグループにar_lazy_preloadというgemが追加されている理由が気になる人もいるかもしれませんね。このライブラリはActive Recordの拡張ではありますが、特にGraphQL APIと組み合わせると大変便利なのです。N+1クエリ問題を退治するのに使われる古典的な#preload#eager_loadはあまり効率がよくありません。遅延プリロード(lazy preload)を用いることで、データローダーやバッチローダーによるパフォーマンス低下や複雑性のオーバーヘッドを回避できるようになります(もちろん、完璧ではありませんが)。

🔗 ログ出力と計測

私たちの偉大なるGemfileさまのproductionグループには、以下のログ出力/監視ツールが含まれています。

group :production do
  gem 'yabeda-sidekiq', require: false
  gem 'yabeda-puma-plugin', require: false

  gem 'lograge'
end

yabeda-rb/yabeda - GitHub
roidrage/lograge - GitHub

YabedaはRubyアプリやRailsアプリで使えるinstrumentation(計測)フレームワークです。著名なライブラリ(SidekiqやPumaなど)や監視バックエンド(PrometheusやDataDogなど)を対象とするさまざまなプラグインが利用できます。

参考: Yabeda: monitoring monogatari by Evil Martians

Logrageは、Railsの冗長なログ出力を簡潔な形式で構造化することで、ログ収集システムで解析しやすくしたり、クエリ可能なデータソースに変換したりできます。

追加 私たちのデフォルトグループには、こんな(アンチ)ログgemもあります。

gem 'silencer', require: ['silencer/rails/logger']

stve/silencer - GitHub

Railsにデフォルトのヘルスチェックエンドポイントが追加されて以来、ログに無駄なGET /up行があふれかえるようになってしまいました。silencer gemをインストールして以下の設定を行えばこれを黙らせることが可能です。

# config/initializers/silencer.rb
Rails.application.configure do
  config.middleware.swap(
    Rails::Rack::Logger,
    Silencer::Logger,
    config.log_tags,
    silence: ["/up"]
  )
end

🔗 開発ツール

本番環境の話はこのぐらいにして、開発環境の話に移りましょう(私たちの仕事は、実はエンジニアなのですから)。Rubyは開発者が幸せになれるように設計されていますが、この原則に沿ってRubyでアプリケーションを開発するには、ツールを少しばかり調整する必要があります。

どうすればRails開発者が心の底から幸せを感じられるようになるでしょうか?はい、開発環境でトラブルが一切発生しなくなることですよね 🙂 この問題は、Dockerで十分解決できます。

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(更新翻訳)

他にはどんなことができるでしょうか?退屈な定形作業を減らすことですよね。そこで私たちは、そういう作業を支援するロボットを追加してクールな作業に専念できるようにしたり、開発エクスペリエンスをもっと快適にして良いコードを書けるようにするgemを追加しています。

以下は火星人たちがRailsプロジェクトで利用している有名な開発ツールです。

gem 'bootsnap', require: false

bootsnapはRailsにデフォルトでバンドルされるので、自分で導入する必要はありません。そのまま使いましょう!


gem 'database_validations'
gem 'database_consistency'

toptal/database_validations - GitHub
djezzzl/database_consistency - GitHub

database_validations gemとdatabase_consistency gemのペアは、モデル層とデータベーススキーマの間で一貫性を強制するのに役立ちます。null: falseを指定すると、validates :foo, presence: trueと書かなければならなくなります。validates :bar, uniqueness: trueを指定すると、そのカラムにuniqueインデックスを定義する必要があります。


gem 'isolator'
gem 'after_commit_everywhere'

palkan/isolator - GitHub
Envek/after_commit_everywhere - GitHub

多くのデータベース操作はトランザクション上で行われるので、ACID原則はどんなときも忘れてはいけません。データベーストランザクションは論理トランザクションと同じではなく、データベース以外の場所で発生する副作用についてはデータベースで保護できないので、自分たちで対処する必要があります。

たとえば、"記事を公開しました"というメールをデータベーストランザクション内から送信すると、トランザクションのコミットが万一失敗した場合に誤ってメール通知が送信されてしまう可能性があります。

同様に、あるAPIリクエストを実行したときにトランザクションがロールバックされると、サードパーティのシステム(支払いゲートウェイなど)との不整合が生じる可能性があります。

Isolator gemはそうした問題の特定に役立ち、after_commit_everywhere gemはCOMMITが成功した場合に副作用を実行する便利なAPIを提供します。

追加 Rails 7.2には、このafter_commit_everywhere gemと似た機能としてActiveRecord::Transaction#after_commit#51474)や、コミット時にActive Jobのジョブを自動的にエンキューする機能(#51426)が導入されるので、もしかするとafter_commit_everywhereは今後不要になるかもしれません。


その他のデータベース関連gemについても寸評を加えておきます。

# マイグレーションによるダウンタイムを防ぐ
gem 'strong_migrations'
# データベースのヘルスに役立つ洞察を得られる
gem 'rails-pg-extras'

# productionデータベースの一部をマスクしてダンプし、
# ローカルでのプロファイリングやベンチマークを行えるようにする
gem 'evil-seed'

ankane/strong_migrations - GitHub
pawurb/rails-pg-extras - GitHub
evilmartians/evil-seed - GitHub


ベンチマーク実行やプロファイリングも開発の一部です。ここではそういう場面で有用なgemをいくつか紹介します。

# プロファイリングで必須のツールをいくつか
gem 'derailed_benchmarks'
gem 'rack-mini-profiler'
gem 'stackprof'
gem 'vernier'

zombocom/derailed_benchmarks - GitHub
MiniProfiler/rack-mini-profiler - GitHub
tmm1/stackprof - GitHub
jhawthorn/vernier - GitHub

Stackprofといえば、Speedscopeという素晴らしいスタックプロファイルツールもぜひどうぞ。

追加 Ruby 3.2以降で動作するプロジェクトには、vernierというRubyの次世代サンプリングプロファイラの利用をおすすめします。vernierは、Railsのinstrumentationなどのサポートも組み込まれています。


gem 'bundler-audit', require: false
gem 'brakeman', require: false

rubysec/bundler-audit - GitHub
presidentbeef/brakeman - GitHub

セキュリティは避けて通るわけにはいきません。bundle auditを実行して既知のCVEがプロジェクトの依存関係に含まれていないかどうかをチェックしましょう。
また、Brakemanを定期的に実行してコードベースのセキュリティ問題をスキャンしましょう3。今なら、CIサービスが独自に提供しているセキュリティ分析ツールを代わりに使う手もあります。


gem 'danger', require: false

danger/danger - GitHub

Dangerは、コードレビューのエクスペリエンスをレベルアップします。たとえば以下のように、複雑な定形作業を自動化したり変更をハイライトしたりします。やりたいことはほぼ何でも自動化スクリプトにお任せできます。

  • ラベルの自動追加
  • テストがない場合に警告する
  • 望ましくないstructure.sqlを変更する
  • etc.

参考: Danger on Rails: make robots do some code review for you!—Martian Chronicles, Evil Martians’ team blog


gem 'next_rails'

fastruby/next_rails - GitHub

Railsをアップグレードするなら、next_rails gemにアップグレード作業の道案内をやってもらいましょう。


gem 'attractor'
gem 'coverband'

julianrubisch/attractor - GitHub
danmayer/coverband - GitHub

コードベースを健全な状態に保つには、定期検査を実施して、コードの中で最も痛んでいる部分を監視する必要があります。

たとえば静的解析では、「チャーンvs複雑性」4のような有名な手法を用いることでリファクタリングする価値があるかどうかを判定できます。この種のツールは豊富にありますが、私たちが選んだのはAttractorです。

production環境でのカバレッジ(私たちはCoverband gemを使っています)は、コードのどの部分の重要性が高いかについて別の視点を提供してくれます。つまり、リファクタリング中はここに注目する必要があります。また、消す必要のある"デッドコード"(まったく実行されないコード)も検出できます。


残りは定番のgemです5

eval_gemfile 'gemfiles/rubocop.gemfile'

# gemfiles/rubocop.gemfile
gem 'standard'
gem 'rubocop-rspec'
gem 'rubocop-rails'

testdouble/standard - GitHub
rubocop/rubocop-rspec - GitHub
rubocop/rubocop-rails - GitHub

私たちのRuboCop化手法について詳しくは、以下の関連記事をどうぞ。

参考: RuboCoping with legacy: Bring your Ruby code up to Standard—Martian Chronicles, Evil Martians’ team blog

🔗 テストツール

テストを書いて実行することは日々の開発業務の一部でもありますが、それでも独自の工夫を盛り込むべきです。Ruby and Railsでアプリケーションを構築する大きなメリットのひとつは、高度なテストという文化が栄えていることです。

まずは基本的なものから。

gem 'rspec-rails'
gem 'factory_bot'

rspec/rspec-rails - GitHub
thoughtbot/factory_bot - GitHub

はい、私たちもRSpecのファンであり、fixtureよりもFactoryBotによるfactoryを重用しています(ただしfactoryだけを使うわけではありません)。MinitestとRSpecのどちらがいいか、factoryとfixtureのどちらがいいかという話は別の機会に譲ることにして、パラダイムに関わらないライブラリの話を続けることにします。

gem 'cuprite'
gem 'site_prism'

gem 'capybara-thruster'

rubycdp/cuprite - GitHub
site-prism/site_prism - GitHub
evilmartians/capybara-thruster - GitHub

高速かつ壊れにくいシステムテストを作成するには、最新のブラウザ制御ツール(Cuprite)を利用するとともに、テストシナリオをオブジェクト指向で記述します(site_prismのPage Object Modelパターンを用います)。

Railsでブラウザテストを飼いならす方法について詳しくは、以下の記事をどうぞ。

Railsでブラウザテストを「正しく」行う方法(翻訳)


追加 Capybara Thrusterは、BasecampのHTTP/2プロキシであるThrusterをシステムテストのWebサーバーとして利用可能にするマイクロgemです。私たちはThrusterをproduction環境(やdevelopment環境)のサーバーとして採用したわけではありませんが、Thrusterはアセットの配信が非常に高速でページ読み込み時間を短縮できるので、ブラウザベースのテストで非常に便利であることがわかりました。
なお、AnyCableを利用するプロジェクトでは、Evil Martians特製のAnyCable Thrusterディストリビューションを用いれば追加ハックなしでAnyCableを実行可能になるので、Thrusterは既にマストアイテムです。

anycable/thruster - GitHub


gem 'with_model'

Casecommons/with_model - GitHub

with_modelも私たちのお気に入りgemのひとつです。かなり昔に、Railsモデルのconcernsのテストを改善する方法を探していたときに初めてこのgemを発見しました。このライブラリを使うと、データベースの一時テーブルに接続された一時的なモデルを作成できるようになるので、モジュールやRailsのconcernsはもちろん、振る舞いが共有される任意の実装をテストできるようになります。

gem 'n_plus_one_control'

palkan/n_plus_one_control - GitHub

N+1クエリ問題は、Railsアプリケーションで頻発する問題のひとつです。N+1クエリ問題は発生しやすく、問題を検出するツールはいくつかあるものの、(最近のActive Recordの.strict_loading機能を除けば)問題を防止することまではできません。

n_plus_one_control gemを使えば、今後N+1クエリの発生を防止するテストを書けるようになります。


gem 'webmock'
gem 'vcr'

bblimke/webmock - GitHub
vcr/vcr - GitHub

テストが外の世界にアクセスするようなことは決してあってはなりません。私の経験則はいたってシンプルで、ネットのつながらない飛行機に乗っていても実行すればパスするテストだけが、良いテストです。

webmockWebMock.disable_net_connect!rails_helper.rbまたはtest_helper.rbに追加するだけで、テストが実際のネットワーク呼び出しに依存することを回避できます。


ネットワーク経由の呼び出しや時間も、不安定なテストの原因になる可能性があります。

gem 'zonebie'

alindeman/zonebie - GitHub

テストが時間に依存するのをエレガントに防ぐ方法のひとつは、タイムゾーンをランダムに変更してテストを実行することです。zonebie gemをバンドルに含めるだけでこれをやってくれます。

私たちは、かの有名なTimecop gemを使っていない点にご注意ください(TimecopはRailsでは冗長です)。代わりに、以下の記事のようにRails組み込みのTimeHelpersを利用できます。

参考: Replace Timecop With Rails’ Time Helpers in RSpec - Andy Croll


gem 'rspec-instafail', require: false
gem 'fuubar', require: false

grosser/rspec-instafail - GitHub
thekompanee/fuubar - GitHub

RSpecユーザー向けの便利ツールもいくつか紹介します。

Fuubarは、RSpecのプログレスバーを見やすくフォーマットします(これがRSpecのデフォルトでない理由がわかりません)。

rspec-instafailも、CIでテストを実行するときに便利なフォーマッタです。エラーが発生した時点でエラーが表示されるので、テストスイートの実行完了を待たずに修正を開始できます。


最後に、私たちのGemfileでtestグループのトリを務める重要なgemを紹介します。

gem 'test-prof'

test-prof/test-prof - GitHub

TestProfは、"Railsの遅いテストスイートを診断する名医"です。テストが数百件であろうと数万件に達していようと、速いテストは常に価値があります。TestProfを使えば、わずかな手間でテスト実行時間を劇的に改善できます。

TestProf: Ruby/Railsの遅いテストを診断するgem(翻訳)

🔗 その他もろもろ

私たちが使っている素晴らしいライブラリすべてを1本のブログ記事だけで解説するのは基本的に無理です。そこでボーナスとして、いくつかの便利ライブラリをピックアップして紹介します。

gem 'anycable-rails'

gem 'feature_toggles'

gem 'redlock'

gem 'anyway_config'

gem 'retriable'

gem 'nanoid'

gem 'dry-initializer'
gem 'dry-monads'
gem 'dry-effects'

それぞれのgemについて、使いたくなる状況を手短に記します。

  • リアルタイム機能を構築したい: AnyCableの出番です。

anycable/anycable - GitHub

  • フィーチャートグル機能を手軽に導入したい:  feature_togglesという最小限のソリューションがあります。

bibendi/feature_toggles - GitHub

  • 分散ロックメカニズムを探している: Redisはそのような場合に最適です。redlockはそのための方法です。

leandromoreira/redlock-rb - GitHub

  • アプリケーションのコンフィグを管理したい: Anyway Configのコンフィギュレーションクラスの利用を検討してみましょう。

palkan/anyway_config - GitHub

  • リトライロジックを手書きするのがだるい: retriable gemをチェックしてみましょう。

kamui/retriable - GitHub

  • 追加 一意の識別子を高速かつ欲しい形で生成したい方は、nanoid をチェックしてみましょう(弊社のAndrey Sitnikが作ったnanoid JSの移植版です)。

radeno/nanoid.rb - GitHub

dry-rb/dry-initializer - GitHub
dry-rb/dry-monads - GitHub

  • 抽象化されたレイヤ間でコンテキストをもっと安全に受け渡ししたい: dry-effectsライブラリの"algebraic effects"を試してみましょう。

dry-rb/dry-effects - GitHub

この調子でいくらでも続けられますが、あまり押し付けがましいのも考えものです。理想のツールベルトはエンジニアごとに違って当然なので、いろいろ試しながら自分に合ったツールベルトを作り上げましょう!その他のツールについては鵜呑みにせず、参考程度に考えて使いましょう。

🔗 以上、夢のお話でした

さて、どうやら現実に帰ってくるときが来たようです。火星人たちの「理想の」Gemfileがどのようなものか、皆さんにもある程度想像いただけることを、そして運良く皆さんも自分なりの夢を描くためのインスピレーションを得られることを願っています。

追加 寝る前に読めば、きら星のようなgemで満たされた素晴らしいものになる夢を見られること請け合いの良書をお探しでしたら、私が書いた『Layered Design for Ruby on Rails applications』をぜひご検討ください(本記事で紹介したgemのほとんどが同書に収録されているのを見てもどうか驚かないでください)。

P.S. 悪夢のようなgemもたまにあったりします↓


Evil Martiansは、お客様のプロジェクトやプロダクトの設計ニーズにお応えいたします。夢のRailsプロジェクト(あるいはそれ以上のプロジェクト)であるかを問わず、お客様のWebアプリケーションやモバイルアプリケーションで、プロダクト設計、フロントエンド、ソフトウェア信頼性のような専門家による解決が必要な問題がおありでしたら、いつでも私たちが出動いたします!プロジェクトを軌道に乗せて現実化したいお客様もお待ちしております。元記事のフォームまでご相談をお寄せください。

🔗 Changelog

原文Changelog

🔗 1.1.0(2024-05-30)

  • 追加 gemfile.directoryサイトの説明

  • 追加 書籍『Layered design for Rails』の説明

  • 追加 以下のgem

    • nanoid
    • capybara-thruster
    • vernier
    • silencer(サンプル付き)
    • skooma
    • imgproxy-rails
  • 削除 activerecord_postgres-enum gem(既にRailsでサポート済みのため)

  • 追加 good_job gemとsolid_queue gem

🔗 1.0.1 (2023-01-20)

  • redis-mutexredlockに置き換えた(redis-mutexの作者自らおすすめまでしているため)

関連記事

AnyCable 1.0: RubyとGoによるリアルタイムWebの4年間(翻訳)

Rails: anyway_config gemでRubyの設定を正しく整理しよう(翻訳)


  1. 訳注: GVLは、Ruby 3.0以降thread_schedと呼ばれるようになりました(参考: Rubyの(グローバル)VMロックをトレースする(翻訳)) 
  2. 参考: §1.11.1 必要条件 -- Active Model の基礎 - Railsガイド 
  3. 訳注: Rails 7.2からデフォルトでBrakemanが含まれます(#50507)。 
  4. 訳注: コードメトリクスにおいて、churn(=撹拌)はファイルの更新頻度の高さを表す指標のことで、complexity(複雑さ)やコードカバレッジなどと組み合わせて評価されることがよくあります(参考1参考2)。 
  5. 訳注: Rails 7.2からrubocop-rails-omakase gemがデフォルトで含まれるようになりました(#50486)。 

CONTACT

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