Ruby
Rails
Trailblazer

Rails5.2 Trailblazer 導入 7アクションコードサンプル(備忘録)

概要

 Ruby on Rails における gem Trailblazer についての記事になります。
 Trailblazer とは、 MVC モデルにおける M(Model) or C(Controller) の肥大化を防ぐための gem ですね!Model でValidationをはると、どうしても様々な問題が起きてきます。2つ目の問題点を解決するために Form ごと の Validation をはるようにしています。

  • コード量の増加
  • Model に無いパラメータをどこで対応するか
  • (etc …)

 https://github.com/trailblazer/trailblazer
 Trailblazer を用いたサンプルアプリ

目次

  • 開発環境
  • サンプルアプリの作成
  • new・create の実装
  • show・index の実装
  • edit・update の実装
  • destroy の実装
  • 関連が1対多 の場合

開発環境

OS: MaxOS High Sierra Version 10.13.6
Ruby: 2.5.0
Ruby on Rails: 5.2.0

サンプルアプリの作成

rails new


 まずはじめに以下のコマンドで、サンプルアプリを作成しましょう。

$ rails new trailblazer_app -d mysql

Gemfile の編集


 次に gem ファイルを編集します。今回は、デフォルトで生成されるものに、以下のものを追加します。

# 環境変数設定用 gem
gem 'dotenv-rails'

# trailblazer 用 gem
gem 'trailblazer-cells'
gem 'trailblazer-loader'
gem 'trailblazer-rails'
gem 'reform-rails'

# DB スキーマ管理用 gem 
gem 'ridgepole'

 いつのものやつ実行します。

$ ./bin/bundle install --path vendor/bundle

.gitignore の編集


 次に .gitignore ファイルを編集します。こちらは適宜でお願いします。

# Ignore bundler config.
/.bundle
/vendor/bundle

# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep

# Ignore uploaded files in development.
/storage/*

/node_modules
/yarn-error.log

/public/assets
.byebug_history

# Ignore master key for decrypting credentials and more.
/config/master.key

# Ignore Rubymine config.
/.idea

# Ignore env file.
.env

DBの設定


 Rails5 + MySQL + Ridgepoleによる DB設計・開発(備忘録)を参照してください。今回の .env ファイル、 database.yml、 ER図 は以下のようにしておきます。
 以降、DBの作成・ユーザーの作成(・スキーマファイルの作成)までできているということで話を進めていきます。

(.env)

DATABASE_USERNAME="trailblazer_app"
DATABASE_PASSWORD="trailblazer_app"
DATABASE_HOSTNAME="localhost"
(database.yml)

default: &default
  adapter:  mysql2
  encoding: utf8
  pool:     5
  username: <%= ENV['DATABASE_USERNAME'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  host:     <%= ENV['DATABASE_HOSTNAME'] %>
  database: trailblazer_app

production:
  <<: *default

development:
  <<: *default

test:
  <<: *default
  database: trailblazer_app_test

Screen Shot 2018-08-08 at 18.35.51.png

Model の作成


 以下のようなモデルになりますね。それでは、次から Teacher に対する操作を(最後に User に対する操作を)行なっていきたいと思います。

class User < ApplicationRecord
  belongs_to :teacher
  self.table_name = 'users'
end
class Teacher < ApplicationRecord
  has_many :users
  self.table_name = 'teachers'
end

new・create の実装

routes.rb の編集


 あらかじめ、すべてのアクションにおけるルーティングを作成しておきましょう。

Rails.application.routes.draw do
  resources :users
  resources :teachers
end

Trailblazer 用ファイルの作成


 Trailblazer 用ファイルは、主に2つあります。それらは、 concepts/.*/contractconcepts/.*/operation です。
 concepts/.*/contract/ では、 Form ごとのパラメータと Validation を記述します。
 concepts/.*/operation/ では、 ビジネスロジックを記述した各ファイルを格納します。

application_contract.rb の作成

 このファイルを全ての contract ファイルが継承するようにします。

class ApplicationContract < Reform::Form
  include Reform::Form::ActiveRecord
  require 'reform/form/validation/unique_validator'
end

application_operation.rb の作成

 このファイルを全ての operation ファイルが継承するようにします。

class ApplicationOperation < Trailblazer::Operation
  protected

  def handle_validation_error(options, **)
    options['validation_errors'] = options['contract'].errors.to_hash
  end

  def handle_internal_error!(options, **)
    options['operation_class'] = self.class
    raise TrailblazerApp::Error::OperationError.new(options)
  end
