
導入
お久しぶりです!株式会社スタメンの ちぇる です。前回の「ながらRuby会議01」に続き、今回は「Kaigi on Rails 2025」に参加してきました!
Kaigi on Rails とは、「初学者から上級者までが楽しめるWeb系の技術カンファレンス」です!年に一度開催され、国内外から多くの参加者やスピーカーが集まり、Railsに関するさまざまなテーマでの講演や交流が行われます。
弊社スタメンの福利厚生には「まるっとカンファレンス補助」という制度が存在し、今回はこちらを利用して、名古屋から東京へ1泊2日でカンファレンスに参加させていただきました🙌


印象に残ったセッション
2日間にわたって開催された Kaigi on Rails は、新たな発見のある講演ばかりでした。その中でも、本記事では特に印象に残ったセッションに関してご紹介させていただきます🖊️
入門 FormObject
Ruby on Railsで実装していると、Rails Wayから外れることってありませんか?本セッションでは、その代表例である FormObject の使い所に関するお話を聞くことができました。僕自身も、以前に業務で FormObject を使っていたこともあり、非常に勉強になる点が多かったです。
そもそも、FormObject の使用基準は何でしょうか?パッと考えてみると、以下のような観点が挙げられます。
- バリデーションに
ifがついたら? - メールフォームを実装するとき?
accepts_nested_attributes_forの代わり?
本セッションでは、普段なんとなく使っていた FormObject の使い所を明確に言語化・整理されており、とても納得感がありました。
FormObjectの特徴
- ① DBに紐づかないオブジェクト
- ② モデルと同じインターフェースでコントローラから呼ばれる
- ③ 独自のライフサイクル処理をもてる
- ④ ビューの状態を保持できる
(例)
class UserNewForm include ActiveModel::Model attr_accessor :email, :name validates :email, :name, presence: true before_action :email_to_lower_case def save end private def email_to_lower_case end end
class UsersController ApplicationController def create @form = UserNewForm.new(create_params) if @form.save redirect_to new_users_path, notice: 'ユーザを作成しました' else render :new end end end
FormObjectの使い所
「なぜ、FormObjectが必要になるのか?」という問いに対する結論は、以下であると述べられていました。
Rails Wayの制約があると、ユーザの目的が実現できないから
Rails Wayのフォーム処理は、次のような前提のもとに成り立っています。
- 一度に操作するモデルの数が一つ
- モデルのライフサイクル処理のパターンが一つ
つまり、これら前提が壊れる時が FormObject の使い所なのです!例えば、以下のようなケースが挙げられます。
- 一度に操作するモデルの数が一つではない
- モデルを操作しない(メールフォーム等)
- 複数のモデルを操作する(会社情報と個人情報の同時更新等)
- モデルのライフサイクル処理をアクションごとに分ける(ユーザの作成と編集でバリデーションを分ける等)
特に、現代のユーザは複雑な操作をワンタップで実現したいという要望も強く、Rails Wayから外れることがしばしばあります。しかし、こういった「レールの伸ばし方」は先人たちが用意してくれているため、巨人の肩に乗る姿勢を持つことが大切であるとお話しされていました。
今後は、FormObject を「なんとなく使う」のではなく、「使うべき所で使う」ように注意していきたいと思いました!
2分台で1500examples完走!爆速CIを支える環境構築術
CIが高速であるということは、ビジネスに直結します。なぜなら、不具合の原因特定と修正のサイクルを早く回すことができ、安定したリリースを早期にすることができるからです。
本セッションでは、開発において欠かせないCIをどのように速くしたか、その体験談をお聞きすることができました。
RSpecの実行を早くするには
全体のCI処理が遅くなってしまう原因として、「直列実行でCPUを使いきれていない」ことが挙げられます。そのため、まずは並列処理にすることが、CIの実行時間を短縮する選択肢の一つと考えられます。
そこで、TwoGateさんでは parallel_tests を導入するに至ります。この gem は、マルチプロセスによる並列実行をサポートしています。
また、spec単位での実行時間にはバラツキがあります。つまり、均等に並列実行しないと求めている効果が得られません。そこで、spec単位の実行時間の履歴を記録して最適な分割をしてくれる knapsack_pro-ruby の gem も導入されていました。
これらの対応後、8コアCPU で 8並列 で Rspec を実行したそうです。結果、本来であればCI実行時間の 1/8 程度の短縮を期待できそうですが、実際は 29分38秒 → 23分20秒 の短縮にとどまり、思うような結果が得られませんでした。なぜなら、8並列にしたことで、DB書き込みによるディスクI/Oに負荷がかかってしまったためです…。

次の改善として、ストレージに tmpfs を利用しました。tmpfs はメモリ上に構築されるファイルシステムで、再起動時にデータが消失する点が特徴です。そして、この変更によってディスクI/Oの負荷が軽減され、CIの実行時間は 29分38秒 から 5分 へと短縮され、なんと 約6倍 のスピードアップを実現することができたそうです!

