改良中...複数のモデルを同時に保存する
前回思い浮かんだ疑問や問題に、できるところから自分なりに対応してみた。
入力されている明細のみ保存する
- Journalモデルに入力チェックのメソッドinput?を追加してみた。
- ついでに、インデックス値の先頭に「j」を付加して、idと重複する可能性を無くしておいた。
# モデル: app/models/journal.rb class Journal < ActiveRecord::Base belongs_to :slip validates_presence_of :comment, :yen def initialize(*attr) super # 以下の形式でユニークなインデックス値を設定する。 # 例: j948696358 @index = "j#{Time.now.hash.abs}" end def index id || @index end # 入力があるか、ないか? def input? # データベースのデータ型が、stringまたはtextの場合、nilは""(空文字)に変換される。 # そのため、commentだけでは常に""、つまりtrueと判定され、未入力チェックができない。 !comment.blank? || yen end end
- コントローラーでは入力のある明細のみ、保存(save)や検証(validate)を行う。
# コントローラー: app/controllers/slips_controller.rb class SlipsController < ApplicationController ...(途中省略)... def new @slip = Slip.new @journals = (1..4).map {Journal.new} end def create @slip = Slip.new(params[:slip]) @journals = params[:journal].map {|index_attr| Journal.new(index_attr[1])} Slip.transaction do @journals.each {|journal| journal.slip = @slip} @slip.save! @journals.each {|journal| journal.save! if journal.input?} ## 追記if journal.input? flash[:notice] = '新規作成しました。' redirect_to :action => 'list' end rescue @journals.each {|journal| journal.valid? if journal.input?} ## 追記if journal.input? render :action => 'new' end ...(途中省略)...
- しかし、これでは明細数0件の伝票が出来上がってしまう...。
少なくとも1件以上明細が存在することを検証する
- Slipモデルにmake_journalsメソッドを定義して、明細(Journal)はこのメソッドを通してを作成することにした。(データベース保存前に、Slipモデルが管理している明細(Journal)を知るため)
- validateメソッドの中で、1件も明細が無い場合は検証エラーになるようにしておく。
- ついでに、明細の合計と、伝票の合計もチェックするようにしておいた。
- errors.add_to_baseによって、error_message_forで表示するエラー警告のみ追加している。
# モデル: app/models/slip.rb class Slip < ActiveRecord::Base has_many :journals, :dependent=>:destroy validates_presence_of :number, :executed_on, :total_yen def validate # 明細の入力チェック unless @editing_journals.inject(false) {|result, journal| result || journal.input?} errors.add_to_base("明細が一行も入力されていません。") end # 合計金額のチェック # nilが含まれると数値として取り扱えないので、to_iで数値に変換しておく必要あり unless total_yen.to_i == @editing_journals.sum {|journal| journal.yen.to_i} errors.add_to_base("明細の合計と、合計金額が一致していません。") end end def make_journals(params_journal) @editing_journals = params_journal.map {|index_attr| Journal.new(index_attr[1])} @editing_journals.each {|journal| journal.slip = self} @editing_journals end end
- 上記に伴って、コントローラーも以下のように修正しておく。
# コントローラー: app/controllers/slips_controller.rb class SlipsController < ApplicationController ...(途中省略)... def new @slip = Slip.new @journals = (1..4).map {Journal.new} end def create @slip = Slip.new(params[:slip]) ## @journals = params[:journal].map {|index_attr| Journal.new(index_attr[1])} ## 削除 @journals = @slip.make_journals(params[:journal]) ## 追加 Slip.transaction do ## @journals.each {|journal| journal.slip = @slip} ## 削除 @slip.save! @journals.each {|journal| journal.save! if journal.input?} flash[:notice] = '新規作成しました。' redirect_to :action => 'list' end rescue @journals.each {|journal| journal.valid? if journal.input?} render :action => 'new' end ...(途中省略)...
編集中の明細の順序を保存する
- パラメーターとして受け取るハッシュは順序を保持していないため、現状では編集中に明細の順序が勝手に入れ替わってしまう状況が発生する...。順序を保持するためにpositionフィールドを追加してみた。
# マイグレーション: db/migrate/002_create_journals.rb class CreateJournals < ActiveRecord::Migration def self.up create_table :journals do |t| t.column :comment, :string t.column :yen, :integer t.column :slip_id, :integer t.column :position, :integer end end def self.down drop_table :journals end end
- Slipモデルのmake_journalsの最終行で、sort_byによって、position列で並び替える。
# モデル: app/models/slip.rb class Slip < ActiveRecord::Base has_many :journals, :order=>:position, :dependent=>:destroy ## 追記:order=>:position validates_presence_of :number, :executed_on, :total_yen def validate # 明細の入力チェック unless @editing_journals.inject(false) {|result, journal| result || journal.input?} errors.add_to_base("明細が一行も入力されていません。") end # 合計金額のチェック # nilが含まれると数値として取り扱えないので、to_iで数値に変換しておく必要あり unless total_yen.to_i == @editing_journals.sum {|journal| journal.yen.to_i} errors.add_to_base("明細の合計と、合計金額が一致していません。") end end def make_journals(params_journal) @editing_journals = params_journal.map {|index_attr| Journal.new(index_attr[1])} @editing_journals.each {|journal| journal.slip = self} @editing_journals = @editing_journals.sort_by {|journal| journal.position} ## 追記 = @editing_journals.sort_by {...} end end
- 上記に伴って、明細のビューでは1から順に行番号を表示するように変更した。
<%# ビュー: app/views/journals/_header.rhtml %> <!--[header:journal]--> <tr> <th>No.</th> <th>摘要</th> <th>金額</th> </tr> <!--[eoheader:journal]-->
<%# ビュー: app/views/journals/_form.rhtml %> <% @journal = form %> <% @journal_count += 1 rescue @journal_count = 1 %> <tr> <td colspan="3"> <%= error_messages_for 'journal' %> </td> </tr> <!--[form:journal]--> <tr> <td> <%= text_field "journal", 'position', :index=>@journal.index, :value=>@journal_count, :size=>4, :readonly=>true %> </td> <td> <%= text_field "journal", 'comment', :index=>@journal.index %> </td> <td> <%= text_field 'journal', 'yen', :index=>@journal.index %> </td> </tr> <!--[eoform:journal]-->
これで、現在の見た目は以下のようになる。
- 明細が一行も入力されていない、というエラーが発生している。
- エラーが検証されるのは、入力のある行だけ。