end

concepts/teachers/contract/create.rb の作成

 ここには、入力フォームの各パラメータを記述します。ただ、主にフォームの各パラメータは、 Model に準ずることが多いので、ある Model オブジェクトを使用できるようにしておきます。

class Teachers::Contract::Create < ApplicationContract
  NAME_MAX_LENGTH = 64

  model Teacher

  property :name
  # property :password_confirmation, virtual: true

  validates(
    :name,
    presence: true,
    length: { maximum: NAME_MAX_LENGTH }
  # unique: { scope: %i[name class], case_sensitive: false },
  )
end

concepts/teachers/operation/create.rb の作成

 ここには、入力フォームの各パラメータの Validation Check。DB との操作を行います。

class Teachers::Operation::Create < ApplicationOperation
  step :validate
  failure :handle_validation_error, fail_fast: true
  step :persist!
  failure :handle_internal_error!

  private

  def validate(options, params)
    contract = Teachers::Contract::Create.new(Teacher.new)

    contract.name = params[:name]
    # contract.~~~ = params[:~~~]

    options['contract'] = contract

    contract.valid?
  end

  def persist!(options, **)
    contract = options['contract']
    model = Teacher.new(
        name: contract.name
    )

    options['model'] = model

    model.save!
  end
end

TeachersController の作成


 今回、 Controller には、以下を記述しておきます。 index アクションは詳細に実装しませんが、画面遷移のため、記述だけしておきます。一応、エラーメッセージその他もろもろ表示できるように ApplicationControllerApplicationHelper も編集しておきましょう。

class TeachersController < ApplicationController
  def index

  end

  def new
    @form = Teachers::Contract::Create.new(Teacher.new)
  end

  def create
    # teacher_params = params.require(:teacher).permit(:name. ~~~, ~~~)
    teacher_params = params.require(:teacher).permit(:name)
    result = Teachers::Operation::Create.call(teacher_params)

    if result['validation_errors'].present?
      @form = result['contract']
      flash[:error] = translate_validation_errors(result)
      render 'new'
    else
      redirect_to teachers_path, flash: { notice: 'Teacher を作成しました。' }
    end
  end
end

ApplicationController の編集

class ApplicationController < ActionController::Base
  # error 文 を ja.yml を用いて翻訳する。
  def translate_validation_errors(result)
    validation_errors = result['validation_errors']
    class_name = result['contract'].instance_variable_get(:@model).class.name
    error_messages = []
    validation_errors.each do |key, values|

      values.each do |value|
        error_messages << I18n.t('activerecord.attributes.' << class_name.downcase << '.' << key.to_s) + value.to_s
      end
    end
    error_messages
  end
end

ApplicationHelper の編集

module ApplicationHelper
  # ページごとの完全なタイトルを返す。
  def full_title(page_title = '')
    base_title = 'Trailblazer App'
    if page_title.empty?
      base_title
    else
      "#{page_title} | #{base_title}"
    end
  end
end

config/initializers/locale.rb の作成

I18n.config.available_locales = :ja
I18n.default_locale = :ja

ja.yml の作成

