has_manyな関連で、1アクションで複数のモデルを同時に保存するには?
前回までのhas_oneな関連に限定した、複数のモデルを同時に保存するコード
まず、前回のコードは、あまりにも手抜きだったので、もう少しscaffoldに習った形式に書き直してみた。この後has_manyな関連に対応させることを目指して、以下のようにしてみた。
-
-
- 既にRails2.0もリリースされ、Rails1.1.6時代の書き方では動作しない状況(end_form_tagなど)になりつつありますが、自分の頭が追いついていないので、ここではRails1.1.6仕様なコードを書いて、Rails1.2.6環境で動作確認しています。
-
- まずはモデルを生成して...
script/generate model slip script/generate model journal
- マイグレーションの設定...
# db/migrate/001_create_slips.rb class CreateSlips < ActiveRecord::Migration def self.up create_table :slips do |t| t.column :number, :integer t.column :executed_on, :string t.column :total_yen, :integer end end def self.down drop_table :slips end end # 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 end end def self.down drop_table :journals end end
- 上記設定をしてから、rake migrateを実行した。
- その後、scaffoldの実行する。
script/generate scaffold slip script/generate scaffold journal
- 以下、ビューのコーディング
<%# ビュー: app/views/slips/new.rhtml %> <h1>New slip</h1> <%= start_form_tag :action => 'create' %> <%= render :partial=>'form' %> <%= submit_tag %> <%= end_form_tag %> <%= link_to 'Back', :action => 'list' %>
<%# ビュー: app/views/slips/edit.rhtml %> <h1>Editing slip</h1> <%= start_form_tag :action => 'update', :id => @slip %> <%= render :partial=>'form' %> <%= submit_tag %> <%= end_form_tag %> <%= link_to 'Show', :action => 'show', :id => @journal %> | <%= link_to 'Back', :action => 'list' %>
<%# ビュー: app/views/slips/_form.rhtml %> <%= error_messages_for 'slip' %> <!--[form:slip]--> <p><label for="slip_number">伝票No.</label><br/> <%= text_field 'slip', 'number' %></p> <p><label for="slip_executed_on">実行日</label><br/> <%= text_field 'slip', 'executed_on' %></p> <p><label for="slip_total_yen">合計金額</label><br/> <%= text_field 'slip', 'total_yen' %></p> <table class="journal"> <%= render :partial=>'journals/header' %> <%= render :partial=>'journals/form' %> </table> <!--[eoform:slip]-->
<%# ビュー: app/views/journals/_header.rhtml %> <!--[header:journal]--> <tr> <th>摘要</th> <th>金額</th> </tr> <!--[eoheader:journal]-->
<%# ビュー: app/views/journals/_form.rhtml %> <tr> <td colspan="2"> <%= error_messages_for 'journal' %> </td> </tr> <!--[form:journal]--> <tr> <td> <%= text_field 'journal', 'comment' %> </td> <td> <%= text_field 'journal', 'yen' %> </td> </tr> <!--[eoform:journal]-->
- 以下、コントローラーのコーディング
# コントローラー: app/controllers/slips_controller.rb class SlipsController < ApplicationController ...(途中省略)... def new @slip = Slip.new @journal = Journal.new end def create @slip = Slip.new(params[:slip]) @journal = Journal.new(params[:journal]) Slip.transaction do @journal.slip = @slip @slip.save! @journal.save! flash[:notice] = '新規作成しました。' redirect_to :action => 'list' end rescue @journal.valid? render :action => 'new' end def edit @slip = Slip.find(params[:id]) @journal = @slip.journal end def update @slip = Slip.find(params[:id]) @journal = @slip.journal Slip.transaction do @journal.slip = @slip @slip.save! @journal.save! flash[:notice] = '変更を保存しました。' redirect_to :action => 'show', :id => @slip end rescue @journal.valid? render :action => 'edit' end ...(途中省略)... end
- 以下、モデルのコーディング
# モデル: app/models/slip.rb class Slip < ActiveRecord::Base has_one :journals, :dependent=>:destroy validates_presence_of :number, :executed_on, :total_yen end # モデル: app/models/journal.rb class Journal < ActiveRecord::Base belongs_to :slip validates_presence_of :comment, :yen end
/* スタイルシート: public/stylesheets/scaffold.css */ .journal { background-color: #eee; } .journal #errorExplanation { margin-top: 20px; margin-bottom: 0; }
以上のコードを元に、has_manyな関連に対応してみる。
has_manyな関連に対応した、複数のモデルを同時に保存するコード
- Slipモデルの関連をhas_manyに変更して...
# モデル: app/models/slip.rb class Slip < ActiveRecord::Base has_many :journals, :dependent=>:destroy validates_presence_of :number, :executed_on, :total_yen end
- Journalモデルのインスタンスは配列として扱うことにする。(ここでは新規作成のデフォルトは4明細に設定)
# コントローラー: app/controllers/slips_controller.rb class SlipsController < ApplicationController ...(途中省略)... def new @slip = Slip.new @journals = (1..4).map {Journal.new} end ...(途中省略)...
- 伝票のrender :partial=>'journals/form'には、:collection=>@journalsオプションを追加して、繰り返し描画する。
<%# ビュー: app/views/slips/_form.rhtml %> <%= error_messages_for 'slip' %> <!--[form:slip]--> <p><label for="slip_number">伝票No.</label><br/> <%= text_field 'slip', 'number' %></p> <p><label for="slip_executed_on">実行日</label><br/> <%= text_field 'slip', 'executed_on' %></p> <p><label for="slip_total_yen">合計金額</label><br/> <%= text_field 'slip', 'total_yen' %></p> <table class="journal"> <%= render :partial=>'journals/header' %> <%= render :partial=>'journals/form', :collection=>@journals %> </table> <!--[eoform:slip]-->
- 明細行のtext_fieldには:indexオプションを追加して、パラメーターを階層化されたハッシュとして渡す。
- auto_indexを利用した<%= text_field 'journal[]', 'comment' %>のような書き方では、新規作成する時にidがnilのため、うまく動かなかった。
<%# ビュー: app/views/journals/_form.rhtml %> <% @journal = form %> <tr> <td colspan="2"> <%= error_messages_for 'journal' %> </td> </tr> <!--[form:journal]--> <tr> <td> <%= text_field "journal", 'comment', :index=>@journal.index %> </td> <td> <%= text_field 'journal', 'yen', :index=>@journal.index %> </td> </tr> <!--[eoform:journal]-->
- Journalモデルの中で、@journal.indexは以下のように定義しておいた。
- 最初はTime.now.to_fとして、現在時刻を小数に変換したキーを利用していたが、タイミングによっては、CPUの処理が高速で同じキーがいくつも生成されてしまう*1ことがあったので、最終的にTime.now.hashに落ち着いた。
class Journal < ActiveRecord::Base belongs_to :slip validates_presence_of :comment, :yen # Time.now.hashで、現在の時刻をキーにした重複しない整数値が取得できると思っている。 # この例では必要ないが、数値がマイナスだと、auto_complete_fieldがうまく機能しなかったので、絶対値(abs)を取ることにした。 def initialize(*attr) super @index = Time.now.hash.abs end def index id || @index end end
- 以上の設定をしておくと、何も入力しない状態でsubmitボタンを押した時、params[:journal]は以下のようなハッシュ値として取得できる。
"{"948528444"=>{"yen"=>"", "comment"=>""}, "948528790"=>{"yen"=>"", "comment"=>""}, "948528880"=>{"yen"=>"", "comment"=>""}, "948528850"=>{"yen"=>"", "comment"=>""}}
- 取得したパラメーターは、コントローラーで以下のように処理する。
# コントローラー: app/controllers/slips_controller.rb class SlipsController < ApplicationController ...(途中省略)... # params[:journal].map {}ブロックのindex_attrは、以下のように変化していく。 # ["948528444", {"yen"=>"", "comment"=>""}] # ["948528790", {"yen"=>"", "comment"=>""}] # ["948528880", {"yen"=>"", "comment"=>""}] # ["948528850", {"yen"=>"", "comment"=>""}] # # つまり、[key, value]のペア配列として取得できるのだ。(valueの中身は属性のハッシュ) # よって、index_attr[1]で{"yen"=>"", "comment"=>""}が取得できることになる。 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!} flash[:notice] = '新規作成しました。' redirect_to :action => 'list' end rescue @journals.each {|journal| journal.valid?} render :action => 'new' end
以上で、とりあえずの目標である、has_manyな関連に対応して1アクションで2モデルを保存すること、は可能になった!(ホントに基本的な部分だけ)しかし、可能は可能になったが、果たしてこのような処理方法で問題ないのだろうか?業務システムでは似たような処理は少なからず必要になると思うが、皆さん、どのような方法で実現しているのだろう...。
今、感じている疑問や問題
- 複数のインスタンスを一度に保存しようとすると、インスタンス同士を識別するキーが必要になると思うが、新規作成のidがnilの状況で、どのように処理するのが効率的なのだろうか?
- 今回の例ではTime.now.hashを使ったが、カウンタを1ずつ増やす方法、ランダムな値を使う方法等も考えられる。
- ちなみに現状では、もしもjournalのレコード数が10億に近づくと、idの値とダブってしまう可能性もある。(英字と組み合わせたキーにしておく方が良いかもしれない。)
- 現状では、4明細固定の保存しかできない。入力がない行(現状では未入力エラーになるが)は保存すべきではない。(...かといって、全く明細がない伝票でも登録できてしまうのは問題だ。)
- Ruby1.8までは、ハッシュは順序を保存することができない。しかし、伝票の明細行の順序は重要だ。現状では、明細行の順序が崩れてしまう可能性がある。
- 伝票を入力していると、明細行の挿入・削除・コピーもやりたくなるのが人情。(acts_as_listを利用したいが、データベース保存前の状態で利用できるのだろうか?)
- さらに明細行(Journal)が、has_manyなアイテムを保持している場合はどうしようか?その場合のparamsのキー表現はどうなる?
- slips_controllerなのにjournalをコントロールしているような感じ。このような役割分担で、果たして良いのだろうか?
...んー、考え始めると疑問が尽きない...。