改良中...複数のモデルを同時に保存する

前回思い浮かんだ疑問や問題に、できるところから自分なりに対応してみた。

入力されている明細のみ保存する

  • 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]-->


これで、現在の見た目は以下のようになる。

  • 明細が一行も入力されていない、というエラーが発生している。

  • エラーが検証されるのは、入力のある行だけ。