19
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Formオブジェクトパターン

Last updated at Posted at 2021-02-22

1つのフォームから送信したデータを複数のテーブルへ分けて保存する方法をFormオブジェクトパターンを使用して実装していきましょう。

プロジェクト作成

# 「investment_app」という名前で、Railsバージョン “6.0.0” で作成
% rails _6.0.0_ new investment_app -d mysql

# investment_appディレクトリに移動
% cd investment_app

# investment_appのDBを作成
% rails db:create

ルーティングを設定

Rails.application.routes.draw do
  root "investments#index"
  resources :investments, only: [:index, :new, :create]
end

has_one

アソシエーションが1対1の時に使われる、アソシエーションを定義するためのメソッドで,親モデル側に「has_one」を、子モデル側に「belongs_to」を書きます。

モデルとマイグレーションファイルを作成

Userモデル作成

% rails g model user

住所情報のAddressモデル作成

% rails g model address

投資金額情報のInvestmentモデル作成

% rails g model investment

with_options

「with_options 〇〇 」で、複数の情報に対して共通したオプションを付けられます。

formatオプション

モデルのクラスの中でvalidatesメソッドを使う時に利用できるオプションです。
format: {with: 正規表現, message: 正規表現にマッチしなかった場合のエラーメッセージ}
で特定のカラムへ正規表現を用いた入力制限をすることができます。

正規表現 意味
/\A[ぁ-んァ-ン一-龥々]/ 全角ひらがな、全角カタカナ、漢字
/\A[ァ-ヶー-]+\z/ 全角カタカナ
/\A[a-zA-Z0-9]+\z/ 半角英数
/\A\d{3}[-]\d{4}\z/ 郵便番号(「-」を含む且つ7桁)
greater_than_or_equal_to: 〇〇 〇〇と同じか、それ以上の数値
less_than_or_equal_to: □□ □□と同じか、それ以下の数値

Userモデルにアソシエーションとバリデーションを設定

class User < ApplicationRecord
  has_one :address
  has_one :investment
  with_options presence: true do
    validates :name, format: { with: /\A[ぁ-んァ-ン一-龥々]/, message: "is invalid. Input full-width characters." }
    validates :name_reading, format: { with: /\A[ァ-ヶー-]+\z/, message: "is invalid. Input full-width katakana characters." }
    validates :nickname, format: { with: /\A[a-z0-9]+\z/i, message: "is invalid. Input half-width characters." }
  end
end

Addressモデルのアソシエーション

class Address < ApplicationRecord
  belongs_to :user
end

Investmentモデルのアソシエーション

class Investment < ApplicationRecord
  belongs_to :user
end

マイグレーションファイルを編集

User
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name,           null: false
      t.string :name_reading,   null: false
      t.string :nickname,       null: false
      t.timestamps
    end
  end
end
Address
class CreateAddresses < ActiveRecord::Migration[6.0]
  def change
    create_table :addresses do |t|
      t.string :postal_code,    default: "",  null: false
      t.integer :prefecture,                  null: false
      t.string :city,           default: ""
      t.string :house_number,   default: ""
      t.string :building_name,  default: ""
      t.references :user,                     null: false,  foreign_key: true
      t.timestamps
    end
  end
end
Investment
class CreateInvestments < ActiveRecord::Migration[6.0]
  def change
    create_table :donations do |t|
      t.integer :price,   index: true,  null: false
      t.references :user,               null: false,  foreign_key: true
      t.timestamps
    end
  end
end

ActiveHash」を使って都道府県モデルを作成し実装

gem 'active_hash'
% bundle install

## app/modelsに prefecture.rbを作成し以下のように編集

class Prefecture < ActiveHash::Base

  self.data = [
               {id: 0, name: '--'}, {id: 1, name: '北海道'}, {id: 2, name: '青森県'}, 
               {id: 3, name: '岩手県'}, {id: 4, name: '宮城県'}, {id: 5, name: '秋田県'}, 
               {id: 6, name: '山形県'}, {id: 7, name: '福島県'}, {id: 8, name: '茨城県'}, 
               {id: 9, name: '栃木県'}, {id: 10, name: '群馬県'}, {id: 11, name: '埼玉県'}, 
               {id: 12, name: '千葉県'}, {id: 13, name: '東京都'}, {id: 14, name: '神奈川県'}, 
               {id: 15, name: '新潟県'}, {id: 16, name: '富山県'}, {id: 17, name: '石川県'}, 
               {id: 18, name: '福井県'}, {id: 19, name: '山梨県'}, {id: 20, name: '長野県'}, 
               {id: 21, name: '岐阜県'}, {id: 22, name: '静岡県'}, {id: 23, name: '愛知県'}, 
               {id: 24, name: '三重県'}, {id: 25, name: '滋賀県'}, {id: 26, name: '京都府'}, 
               {id: 27, name: '大阪府'}, {id: 28, name: '兵庫県'}, {id: 29, name: '奈良県'}, 
               {id: 30, name: '和歌山県'}, {id: 31, name: '鳥取県'}, {id: 32, name: '島根県'}, 
               {id: 33, name: '岡山県'}, {id: 34, name: '広島県'}, {id: 35, name: '山口県'}, 
               {id: 36, name: '徳島県'}, {id: 37, name: '香川県'}, {id: 38, name: '愛媛県'}, 
               {id: 39, name: '高知県'}, {id: 40, name: '福岡県'}, {id: 41, name: '佐賀県'}, 
               {id: 42, name: '長崎県'}, {id: 43, name: '熊本県'}, {id: 44, name: '大分県'}, 
               {id: 45, name: '宮崎県'}, {id: 46, name: '鹿児島県'}, {id: 47, name: '沖縄県'}
              ]
