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

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

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.

@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.
  1. 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?

Why don’t you wish to use update_attributes?

Dan

Vish

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

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

Hi --

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

(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

Hi Vish,

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,

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

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

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

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