ja:
  # --- Default ja.yml (https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/ja.yml) ---
  activerecord:
    errors:
      messages:
        record_invalid: "バリデーションに失敗しました: %{errors}"
        restrict_dependent_destroy:
          has_one: "%{record}が存在しているので削除できません"
          has_many: "%{record}が存在しているので削除できません"
  date:
    abbr_day_names:
    - 
    - 
    - 
    - 
    - 
    - 
    - 
    abbr_month_names:
    -
    - 1月
    - 2月
    - 3月
    - 4月
    - 5月
    - 6月
    - 7月
    - 8月
    - 9月
    - 10月
    - 11月
    - 12月
    day_names:
    - 日曜日
    - 月曜日
    - 火曜日
    - 水曜日
    - 木曜日
    - 金曜日
    - 土曜日
    formats:
      default: "%Y/%m/%d"
      long: "%Y年%m月%d日(%a)"
      short: "%m/%d"
    month_names:
    -
    - 1月
    - 2月
    - 3月
    - 4月
    - 5月
    - 6月
    - 7月
    - 8月
    - 9月
    - 10月
    - 11月
    - 12月
    order:
    - :year
    - :month
    - :day
  datetime:
    distance_in_words:
      about_x_hours:
        one: 約1時間
        other: 約%{count}時間
      about_x_months:
        one: 約1ヶ月
        other: 約%{count}ヶ月
      about_x_years:
        one: 約1年
        other: 約%{count}年
      almost_x_years:
        one: 1年弱
        other: "%{count}年弱"
      half_a_minute: 30秒前後
      less_than_x_minutes:
        one: 1分以内
        other: "%{count}分未満"
      less_than_x_seconds:
        one: 1秒以内
        other: "%{count}秒未満"
      over_x_years:
        one: 1年以上
        other: "%{count}年以上"
      x_days:
        one: 1日
        other: "%{count}日"
      x_minutes:
        one: 1分
        other: "%{count}分"
      x_months:
        one: 1ヶ月
        other: "%{count}ヶ月"
      x_years:
        one: 1年
        other: "%{count}年"
      x_seconds:
        one: 1秒
        other: "%{count}秒"
    prompts:
      day: 
      hour: 
      minute: 
      month: 
      second: 
      year: 
  errors:
    format: "%{attribute}%{message}"
    messages:
      accepted: を受諾してください
      blank: を入力してください
      present: は入力しないでください
      confirmation: と%{attribute}の入力が一致しません
      empty: を入力してください
      equal_to: は%{count}にしてください
      even: は偶数にしてください
      exclusion: は予約されています
      greater_than: は%{count}より大きい値にしてください
      greater_than_or_equal_to: は%{count}以上の値にしてください
      inclusion: は一覧にありません
      invalid: は不正な値です
      less_than: は%{count}より小さい値にしてください
      less_than_or_equal_to: は%{count}以下の値にしてください
      model_invalid: "バリデーションに失敗しました: %{errors}"
      not_a_number: は数値で入力してください
      not_an_integer: は整数で入力してください
      odd: は奇数にしてください
      required: を入力してください
      taken: はすでに存在します
      too_long: は%{count}文字以内で入力してください
      too_short: は%{count}文字以上で入力してください
      wrong_length: は%{count}文字で入力してください
      other_than: は%{count}以外の値にしてください
    template:
      body: 次の項目を確認してください
      header:
        one: "%{model}にエラーが発生しました"
        other: "%{model}に%{count}個のエラーが発生しました"
  helpers:
    select:
      prompt: 選択してください
    submit:
      create: 登録する
      submit: 保存する
      update: 更新する
  number:
    currency:
      format:
        delimiter: ","
        format: "%n%u"
        precision: 0
        separator: "."
        significant: false
        strip_insignificant_zeros: false
        unit: 
    format:
      delimiter: ","
      precision: 3
      separator: "."
      significant: false
      strip_insignificant_zeros: false
    human:
      decimal_units:
        format: "%n %u"
        units:
          billion: 十億
          million: 百万
          quadrillion: 千兆
          thousand: 
          trillion: 
          unit: ''
      format:
        delimiter: ''
        precision: 3
        significant: true
        strip_insignificant_zeros: true
      storage_units:
        format: "%n%u"
        units:
          byte: バイト
          gb: GB
          kb: KB
          mb: MB
          tb: TB
          pb: PB
          eb: EB
    percentage:
      format:
        delimiter: ''
        format: "%n%"
    precision:
      format:
        delimiter: ''
  support:
    array:
      last_word_connector: 
      two_words_connector: 
      words_connector: 
  time:
    am: 午前
    formats:
      default: "%Y年%m月%d日(%a) %H時%M分%S秒 %z"
      long: "%Y/%m/%d %H:%M"
      short: "%m/%d %H:%M"
    pm: 午後
  # --- Default ja.yml (https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/ja.yml) ---
  # --- 各 Model のパラメータ ---
  activerecord:
    attributes:
      teacher:
        id: "ID"
        name: "名前"

html.erb の作成


 今回は、次の4つのファイルを作成します。

  • views/teachers/index.html.erb(まだ特に記述はしません。)
  • views/teachers/new.html.erb
  • views/shared/_error_messages.html.erb
  • views/layouts/application.html.erb
<!-- index.html.erb -->

(まだ記述無し)
<!-- new.html.erb -->

<%= form_with model: @form, url: teachers_path, method: :post do |f|%>
  <%= render 'shared/error_messages' %>

  <label>名前</label>
  <input type="text" name="teacher[name]", value="<%= @form.name %>", placeholder="名前" />
  <button type="submit">登録する</button>
<%- end -%>
<!-- _error_messages.html.erb -->

