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
マイグレーションファイルを編集
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
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
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
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の中に記述します。
そしてデータをテーブルに保存する処理も追加します。
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 %>
以上です。