end

テーブル作成

% rails db:migrate

コントローラを作成

% rails g controller investments 

アクションを定義

class InvestmentsController < ApplicationController
  def index
  end

  def new
  end

  def create
    user = User.create(user_params)
    Address.create(address_params(user))
    Investment.create(investment_params(user))
    redirect_to action: :index
  end

  private

  def user_params
    params.permit(:name, :name_reading, :nickname)
  end

  def address_params(user)
    params.permit(:postal_code, :prefecture, :city, :house_number, :building_name).merge(user_id: user.id)
  end

  def investment_params(user)
    params.permit(:price).merge(user_id: user.id)
  end
end

newのビューを作成

<%= form_with(url: investment_path, local: true) do |form| %>
  <h1>ユーザー名を入力</h1>
  <div class="field">
    <%= form.label :name, "名前(全角)" %>
    <%= form.text_field :name %>
  </div>
  <div class="field">
    <%= form.label :name_reading, "フリガナ(全角カタカナ)" %>
    <%= form.text_field :name_reading %>
  </div>
  <div class="field">
    <%= form.label :nickname, "ニックネーム(半角英数)" %>
    <%= form.text_field :nickname %>
  </div>
  <h1>住所を入力</h1>
  <div class="field">
   <%= form.label :postal_code, "郵便番号(ハイフンを含む)" %>
   <%= form.text_field :postal_code %>
 </div>
 <div class="field">
   <%= form.label :prefecture, "都道府県" %>
   <%= form.collection_select :prefecture, Prefecture.all, :id, :name, {} %>
 </div>
 <div class="field">
   <%= form.label :city, "市町村(任意)" %>
   <%= form.text_field :city %>
 </div>
  <div class="field">
    <%= form.label :house_number, "番地(任意)" %>
    <%= form.text_field :house_number %>
  </div>
  <div class="field">
    <%= form.label :building_name, "建物名(任意)" %>
    <%= form.text_field :building_name %>
  </div>

  <h1>投資金額</h1>
  <div class="field">
    <%= form.label :price, "いくら投資しますか?(1〜1000000円、半角)" %>
    <%= form.text_field :price %>
    <%= "円" %>
  </div>

  <div class="actions">
    <%= form.submit "投資する" %>
  </div>

<% end %>

この実装には欠点があります。

ユーザーが必須項目を入力し忘れてた際にバリデーションによってリダイレクトしたり、エラーメッセージが表示できない仕様になっています。また、これらの情報を編集、更新する際の処理が複雑になります。

そこであるデザインパターンを使います。

Formオブジェクトパターン

1つのフォーム送信で複数のモデルを操作したい場合や、テーブルに情報を保存しない情報に対するバリデーションをかけたい場合に使います。

ActiveModel::Model

クラスにActiveModel::Modelをincludeすると、そのクラスのインスタンスはActiveRecordを継承したクラスのインスタンスと同様にヘルパーメソッドの引数として扱えたり、バリデーションが使えるようになります。

app/models/にuser_investment.rbを作成

class UserInvestment
  include ActiveModel::Model
end
class UserInvestment
  include ActiveModel::Model
  attr_accessor :name, :name_reading, :nickname, :postal_code, :prefecture, :city, :house_number, :building_name, :price
end

attr_accessorを活用しています。今回のフォームで利用する保存したい各テーブルのカラム名全てについてゲッターとセッターを定義することで、インスタンスを生成した際にform_withの引数として利用できるようになります。

各テーブルへの保存の処理を実行する前に、バリデーションに引っかからないことをまとめてチェックするために
user.rbにあるバリデーションの記述を、Formオブジェクトへ移します。

class User < ApplicationRecord
  has_one :address
  has_one :investment

