Best practice for multiple models in one form

Hopefully someone can point me in the right direction here. I have a
couple of models that are associated like this:

class Event < ActiveRecord::Base
has_many :event_dates
has_many :event_attendees, :as => :attending
has_many :event_groups
belongs_to :event_type
belongs_to :event_qualification
belongs_to :event_location
belongs_to :event_facilitator
has_many :payment_installments
has_many :event_letters

Now, the goal is to have all of these details available in one form.
I have done all this, with no problems, using AJAX to add additional
fields, etc for the has_many relationships.

My question is, what is the Rails best practice when saving this
information to the database? The reason I ask is in relation to
validations, error reporting to the user, and redirecting back to the
form keeping all their edits in place. I know Rails does this
automagically for one single model/form relationship but when dealing
with multiple models, what is the best way to achieve this?

An example of a problem with payment_installments. There is a
boolean field in the parent event record, then there is a has_many
relationship to payment_installments where they are all defined in
the child table. OK, what if the user selects the boolean checkbox
for payment installments, but doesn’t enter any installment
information. Technically the event model is intact, which is what
the form is primarily based on, as it’s just a true/false. But since
there has been no child record information entered, it should fail
validation, but the validation needs to be checked across two
models. Any ideas?

Thanks,
Dan

Validations can be chained, although handling complex dependency rules
might
be a pain. See this:

http://ar.rubyonrails.com/classes/ActiveRecord/Validations/ClassMethods.html#M000304

and there are also many examples in the Book.

Vish

Excellent. I’ve used validates_associated :payment_installments
and it seems to work great, for creating new child records that is.

I find that I still have an issue that maybe someone can help with.
In relation to updating a record, the main model, event is being
saved using update_attributes, but this doesn’t save the child
records in one hit. The one hit save is the goal so that then I can
cascade my validations and fire an error back to the user if there is
a problem.

The current solution is:
@installment = PaymentInstallment.find_by_id(installment_form[:id])
@installment.name = installment_form[“name”]
@installment.save

@event.update_attributes(params[:event])

What would be great is if I could do this:
@event.payment_installment(installment_form[:id]).name =
installment_form[“name”]

@event.save

