How to Formオブジェクト
概要
入力フォームなどのデータを、一括で複数のテーブルに保存できる「Formオブジェクト」についてアウトプットします。
Formオブジェクトとは
1回のフォーム送信で複数のモデルを更新することができる仕組みです。Formオブジェクトは、ActiveModel::Modelというモジュールを読み込むことで利用することができます。
ActiveModel::Modelをincludeすると、ActiveRecordと同様に「form_for」や「render」などのヘルパーメソッドを使えるようになります。
バリデーションの設定に関しても、ActiveModel::Modelを設定したモデルに複数のテーブルに対する制約を記述できるため、可読性も向上します。
使い方
今回は擬似的に寄付アプリを制作します。
ユーザー、住所、寄付金の情報を一度で全て保存するように設定していきます。
事前準備
まずはアプリケーションの土台を構築します。
% rails _6.0.0_ new donation_app -d mysql % cd donation_app % rails db:create
ルーティングを設定します。
Rails.application.routes.draw do root "donations#index" resources :donations, only: [:index, :new, :create] end
コントローラーを作成します。
% rails g controller donations index
モデルを作成します。今回はユーザー、住所、寄付金の3つです。
% rails g model user
% rails g model address
% rails g model donation
アソシエーションを組みます。
class User < ApplicationRecord has_one :address has_one :donation end
class Address < ApplicationRecord
belongs_to :user
end
|
class Donation < 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 CreateDonations < 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
|
% rails db:migrate
|
これで事前準備は完了です。
Formオブジェクトを用いた実装
まず、Formオブジェクトを記述するディレクトリ、ファイルを作成します。
app/forms/user_donation.rbとなるようにformsディレクトリとuser_donation.rbファイルを作成します。
user_donation.rbを編集します。
class UserDonation include ActiveModel::Model attr_accessor :name, :name_reading, :nickname, :postal_code, :prefecture, :city, :house_number, :building_name, :price 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, format: {with: /\A[0-9]+\z/, 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"} 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) # 寄付金の情報を保存 Donation.create(price: price, user_id: user.id) end end
構造は以下のようになっています。
include ActiveModel::Model
ActiveModel::Modelをincludeし、form_forの受け取りやバリデーション設定を可能にします。
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) # 寄付金の情報を保存 Donation.create(price: price, user_id: user.id) end
user = User.create...の部分は、AdressとDonationのカラムにuser_idを設定しているため、user_id: user.idとして保存できるようにするために変数化しています。
コントローラーを編集します。
class DonationsController < ApplicationController
def index
end
def new
@donation = UserDonation.new
end
def create
@donation = UserDonation.new(donation_params)
if @donation.valid?
@donation.save # バリデーションをクリアした時
return redirect_to root_path
else
render "new" # バリデーションに弾かれた時
end
end
|
@donation.valid?とすることで、バリデーションを手動で起動させています。この記述がないと、バリデーションが効かず、空欄のままでも保存できてしまいます。
最後に、ビューを作成します。
#app/view/donations/new.html.erb
<%= form_with(model: @donation, url: donations_path, local: true) do |form| %> <%= render 'error_messages', donation: @donation %>
<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> <div class="actions"> <%= form.submit "寄付する" %> </div> <% end %>
<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, {include_blank: "---"} %> </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 %>
都道府県の選択フォームにはActive::Hashを利用しています。
Active::Hashについては別の記事でまとめています。
エラーメッセージ
# app/views/donations/_error_messages.html.erb
<% if donation.errors.any? %> <div class="error-alert"> <ul> <% donation.errors.full_messages.each do |message| %> <li class="error-message"> <%= message %> </li> <% end %> </ul> </div> <% end %>
モデル単体テスト
gemを導入します。
group :development, :test do # 中略 gem 'rspec-rails' gem 'factory_bot_rails' end
.rspecを編集します。
--require spec_helper
--format documentation
テストファイルを生成します。
FactoryBotを作成します。spec/factories/user_donations.rbとなるようにディレクトリとファイルを作成します。
user_donations.rbを編集します。
FactoryBot.define do factory :user_donation do name { '鈴木' } name_reading { 'スズキ' } nickname { 'suzuki' } postal_code { '123-4567' } prefecture { 1 } city { '東京都' } house_number { '1-1' } building_name { '東京ハイツ' } price { 2000 } end end
テストを記述します。
require 'rails_helper' RSpec.describe UserDonation, type: :model do describe '寄付情報の保存' do before do @user_donation = FactoryBot.build(:user_donation) end it 'すべての値が正しく入力されていれば保存できること' do expect(@user_donation).to be_valid end it 'nameが空だと保存できないこと' do @user_donation.name = nil @user_donation.valid? expect(@user_donation.errors.full_messages).to include("Name can't be blank") end it 'nameが全角日本語でないと保存できないこと' do @user_donation.name = "suzuki" @user_donation.valid? expect(@user_donation.errors.full_messages).to include("Name is invalid. Input full-width characters.") end it 'name_readingが空だと保存できないこと' do @user_donation.name_reading = nil @user_donation.valid? expect(@user_donation.errors.full_messages).to include("Name reading can't be blank") end it 'name_readingが全角日本語でないと保存できないこと' do @user_donation.name_reading = "スズキ" @user_donation.valid? expect(@user_donation.errors.full_messages).to include("Name reading is invalid. Input full-width katakana characters.") end it 'nicknameが空だと保存できないこと' do @user_donation.nickname = nil @user_donation.valid? expect(@user_donation.errors.full_messages).to include("Nickname can't be blank") end it 'nicknameが半角でないと保存できないこと' do @user_donation.nickname = "すずき" @user_donation.valid? expect(@user_donation.errors.full_messages).to include("Nickname is invalid. Input half-width characters.") end it 'postal_codeが空だと保存できないこと' do @user_donation.postal_code = nil @user_donation.valid? expect(@user_donation.errors.full_messages).to include("Postal code can't be blank") end it 'postal_codeが半角のハイフンを含んだ正しい形式でないと保存できないこと' do @user_donation.postal_code = '1234567' @user_donation.valid? expect(@user_donation.errors.full_messages).to include("Postal code is invalid. Include hyphen(-)") end it 'prefectureを選択していないと保存できないこと' do @user_donation.prefecture = 0 @user_donation.valid? expect(@user_donation.errors.full_messages).to include("Prefecture can't be blank") end it 'cityは空でも保存できること' do @user_donation.city = nil expect(@user_donation).to be_valid end it 'house_numberは空でも保存できること' do @user_donation.house_number = nil expect(@user_donation).to be_valid end it 'building_nameは空でも保存できること' do @user_donation.building_name = nil expect(@user_donation).to be_valid end it 'priceが空だと保存できないこと' do @user_donation.price = nil @user_donation.valid? expect(@user_donation.errors.full_messages).to include("Price can't be blank") end it 'priceが全角数字だと保存できないこと' do @user_donation.price = '2000' @user_donation.valid? expect(@user_donation.errors.full_messages).to include("Price is invalid. Input half-width characters.") end it 'priceが1円未満では保存できないこと' do @user_donation.price = 0 @user_donation.valid? expect(@user_donation.errors.full_messages).to include("Price is out of setting range") end it 'priceが1,000,000円を超過すると保存できないこと' do @user_donation.price = 1000001 @user_donation.valid? expect(@user_donation.errors.full_messages).to include("Price is out of setting range") end end end
テストコードを実行します。
% bundle exec rspec spec/models/user_donation_spec.rb
テストが無事パスすれば、Formオブジェクトによる一括保存が実装できました。