#ここの部分を切り取る
  with_options presence: true do
    validates :name, format: { with: /\A[ぁ-んァ-ン一-龥々]/, message: "is invalid. Input full-width characters." }
    validates :name_reading, format: { with: /\A[ァ-ヶー-]+\z/, message: "is invalid. Input full-width katakana characters." }
    validates :nickname, format: { with: /\A[a-z0-9]+\z/i, message: "is invalid. Input half-width characters." }
  end
#ここまで
end
UserInvestment
class UserInvestment
  include ActiveModel::Model

#ここの部分を貼り付け
  with_options presence: true do
    validates :name, format: { with: /\A[ぁ-んァ-ン一-龥々]/, message: "is invalid. Input full-width characters." }
    validates :name_reading, format: { with: /\A[ァ-ヶー-]+\z/, message: "is invalid. Input full-width katakana characters." }
    validates :nickname, format: { with: /\A[a-z0-9]+\z/i, message: "is invalid. Input half-width characters." }
  end
#ここまで

end

続いて、「住所」「投資金」のバリデーションもuser_investment.rbの中に記述します。
そしてデータをテーブルに保存する処理も追加します。

UserInvestment
class UserInvestment
  include ActiveModel::Model

  with_options presence: true do
    validates :name, format: { with: /\A[ぁ-んァ-ン一-龥々]/, message: "is invalid. Input full-width characters." }
    validates :name_reading, format: { with: /\A[ァ-ヶー-]+\z/, message: "is invalid. Input full-width katakana characters." }
    validates :nickname, format: { with: /\A[a-z0-9]+\z/i, message: "is invalid. Input half-width characters." } 
    validates :postal_code, format: { with: /\A[0-9]{3}-[0-9]{4}\z/, message: "is invalid. Include hyphen(-)" }   
    validates :price, numericality: { only_integer: true, message: "is invalid. Input half-width characters." }
  end
  validates :prefecture, numericality: { other_than: 0, message: "can't be blank" }
  validates :price, numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 1000000, message: "is out of setting range" }
end

  def save
    # ユーザーの情報を保存し、「user」という変数に代入
    user = User.create(name: name, name_reading: name_reading, nickname: nickname)
    # 住所の情報を保存
    Address.create(postal_code: postal_code, prefecture: prefecture, city: city, house_number: house_number, building_name: building_name,user_id: user.id)
    # 投資金の情報を保存
    Investment.create(price: price, user_id: user.id)
  end
end

コントローラーのアクションでFormオブジェクトのインスタンスを生成するように変更

class  InvestmentsController < ApplicationController
 def index
 end

 def new
   @user_investment = UserInvestment.new   
 end

 def create
   @user_investment = UserDonation.new(donation_params)  
    if @user_investment.valid?
      @user_investment.save
      redirect_to action: :index
    else
      render action: :new
    end
 end

 private
  # 全てのストロングパラメーターを1つに統合
 def investment_params
  params.require(:user_investment).permit(:name, :name_reading, :nickname, :postal_code, :prefecture, :city, :house_number, :building_name, :price)
 end

end

new.html.erbのform_withメソッドに、Formオブジェクトのインスタンスを引数として渡します。

<%= form_with(model: @user_investment, url: investment_path, local: true) do |form| %>
  <%= render 'error_messages', model: @user_investment %>
  <h1>ユーザー名を入力</h1>
  <div class="field">
    <%= form.label :name, "名前(全角)" %>
    <%= form.text_field :name %>
  </div>
  <div class="field">
    <%= form.label :name_reading, "フリガナ(全角カタカナ)" %>
    <%= form.text_field :name_reading %>
  </div>
  <div class="field">
    <%= form.label :nickname, "ニックネーム(半角英数)" %>
    <%= form.text_field :nickname %>
  </div>
  <h1>住所を入力</h1>
  <div class="field">
   <%= form.label :postal_code, "郵便番号(ハイフンを含む)" %>
   <%= form.text_field :postal_code %>
 </div>
 <div class="field">
   <%= form.label :prefecture, "都道府県" %>
   <%= form.collection_select :prefecture, Prefecture.all, :id, :name, {} %>
 </div>
 <div class="field">
   <%= form.label :city, "市町村(任意)" %>
   <%= form.text_field :city %>
 </div>
  <div class="field">
    <%= form.label :house_number, "番地(任意)" %>
    <%= form.text_field :house_number %>
  </div>
  <div class="field">
    <%= form.label :building_name, "建物名(任意)" %>
    <%= form.text_field :building_name %>
  </div>

  <h1>投資金額</h1>
  <div class="field">
    <%= form.label :price, "いくら投資しますか?(1〜1000000円、半角)" %>
    <%= form.text_field :price %>
    <%= "円" %>
  </div>

  <div class="actions">
    <%= form.submit "投資する" %>
  </div>

<% end %>

以上です。

19
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?