<% if flash[:error].present? %>
  <div>
    <div class="error_text">
      <h2><%= "#{flash[:error].count}個のエラーがあります。" %></h2>
      <ul>
        <% flash[:error].each do |e| %>
          <li><%= e %></li>
        <% end %>
      </ul>
    </div>
  </div>
<% end%>
<!-- application.html.erb -->

<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <% flash.each do |message_type, messages| %>
      <% if message_type == 'alert' || message_type == 'notice' || message_type == 'info' || message_type == 'warning' %>
        <div class="flash_explanation_<%= message_type %>">
          <% messages.each do |message| %>
            <p><%= message %></p>
          <% end %>
        </div>
      <% end %>
    <% end %>
    <%= yield %>
  </body>
</html>

show・index の実装

 この部分に限っては、Trailblazer ファイルは作成されないので、簡単に流していきます。

TeachersController の編集

class TeachersController < ApplicationController
  # --- 追加部分 --- 
  def index
    if params[:name].nil?
      @teachers = Teacher.all
    else
      @teachers = Teacher.where(name: params[:name])
    end
  end

  def show
    @teacher = Teacher.find(params[:id])
  end
  # --- 追加部分 --- 

  def new
    @form = Teachers::Contract::Create.new(Teacher.new)
  end

  def create
    # teacher_params = params.require(:teacher).permit(:name. ~~~, ~~~)
    teacher_params = params.require(:teacher).permit(:name)

    result = Teachers::Operation::Create.call(teacher_params)

    if result['validation_errors'].present?
      @form = result['contract']
      flash[:error] = translate_validation_errors(result)
      render 'new'
    else
      redirect_to teachers_path, flash: { notice: 'Teacher を作成しました。' }
    end
  end
end

ApplicationHelper

module ApplicationHelper
  # ページごとの完全なタイトルを返す。
  def full_title(page_title = '')
    base_title = 'Trailblazer App'
    if page_title.empty?
      base_title
    else
      "#{page_title} | #{base_title}"
    end
  end

  # ---追加部分---
  # 日付フォーマット
  def date_format(date, format)
    # date: 日付 ex) created_at
    # format: フォーマット
    # %A -> 曜日の名称(Sunday, Monday …)
    # %a -> 曜日の省略(Sun, Mon …)
    # %B -> 月の名称(January, February …)
    # %b -> 月の省略(Jan, Feb …)
    # %d -> 日付(0-31)
    # %H -> 時間(00-23)
    # %I -> 時間(01-12)
    # %M -> 分(00-59)
    # %m -> 月の数字(01-12)
    # %S -> 秒(00-60)
    # %Y -> 西暦
    # %y -> 西暦の下2桁
    # %Z -> タイムゾーン
    date.strftime(format)
  end
  # ---追加部分---
end

teachers/index.html.erb

<%= form_with url: teachers_path, method: :get, local: true do %>
  <label>名前</label>
  <input type="text" name="name" placeholder="名前"/>
  <button type="submit">検索する</button>
<% end %>

<% @teachers.each do |teacher| %>
  <p>ID: <%= link_to teacher.id, teacher_path(teacher.id) %></p>
  <p>名前: <%= teacher.name %></p>
  <p>作成日: <%= date_format(teacher.created_at, '%Y年 %m月 %d日') %></p>
  <p>更新日: <%= date_format(teacher.updated_at, '%Y年 %m月 %d日') %></p>
  <hr>
<% end %>

teachers/show.html.erb

<!-- show.html.erb -->

<p>ID: <%= @teacher.id %></p>
<p>名前: <%= @teacher.name %></p>
<p>作成日: <%= date_format(@teacher.created_at, '%Y年 %m月 %d日') %></p>
<p>更新日: <%= date_format(@teacher.updated_at, '%Y年 %m月 %d日') %></p>

edit・update の実装

concepts/teachers/contract/update.rb


 独自の validation メソッドを作成したい場合は、以下のようにします。今回は、 id が本当に存在するかどうかの validation をはることとします。

class Teachers::Contract::Update < ApplicationContract
  NAME_MAX_LENGTH = 64

  model Teacher

  property :id
  property :name

  validates(
    :name,
    presence: true,
    length: { maximum: NAME_MAX_LENGTH },
  )

  validate(
    :existence_of_teacher_id
  )

  def existence_of_teacher_id
    errors.add(:id, '存在しないTeacherです。') if Teacher.pluck(:id).exclude?(id.to_i)
  end
end

concepts/teachers/operation/update.rb