さらになんと、サーバーをクラウドマシンから物理マシンにしてスペックを上げたことで、1分59秒 までCI実行時間の削減を実現されていました。物理マシンの導入は、多くの企業で容易にできる選択ではないと思いますが、コスト観点からもTwoGateさんはメリットを享受できたそうです。すごいですね。
弊社でも、CI実行時間には一定の課題感を持っており、本セッションの内容を応用できないか、是非とも参考にさせていただきたいと感じました!
「技術負債にならない・間違えない」権限管理の設計と実装
権限管理は、システムにおいて非常に重要な機能を果たします。これらが間違って設定されると、「給与を第三者に公開してしまった」「取引先を公開してしまった」といった事態になりかねません。それにより、サービスへの信頼が大きく下がり、事業への損失が大きくなります。つまり、権限管理のミスは許されないのです。
そんな権限管理において、本セッションでは適切な実装方法をレクチャーしていただきました。まずは、よくある間違った実装を見てみます。
class ProjectsController ApplicationController def create unless current_user.admin? redirect_to root_path, alert: '権限が必要です。' return end end end
admin?の判定はアンチパターンです。なぜなら、役割は変わりゆくからです。また、admin?と記載した場合、admin がどのような権限を持っているか、その先のコードを見にいかなければなりません。
役割に依存した実装は、権限が暗黙的になるので、技術的負債になりがちです。さらに、そもそもの役割の権限が変化した際、多くの判定箇所を見に行って修正する必要があります。
- レビュワーは、常に役割の権限を知らないといけない。
- 判定箇所が散らばり、どのような権限が定義されているのかわからない。
そこで、役割ではなく権限に依存する必要があります。
class ProjectsController ApplicationController def create unless current_user.can_create_project? redirect_to root_path, alert: '権限が必要です。' return end end end
class User ApplicationRecord enum :role, %i(admin manager normal external) def can_create_project? admin? || manager? end end
これだけでも、だいぶ見やすくなります。
さらに、権限の要素を「対象」・「操作」・「役割」・「条件」に分割して考えることで、権限管理をより綿密に行うことができるとお話しされていました。
【権限操作の呼び出し】
project = Project.find(1) readable_project = Policy.authorize(current_user, project, :read) projects = Project.all readable_projects = Policy.authorize_scope(current_user, projects, :read) permissions = Policy.permissions(current_user)
【権限操作のインターフェース】
module Policy def self.authorize(user, record, action) context = Context.new(user:) context.authorize(record, action) end def self.authorize_scope(user, scope, action) context = Context.new(user:) context.authorize_scope(scope, action) end def self.permissions(user) context = Context.new(user:) context.permissions end end
【上記の内部実装】
module Policy class Context def authorize(record, action) policy = policy_class(user, record.class).new(user:, record:, mode: :record) policy.record end def authorize_scope(scope, action) policy = policy_class(user, scope.klass).new(user:, record:, mode: :scope) policy.public_send(action.to_sym) end def permissions end private def policy_class(user, record_class) role = user.role.camelize "Policy::#{record_class}::Roles::#{role}".safe_constantize end end end
【権限の基底クラス】
module Policy class Base attr_reader :user, :record, :scope, :mode def initialize(user:, record: nil, scope: nil, mode: nil) @user = user @record = record @scope = scope @mode = mode end end end
【具体的な権限クラス】
module Policy module Project module Role class Manager Base def update case mode when :record assignee? || author? when :scope when :list end end end end end end
このような設計にすることで、権限の有無が一目で分かるようになりました。
その結果、プレックスさんでは CS や PdM がソースコードの一次情報を見て理解できるようになり、開発側への問い合わせが減少。エンジニアは他の開発業務に専念できるようになりました。
さらに、Railsサーバー側からフロントに対して権限のマッピング情報を提供するようになり、フロント側の実装も自然と役割ではなく権限に依存するようになったそうです。
複雑な権限管理を正しく設計したことで、多くの嬉しい副次的効果が得られたんですね!
まとめ
Kaigi on Rails、本当に面白かったです。Railsエンジニアとして、他の活躍されているエンジニアの方々の知見を直接共有していただける機会は、非常に貴重だと感じました。
以下、カンファレンスで特に印象に残った言葉です。
- ソフトウェアの実装における最適解は、常に場合によります
Rails Wayがあるとはいえ、環境や目標は企業ごとに異なります。そのため、最適解も状況によって変わります。今回のようなカンファレンスに参加し、自社の取り組みだけでなく、他社の事例も取り入れることで、視野を広げ、より柔軟に考えるきっかけになったと感じました。
今後も実装で迷うことは多々あると思いますが、どの選択が最善か、常に考え続けていきたいと思います!
最後に
株式会社スタメンでは、一緒に働く仲間を募集しています!詳しくはこちらをご覧ください🙌