Share
Embed
[JA] Domain Modeling with Vanilla Rails: 「操作」から再考するRailsの設計論
Presented at Kaigi on Rails 2026 (October 16, 2026)
Original English version: https://decks.yasaichi.com/20261016/domain-modeling-with-vanilla-rails
Yuichi Goto
October 16, 2026
Transcript
78 slides
-
Domain Modeling with Vanilla Rails 「操作」から再考するRailsの設計論 Yuichi Goto | Kaigi
on Rails 2026 | October 16, 2026 -
メールによるサインアップの成功シナリオ(一部) 1. 2. 3. 4. 5. サインアップフォームを表示する メールアドレスを入力してフォームを送信する メールアドレスの形式が正しく、未登録であることを検証する メールアドレス未確認のユーザーを作成する
確認メールの送信を予約し、確認を促す画面を表示する 注 : はアプリケーション、 はユーザーを表す 8 -
モデル: メールアドレスの検証と確認メールの送信予約を実装 class User < ApplicationRecord validates :email, format: URI::MailTo::EMAIL_REGEXP,
uniqueness: true generates_token_for :email_verification, expires_in: 1.day do [ email, email_verified_at ] end after_create_commit :send_verification_email private def send_verification_email UserMailer.with(user: self).email_verification.deliver_later end end 注 : UserEmail を使えば、メールアドレスとその確認状態を User から分離できる 10 -
User の作成操作で実行されるバリデーションとコールバックを利用 class User < ApplicationRecord validates :email, format: URI::MailTo::EMAIL_REGEXP,
uniqueness: true after_create_commit :send_verification_email def send_verification_email UserMailer.with(user: self).email_verification.deliver_later end end 11 -
コントローラー: Userの作成操作を呼び出し、結果に応じて画面を表示 class UsersController < ApplicationController def new @user =
User.new end scaffold で生成した コントローラーとほぼ同じ def create @user = User.new(user_params) if @user.save redirect_to action: :pending_verification else render :new, status: :unprocessable_content end end end 12 -
ビューとルーティング: モデル名と状態からフォームの送信先を決定 <!-- app/views/users/new.html.erb --> User POST /users <%= form_with(model:
@user) do |form| %> <div> <%= form.email_field :email, required: true, autocomplete: "email" %> 新規の なので になる </div> <div> <%= form.submit "Continue" %> </div> <% end %> # config/routes.rb resources :users, only: %i[ new create ] 13 -
リソース・モデル・テーブルに対する操作を一対一に対応させる Resource routing Scaffold generator Active Record Request to a
resource Controller action Model operation Database operation POST /users UsersController#create User#save INSERT INTO users ... resources :users @user = User.new(...) @user.save 15 -
モデルのCRUD操作を中心にユースケース*を構成 成功シナリオを構成する振る舞い サインアップフォームの表示 メールアドレスの形式と登録有無の検証 メールアドレス未確認のユーザーの作成 メールアドレス確認メールの送信の予約 メールの確認を促す画面の表示 対応する実装 コントローラーとビュー User
作成時のバリデーション 新規Userに対するsave User の after_create_commit コントローラーとビュー * 本発表では、レコードの作成・更新・削除を伴う更新系のユースケースを対象とする 16 -
I was so excited about all the code I wasn’t
writing. 出典 : Rails World 2026 Opening Keynote - DHH 18 -
モデル: Userの作成操作を中心にGoogleサインアップを構成 class User < ApplicationRecord has_many :identities, class_name: "UserIdentity",
inverse_of: :user validates :email, format: URI::MailTo::EMAIL_REGEXP, uniqueness: true validates :email_verified_at, presence: true, on: :google_signup after_create_commit :send_verification_email, unless: :email_verified_at def self.from_omniauth(provider:, uid:, email:, email_verified:) # Return the linked user here if present; otherwise, create one below. User.new(email:) do |user| user.email_verified_at = Time.current if email_verified user.identities.build(provider:, uid:) user.save(context: :google_signup) end end end 注 : Devise ・ OmniAuth の Wiki の実装例 [1] [2] を題材に合わせて変更したもの 23 -
Google アカウントとの紐付け情報を持つ User を作成するファクトリ class User < ApplicationRecord has_many :identities,
class_name: "UserIdentity", inverse_of: :user def self.from_omniauth(provider:, uid:, email:, email_verified:) User.new(email:) do |user| user.identities.build(provider:, uid:) user.save(context: :google_signup) User を保存すると、新規の 紐付け情報も保存される end end end 24 -
メールアドレスの確認状態を検証し、確認メールは送信しない class User < ApplicationRecord on: と unless: で異なる振る舞いを実現 validates
:email_verified_at, presence: true, on: :google_signup after_create_commit :send_verification_email, unless: :email_verified_at def self.from_omniauth(provider:, uid:, email:, email_verified:) User.new(email:) do |user| user.email_verified_at = Time.current if email_verified user.save(context: :google_signup) end end end 25 -
コントローラー: Userの作成操作を呼び出し、結果に応じて画面を表示 class Omniauth::SessionsController < ApplicationController def create @user =
User.from_omniauth(**user_params) が保存済みかどうかで分岐 if @user.persisted? User start_new_session_for(@user) redirect_to after_authentication_url, notice: "User was ..." else redirect_to new_user_path, alert: "Could not authenticate you ..." end end private def user_params = request.env["omniauth.auth"].then { … } end 注 : Google アカウントによる認証のリクエスト・レスポンスの処理に OmniAuth を利用する場合の例 26 -
User モデルを用いた Google ・メールによるサインアップの全体像 User Email sign-up request Google auth
callback Controller action UsersController#create Specialized factory Model operation Database operation User.from_omniauth User#save INSERT INTO users ... Controller action Omniauth::Sessions… Conditional execution on: :google_signup unless: :email_verified_at 2 つのサインアップのユースケースが 1 つの User の作成操作に対応 27 -
なぜモデルは肥大化するのか モデルのCRUD操作を中心とするアプローチでは、更新系の ユースケースに固有の振る舞いが同じモデルに蓄積するから。 クラス・インスタンスメソッドとして: 特定のユースケースに固有の 処理を実装(ref. User.from_omniauth ) バリデーション・コールバックの実行条件として: ユースケースに
応じて実行する処理を切り替え(ref. on: :google_signup ) FYI: 『 Growing Rails Applications in Practice 』 [3] にも類似の指摘がある 28 -
過去のKaigi on Railsにおける振る舞いのモデリングに関する発表 出典 : 左から、 @moro [4] 、 @igaiga
[5] 、 @expajp [6] 31 -
共通の方針: ユースケース固有の振る舞いを既存のモデルから分離する Separate objects Email sign-up request Controller action UsersController#create
Dedicated operation #1 email_verification Google auth callback Controller action Omniauth::Sessions… User Dedicated operation #2 user.identites.build Model operation Database operation User#save INSERT INTO users ... Common behaviour validates :email 33 -
サインアップの成功シナリオの場合 現状の設計 本発表のアプローチ 両方のサインアップを同じ User の作成操作として捉える それぞれに固有の振る舞いを Userに実装 各サインアップを独立した ドメインの操作として捉える
それぞれに固有の振る舞いを 各操作を表現するモデルに実装 BTS: 本アプローチと『 Vanilla Rails is plenty 』 [7] との共通点から、タイトルに Vanilla を採用している 36 -
メールアドレスによるサインアップ操作を表現するモデル(一部) class EmailSignUp include ActiveModel::Attributes, ActiveModel::Model attribute :email, :string def
save return false if invalid? || !user.save UserMailer.with(user:).email_verification.deliver_later true end unless: :email_verified_at で実行条件を private def user @user ||= User.new(email:) end end 指定していたコールバックに相当 38 -
Google アカウントによるサインアップ操作を表現するモデル(一部) class GoogleSignUp # ActiveModel includes omitted. attribute :email_verified,
:boolean attribute :uid, :string validates :email_verified, comparison: { equal_to: true } def save return false if invalid? on: :google_signup で実行条件を 指定していたバリデーションに相当 user.identities.build(provider: :google, uid:) user.save end def user @user ||= User.new(email:, email_verified_at: Time.current) end end 39 -
参考: テーブルと対応しないモデルの基底クラス at WASO class ApplicationModel include ActiveModel::Validations::Callbacks, ActiveModel::Attributes, ActiveModel::Model
NotSaved = Class.new(StandardError) { … } def save save! rescue ActiveModel::ValidationError, ActiveRecord::RecordInvalid, NotSaved false end def save! validate! create_or_update || raise(NotSaved.new('Failed to save the model', self)) true end end : ApplicationModel @_h_s_ [8] 注 という名前は の発表 に由来している 40 -
サインアップ操作に固有の振る舞いが分離されたUserモデル class User < ApplicationRecord has_many :identities, class_name: "UserIdentity", inverse_of:
:user # In this example, we choose to keep the email validations in User. validates :email, format: URI::MailTo::EMAIL_REGEXP, uniqueness: true generates_token_for :email_verification, expires_in: 1.day do [ email, email_verified_at ] end end 41 -
サインアップ操作を表現するモデルに共通するバリデーション class EmailSignUp # Same implementation in GoogleSignUp USER_ATTRIBUTES =
%i[ email ].freeze validate :user_must_be_valid private def user_must_be_valid return if user.valid? User のエラーを操作を表現するモデルの エラーとして取り込む user.errors.each do |error| next unless error.attribute.in?(USER_ATTRIBUTES) errors.import(error) end end end 注 : ActiveModel::Errors#import は Rails 6.1 から利用できる 42 -
コントローラー: サインアップ操作を呼び出し、結果に応じて画面を表示 class EmailSignUpsController < ApplicationController def new @email_sign_up =
EmailSignUp.new end 呼び出すモデルが変わっても コントローラーの構成は同じ def create @email_sign_up = EmailSignUp.new(email_sign_up_params) if @email_sign_up.save redirect_to action: :pending_verification else render :new, status: :unprocessable_content end end end 43 -
操作を表現するモデルを用いたGoogle・メールによるサインアップの全体像 Operation models Email sign-up request Controller action EmailSignUpsController Save
EmailSignUp email_verification Google auth callback Controller action Omniauth::Sessions… User Save GoogleSignUp user.identites.build Model operation Database operation User#save INSERT INTO users ... Common behaviour validates :email リソースの作成操作とモデルの作成操作は 引き続き一対一に対応 44 -
テーブルと対応するサインアップ操作のモデル(一部) class EmailSignUp < ApplicationRecord belongs_to :referrer, class_name: "User", optional:
true belongs_to :user validate :user_must_be_valid, if: :user EmailSignUp を保存すると、 新規Userも保存される after_create_commit :send_verification_email delegate :email, to: :user, allow_nil: true private # NOTE: Same implementation as in the previous examples. def user_must_be_valid; end def send_verification_email; end end 注 : 2 つのモデルを STI で同じテーブルに対応させる設計も可能だが、本題から脱線するため割愛 50 -
操作を記録する場合もRailsのアプローチを維持できる class EmailSignUpsController < ApplicationController def create @email_sign_up = EmailSignUp.new(email_sign_up_params)
if @email_sign_up.save redirect_to action: :pending_verification else render :new, status: :unprocessable_content end end private def email_sign_up_params { user: User.new(params.expect(…)), referrer_id: session[:referrer_id] } end end 51 -
フォームオブジェクトを用いたメールアドレスによるサインアップ(一部) class EmailSignUpForm include ActiveModel::Model include ActiveModel::Attributes attribute :email, :string
def save return false if invalid? || !user.save UserMailer.with(user:) .email_verification.deliver_later true end private def user @user ||= User.new(email:) end end class EmailSignUpsController < ApplicationController def create @email_sign_up_form = EmailSignUpForm.new( email_sign_up_form_params ) if @email_sign_up_form.save redirect_to action: :pending_verification else render :new, status: :unprocessable_content end end end モデルの実装は、記録が不要な 操作を表現するモデルと同じ 53 -
イベントエンティティを用いたメールアドレスによるサインアップ(一部) class EmailSignUp < ApplicationRecord belongs_to :referrer, class_name: "User", optional:
true belongs_to :user class EmailSignUpsController < ApplicationController def create @email_sign_up = EmailSignUp.new( email_sign_up_params ) validate :user_must_be_valid, if: :user after_create_commit :send_verification_email delegate :email, to: :user, allow_nil: true private # NOTE: Same implementation # as in the previous examples. def user_must_be_valid; end def send_verification_email; end end if @email_sign_up.save redirect_to action: :pending_verification else render :new, status: :unprocessable_content end end end モデルの実装は、記録が必要な 操作を表現するモデルと同じ 54 -
操作を表現するモデルと既存のアプローチの関係 Concept level Form web forms Operation domain operations Event
entity domain activities Does the operation need a record? No Implementation level EmailSignUp(Form) uses ActiveModel Yes EmailSignUp uses ActiveRecord 55 -
「操作」という単一の概念から既存のアプローチの実装に帰着できる Concept level Form web forms Operation domain operations Event
entity domain activities Does the operation need a record? No Implementation level EmailSignUp uses ActiveModel Yes EmailSignUp uses ActiveRecord 56 -
再掲: Userモデルを用いたメールアドレスによるサインアップ class User < ApplicationRecord validates :email, format: URI::MailTo::EMAIL_REGEXP,
uniqueness: true generates_token_for :email_verification, expires_in: 1.day do [ email, email_verified_at ] end after_create_commit :send_verification_email private def send_verification_email UserMailer.with(user: self).email_verification.deliver_later end end 68 -
1. バリデーションとコールバックから User が扱うルールを明らかにする class User < ApplicationRecord validates :email,
format: URI::MailTo::EMAIL_REGEXP, uniqueness: true メールアドレスの形式が正しく、未登録であること after_create_commit :send_verification_email private ユーザーの作成に成功した場合、 確認メールの送信が予約されること def send_verification_email UserMailer.with(user: self).email_verification.deliver_later end end 69 -
2.1. 管理者によるユーザー招待のシナリオを想定 1. 2. 3. 4. 5. 管理者が招待先のメールアドレスを入力してフォームを送信する メールアドレス未確認のユーザーを作成する 招待メールの送信を予約する
招待されたユーザーがメールのリンクを開き、招待を承諾する 2で作成したユーザーのメールアドレスを確認済みにする 70 -
2.2. 確認メールの送信予約はサインアップ固有と判断 メールアドレスに User が扱う業務ルール 管理者による招待 よるサインアップ メールアドレスの形式が正しく、未登録 必須 必須
であること ユーザーの作成に成功した場合、確認 必須 必須でない メールの送信が予約されること 71 -
3.2. EmailSignUp モデルとして抽出し、その固有の振る舞いを集約 class EmailSignUp # ActiveModel includes omitted. attribute
:email, :string def save return false if invalid? || !user.save UserMailer.with(user:).email_verification.deliver_later true end private def user @user ||= User.new(email:) end end class User < ApplicationRecord validates :email, format: URI::MailTo::EMAIL_REGEXP, uniqueness: true end 73 -
全体のまとめ & ご清聴ありがとうございました Railsのアプローチ: 更新系のユースケースをモデルのCRUD操作を 中心に構成。高い生産性を実現するが、モデルの肥大化を招きやすい 操作を表現するモデル: 記録の要否によらず同じ概念を利用でき、 Railsのアプローチを維持しつつモデルの肥大化に対処できる モデル化すべきドメインの操作の発見:
モデルが扱う業務ルールに 着目し、それらが帰属する概念を再考する 実装先やレイヤーを議論する前に、「この業務ルールが帰属する概念は 何か」を問うことから始めてみましょう This deck is made with Marp. Thanks, @yhatt! 77 -
参考文献 1. OmniAuth: Overview↩ 2. Managing Multiple Providers↩ 3. Growing
Rails Applications in Practice↩ 4. Simplicity on Rails -- RDB, REST and Ruby↩ 5. Railsの仕組みを理解してモデルを上手に育てる - モデルを見つける、モデルを分割する良いタイミング -↩ 6. 入門 FormObject↩ 7. Vanilla Rails is plenty↩ 8. Rails with ApplicationModel↩ 9. オブジェクト指向入門 第2版 原則・コンセプト↩ 10. エリック・エヴァンスのドメイン駆動設計↩ 78