class Teachers::Operation::Update < ApplicationOperation
  step :validate
  failure :handle_validation_error, fail_fast: true
  step :persist!
  failure :handle_internal_error!

  private

  def validate(options, params)
    contract = Teachers::Contract::Update.new(Teacher.new)

    contract.id = params[:id]
    contract.name = params[:name]

    options['contract'] = contract

    contract.valid?
  end

  def persist!(options, **)
    contract = options['contract']
    model = Teacher.find(contract.id)

    model.name = contract.name

    options['model'] = model

    model.save!
  end
end

TeachersController


class TeachersController < ApplicationController
  def index
    if params[:name].nil?
      @teachers = Teacher.all
    else
      @teachers = Teacher.where(name: params[:name])
    end
  end

  def show
    @teacher = Teacher.find(params[:id])
  end

  def new
    @form = Teachers::Contract::Create.new(Teacher.new)
  end

  def create
    # teacher_params = params.require(:teacher).permit(:name. ~~~, ~~~)
    teacher_params = params.require(:teacher).permit(:name)
    result = Teachers::Operation::Create.call(teacher_params)

    if result['validation_errors'].present?
      @form = result['contract']
      flash[:error] = translate_validation_errors(result)
      render 'new'
    else
      redirect_to teachers_path, flash: { notice: 'Teacher を作成しました。' }
    end
  end

  # --- 追加部分 --- 
  def edit
    @form = Teachers::Contract::Update.new(Teacher.find(params[:id]))
  end

  def update
    teacher_params = params.require(:teacher).permit(:name, :id)
    teacher_params[:id] = params[:id]

    result = Teachers::Operation::Update.call(teacher_params)

    if result['validation_errors'].present?
      @form = result['contract']
      flash[:error] = translate_validation_errors(result)
      render 'edit'
    else
      redirect_to teachers_path, flash: { notice: 'Teacher を更新しました。' }
    end
  end
  # --- 追加部分 --- 
end

teachers/edit.html.erb


<%= form_with model: @form, url: teacher_path, method: :patch, local: true do %>
  <%= render 'shared/error_messages' %>

  <label>名前</label>
  <input type="text" name="teacher[name]" value="<%= @form.name %>" placeholder="名前" />
  <button type="submit">登録する</button>
<% end %>

destroy の実装

concepts/teachers/contract/destroy.rb


class Teachers::Contract::Destroy < ApplicationContract
  NAME_MAX_LENGTH = 64

  model Teacher

  property :id

  validates(
    :id,
    presence: true
  )


  validate(
      :existence_of_teacher_id
  )

  def existence_of_teacher_id
    errors.add(:id, '存在しないTeacherです。') if Teacher.pluck(:id).exclude?(id.to_i)
  end
end

concepts/teachers/contract/destroy.rb


class Teachers::Operation::Destroy < ApplicationOperation
  step :validate
  failure :handle_validation_error, fail_fast: true
  step :persist!
  failure :handle_internal_error!

  private

  def validate(options, params)
    contract = Teachers::Contract::Destroy.new(Teacher.new)

    contract.id = params[:id]

    options['contract'] = contract

    contract.valid?
  end

  def persist!(options, **)
    contract = options['contract']
    model = Teacher.find(contract.id)

    options['model'] = model

    model.destroy!
  end
end

TeachersController


class TeachersController < ApplicationController
  def index
    if params[:name].nil?
      @teachers = Teacher.all
    else
      @teachers = Teacher.where(name: params[:name])
    end
  end

  def show
    @teacher = Teacher.find(params[:id])
    @form = Teachers::Contract::Destroy.new(Teacher.find(params[:id]))
  end

  def new
    @form = Teachers::Contract::Create.new(Teacher.new)
  end

  def create
    # teacher_params = params.require(:teacher).permit(:name. ~~~, ~~~)
    teacher_params = params.require(:teacher).permit(:name)
    result = Teachers::Operation::Create.call(teacher_params)

    if result['validation_errors'].present?
      @form = result['contract']
      flash[:error] = translate_validation_errors(result)
      render 'new'
    else
      redirect_to teachers_path, flash: { notice: 'Teacher を作成しました。' }
    end
  end

  def edit
    @form = Teachers::Contract::Update.new(Teacher.find(params[:id]))
  end

  def update
    teacher_params = params.require(:teacher).permit(:name, :id)
    teacher_params[:id] = params[:id]

    result = Teachers::Operation::Update.call(teacher_params)

    if result['validation_errors'].present?
      @form = result['contract']
      flash[:error] = translate_validation_errors(result)
      render 'edit'
    else
      redirect_to teachers_path, flash: { notice: 'Teacher を更新しました。' }
    end
  end

  # --- 追加部分 --- 
  def destroy
    teacher_params = params.require(:teacher).permit(:id)

    result = Teachers::Operation::Destroy.call(teacher_params)

    if result['validation_errors'].present?
      @form = result['contract']
      redirect_to teachers_path, flash: { alert: translate_validation_errors(result)}
    else
      redirect_to teachers_path, flash: { notice: 'Teacher を削除しました。' }
    end
  end
  # --- 追加部分 --- 