There are two problems here that I can’t figure out:

  1. Is it possible to load a specific child record by id from the
    parent? That way the child records would be “loaded” and then the
    save on the object would save the loaded child in one save hit.
  2. Would I also have to update all the event attributes manually
    before the parent record save? or is there a shortcut (like
    @event.save(params[:event]) to just update the attributes from the
    form in one hit without using update_attributes?

Dan

On 24/09/2006, at 8:34 PM, Vishnu G. wrote:

This will correctly find the payment_installments.
@event.payment_installments.find (installment_form[:id]).name =
installment_form[“name”]
@event.save

Doing this didn’t save the payment_installments records at all,
either using save or update_attributes. Anything I’m missing? Or,
any other ideas to try?

Chaining on save works, but you’ve got to remember that only
has_many relationships are auto-saved (as soon as they are
assigned). In your case, I don’t think that’s a problem.

Why don’t you wish to use update_attributes?

Since update_attributes accepts params[:event] my thought was that
the update_attributes method only updates the @event object with the
parameters passed to the method. It was an assumption, whether
correct or not I don’t know.

Dan

Sorry, I’m missing something here too.

What happens with this code?
payment_intstallment =
@event.payment_installments.find(installment_form[:id])
payment_installment.name = installment_form[“name”]
payment_installment.save

?

I haven’t done any cascading saves like this (this should’ve worked),
but
having many(2 or 3) different saves like the one above doesn’t seem to
be
too much trouble :slight_smile:

Vish

On 25/09/2006, at 11:58 PM, Dan H. wrote:

On 24/09/2006, at 8:34 PM, Vishnu G. wrote:

This will correctly find the payment_installments.
@event.payment_installments.find (installment_form[:id]).name =
installment_form[“name”]
@event.save

Doing this didn’t save the payment_installments records at all,
either using save or update_attributes. Anything I’m missing? Or,
any other ideas to try?

After a bit of investigation, I found that if I use this:
@event.payment_installments.find(installment_form[:id].to_i).name
= installment_form[“name”]
and then if I pull this out again on the next line:
logger.info @event.payment_installments.find(installment_form
[:id].to_i).name

I get the database value, and not the value I assigned to it on the
previous line of code. So I guess there must be some other way to
assign the form data ready for a save to the database, this way seems
“read only”.

Any ideas?

Dan

On 9/24/06, Dan H. [email protected] wrote:

The current solution is:
@installment = PaymentInstallment.find_by_id(installment_form[:id])
@installment.name = installment_form[“name”]
@installment.save

@event.update_attributes(params[:event])

What would be great is if I could do this:
@event.payment_installment(installment_form[:id]).name
= installment_form[“name”]

This will correctly find the payment_installments.
@event.payment_installments.find(installment_form[:id]).name =
installment_form[“name”]
@event.save

Chaining on save works, but you’ve got to remember that only has_many
relationships are auto-saved (as soon as they are assigned). In your
case, I
don’t think that’s a problem.

Why don’t you wish to use update_attributes?

Dan

Vish

Hi Vish,

On 26/09/2006, at 12:09 AM, Vishnu G. wrote:

Sorry, I’m missing something here too.

What happens with this code?
payment_intstallment = @event.payment_installments.find
(installment_form[:id])
payment_installment.name = installment_form[“name”]
payment_installment.save

?

This works.

I guess my preference would be to save the record along with the
parent event record. The reason why is that if I have to catch a
validation error and render the edit action, before rendering the
edit action I need to setup all the variables that are needed to
build the form again. It seems to work against DRY to be saving,
catching errors, if an error then initialising the variables needed
for the form, and rendering the edit action in multiple locations in
my code.

I realise that Rails prefers to have one form per model, but surely
there is a nice way to have multiple models, one form, one save
point, one error catching on save, one render to the edit action if
there is an error?

Dan

Hi David,

On 26/09/2006, at 12:31 AM, [email protected] wrote:

You can use update_attribute:

@event.pi.find(iform[:id]).update_attribute(:name, iform[“name”])

That will change the in-memory object and also save the change to the
database.

Cool. That’s good stuff, thank you.

Did you see my question about catching validation/save errors for
both the parent and child records and how to handle both nicely?

Dan

I’m in a similar situation: I’m also wondering about transactions. Can
you make associated saves like this happen within a DB transaction?

Hi –

On Tue, 26 Sep 2006, Dan H. wrote:

either using save or update_attributes. Anything I’m missing? Or,
any other ideas to try?

After a bit of investigation, I found that if I use this:
@event.payment_installments.find(installment_form[:id].to_i).name
= installment_form[“name”]

(There must be a shorter way to write that… :slight_smile:

and then if I pull this out again on the next line:
logger.info @event.payment_installments.find(installment_form
[:id].to_i).name

I get the database value, and not the value I assigned to it on the
previous line of code. So I guess there must be some other way to
assign the form data ready for a save to the database, this way seems
“read only”.

It’s not read only; it’s just that it’s writing only to the in-memory
object.

Any ideas?

You can use update_attribute:

@event.pi.find(iform[:id]).update_attribute(:name, iform[“name”])

That will change the in-memory object and also save the change to the
database.

David


David A. Black | [email protected]
Author of “Ruby for Rails” [1] | Ruby/Rails training & consultancy [3]
DABlog (DAB’s Weblog) [2] | Co-director, Ruby Central, Inc. [4]
[1] Ruby for Rails | [3] http://www.rubypowerandlight.com
[2] http://dablog.rubypal.com | [4] http://www.rubycentral.org

On 26/09/2006, at 3:19 AM, Dan H. wrote:

This is a long one, but in summary the last problem to overcome is
that on @event.save the payment_installments are being validated, but
then are never updated (before_update is never called).

In the interest of sharing and in case anyone comes across this in
Google and needs to know how to fix it… This is the solution that
I came up with:

To pull the data from the form for the children without saving I used:
j = 0
while j < @event.payment_installments.length
if @event.payment_installments[j].id == installment_form[:id].to_i
@event.payment_installments[j].name = installment_form[“name”]
@event.payment_installments[j].value = installment_form[“value”]
@event.payment_installments[j].due_date_as_text =
installment_form[“due_date”]
end
j += 1
end

To save the parent model with the children:
begin
j = 0
error = false
while j < @event.payment_installments.length
if !@event.payment_installments[j].save
@event.payment_installments[j].errors.each { |k,m|
@event.errors.add(k,"on Payment Installment line #{i+1}, " +m) }
error = true
end
j += 1
end
raise “error” if error
if !@event.update_attributes(params[:event])
raise “error”
end
rescue
… (variables set to redisplay form)
render :action => ‘edit’
else
flash[:notice] = ‘Event was successfully updated.’
redirect_to :action => ‘show’, :id => @event
end

There could well be some optimisations that could be made and there
may be some bugs, but it works pretty well for now. It doesn’t give
me nice Rails-y fuzzy feelings, but it works.

Dan

On 9/25/06, werdnativ [email protected] wrote:

I’m in a similar situation: I’m also wondering about transactions. Can
you make associated saves like this happen within a DB transaction?

AnyModel.transaction do

update db. raise an exception to rollback

end

Isak

This is a long one, but in summary the last problem to overcome is
that on @event.save the payment_installments are being validated, but
then are never updated (before_update is never called).

Why would the update be aborted? valid? returns true and if I check
the values of the model object at after_validation_on_update the
values look all good and are the same as what was entered on the form.

Any ideas would be most helpful…

Here are the full results of my investigation:

There seems to be a few examples on various places on the net that
cover handling validation errors from multiple models when creating
a new record. Unfortunately, there are exactly zero examples on how
to handle validation errors on multiple models when updating
existing records.

One thing I came up with is a mashup of this:

errors-in-rails/
and this:
http://www.edwardthomson.com/blog/2006/04/
rails_validations_with_multipl.html

@pi = @event.payment_installments.find(installment_form[:id].to_i)
@pi.name = installment_form[“name”]
@pi.value = installment_form[“value”]
@pi.due_date_as_text = installment_form[“due_date”]
if !@pi.save
@pi.errors.each { |k,m| @event.errors.add(k,"on Payment
Installment line #{i+1}, " +m) }
end

@event.attributes=(params[:event])
if @event.errors.empty? && @event.save
flash[:notice] = ‘Event was successfully updated.’
redirect_to :action => ‘show’, :id => @event
else
… set various stuff here ready to reshow form
render :action => ‘edit’
end

This pretty much works, a nice error is printed at the top of the
screen.

Of course, nothing is perfect, and it doesn’t preserve the users
edited fields when it reshows the fields. I believe this is because
the @event object does not contain the updated values for the
payment_installments children. In the above code, I take a copy of
the object and save that to the database, I do not edit the
@event.payment_installments array of objects as I can’t seem to
figure out how to do this.

It seems that this is still an issue, I still require the ability to
edit the values held in memory under event.payment_installments.find
(id).name for example. In looking at the ActiveRecord doco I should
be able to utilise the write_attribute method to do this, but it has
no effect. Ah, hang on. The find method always seems to pull from
the database and ignore the values in memory. But, I may be way off
track here. My suspicion is that find is pulling a copy of the
object from the database and updating that. This looks as though
using find with write_attribute maybe a useless combination, I’m
updating a copy instead of the actual attribute in memory.

Although, I seem to remember coding one attempting iterating through
the @event.payment_installments array (not using find) and then
using .name = installment_form[“name”] but that had no effect. I
just tested that again, code is (please forgive my non-ruby-esq code):

j = 0
while j < @event.payment_installments.length
if @event.payment_installments[j].id == installment_form[:id].to_i
@event.payment_installments[j].name = installment_form[“name”]
@event.payment_installments[j].value = installment_form[“value”]
@event.payment_installments[j].due_date_as_text =
installment_form[“due_date”]
end
j += 1
end

@event.attributes=(params[:event])
if @event.save
flash[:notice] = ‘Event was successfully updated.’
redirect_to :action => ‘show’, :id => @event
else
… set various stuff here ready to reshow form
render :action => ‘edit’
end

Believe it or not, this almost works! The validation picks up
errors for the payment_installment model, it also shows the user
entered values on the form redraw, however, for some reason, when it
validates correctly, the data doesn’t change! AAAAHHHHH!
Frustration setting in…

What on earth could be happening here? Why does it fail validation
and looks fine (on the form), but when it passes validation, the data
doesn’t change? In the log, I see it selecting data from the table,
but then it never updates anything… Why would it attempt to
validate something it was never going to save? I have a validation
method in my model, and it prints to the log, this is appearing in my
log, so with good data, the validation is being called as if it’s
going to save.

Dan