end

teachers/show.html.erb

<p>ID: <%= @teacher.id %></p>
<p>名前: <%= @teacher.name %></p>
<p>作成日: <%= date_format(@teacher.created_at, '%Y年 %m月 %d日') %></p>
<p>更新日: <%= date_format(@teacher.updated_at, '%Y年 %m月 %d日') %></p>

<!-- 追加部分 -->
<%= link_to '編集', edit_teacher_path(@teacher.id) %>
<%= form_with model: @form, url: teacher_path(@teacher.id), method: :delete, local: true do %>
  <input type="hidden" name="teacher[id]" value="<%= @form.id %>" >
  <button type="submit">削除</button>
<% end %>
<!-- 追加部分 -->

関連が1対多の場合

 それでは、 User Model に対する操作をやっていきます。

# (ja.yml: 末尾に以下を追記)

  # --追加部分--
  activerecord:
    attributes:
      teacher:
        id: "ID"
        name: "名前"
      user:
        id: "ID"
        name: "名前"
        teacher_id: "Teacher ID"
  # --追加部分--
# (users/contract/create.rb)

class Users::Contract::Create < ApplicationContract
  NAME_MAX_LENGTH = 64

  model User

  property :name
  property :teacher_id

  validates(
    :name,
    presence: true,
    length: { maximum: NAME_MAX_LENGTH }
  )

  validate(
    :existence_of_teacher_id
  )

  def existence_of_teacher_id
    errors.add(:teacher_id, 'が不正です。') if Teacher.pluck(:id).exclude?(teacher_id.to_i)
  end
end
# (users/operation/create.rb)

class Users::Operation::Create < ApplicationOperation
  step :validate
  failure :handle_validation_error, fail_fast: true
  step :persist!
  failure :handle_internal_error!

  private

  def validate(options, params)
    contract = Users::Contract::Create.new(User.new)

    contract.name = params[:name]
    contract.teacher_id = params[:teacher_id]

    options['contract'] = contract

    contract.valid?
  end

  def persist!(options, **)
    contract = options['contract']
    teacher = Teacher.find(contract.teacher_id)
    model = teacher.users.create(
        name: contract.name
    )

    options['model'] = model

    model.save!
  end
end
# (UsersController)

class UsersController < ApplicationController
  def index
    @users = User.all
  end

  def new
    @teachers = Teacher.all
    @form = Users::Contract::Create.new(User.new)
  end

  def create
    user_params = params.require(:user).permit(:teacher_id, :name)
    result = Users::Operation::Create.call(user_params)

    if result['validation_errors'].present?
      @teachers = Teacher.all
      @form = result['contract']
      flash[:error] = translate_validation_errors(result)
      render 'new'
    else
      redirect_to users_path, flash: { notice: 'User を作成しました。' }
    end
  end
end
<!-- (users/new.html.erb) -->

<%= form_with model: @form, url: users_path, method: :post, local: true do %>
  <%= render 'shared/error_messages' %>

  <label>教師</label>
  <select name="user[teacher_id]">
    <% @teachers.each do |teacher| %>
      <option value="<%= teacher.id %>"><%= teacher.name %></option>
    <% end %>
  </select>

  <label>名前</label>
  <input type="text" name="user[name]" value="<%= @form.name %>" placeholder="名前" />
  <button type="submit">登録する</button>
<% end %>
<!-- (users/index.html.erb) -->

<% @users.each do |user| %>
  <p>ID: <%= user.id %></p>
  <p>名前: <%= user.name %></p>
  <p>教師名: <%= user.teacher.name %></p>
  <p>作成日: <%= date_format(user.created_at, '%Y年 %m月 %d日') %></p>
  <p>更新日: <%= date_format(user.updated_at, '%Y年 %m月 %d日') %></p>
  <hr>
<% end %>