Anand
Anand

Reputation: 3760

How to validate multiple models in a single transaction?

In my rails (4.1.6) app, I have a contact model that has_one :address, :email

I construct a contact and related address and email in a single form using fields_for:

views/contacts/new.html.erb

<%= form_for @contact, ... %>
  ...
  <%= fields_for :address do |address_fields| %>
      <%= address_fields.text_field :street, ... %>
      <%= address_fields.text_field :city, ... %>
      ...
  <% end %>

  <%= fields_for :email do |email_fields| %>
      <%= email_fields.text_field :display_name, ... %>
      <%= email_fields.text_field :mail_id, ... %>
  <% end %>
  ...

<% end %>

I want email to be required, while address is optional. In other words, if email is not provided, none of the 3 models should be created, but if only email is provided, the email and contact must be created.

One way that does work is to validate the params manually in the contacts_controller#create before constructing anything, and flash[:error] and return without saving if email is not specified, or save it if all is well:

contacts_controller.rb

def create
  @contact = Contact.new
  if(params_email_valid? params)
     @contact.save!
     @email = Email.create(...) 
     @email.save!
     ...
  else
     flash[:error] = 'Email must be specified to save a contact'
     redirect_to :root
  end
end

private:
  def params_email_valid? params
    !(params[:email][:display_name].blank? || params[:email][:mail_id].blank?)
  end

Another way that may work is to drop down to SQL and validate everything through direct SQL calls in a transaction.

However, both of these are not 'the rails way', since validations belong in the models. So, I am trying to use some combination of validates_presence_of, validates_associated and custom validators to validate this scenario. The problem here is that model level validation of associated models requires either self to be already saved in the database, or the associated model to be already saved in the database. Is there a way to validate all these models in a single transaction?

Upvotes: 0

Views: 228

Answers (1)

argentum47
argentum47

Reputation: 2382

Considering you have appropriate validations in the models:

class Contact <
  has_many :addresses
  has_many :emails
  #add
  accepts_nested_attributes_for :addresses, :emails #you can add some validations here to like reject_all if blank? see the docs
end

class Address <
  belongs_to :contact
end

class Email <
  belongs_to :contact
end

In your CompaniesController

def new
  @contact = Contact.new
  @contact.addresses.new
  @contact.emails.new
end

def create
  @contact = Contact.new(contact_params)
  if @contact.save
    #redirect add flash
  else
   #add flash
   #render action: new
end

protected
  def contact_params
   #permit(#contact_fields, address_attributes: [#address_fields], email_attributes: [#email_fields])
  end

And you would like to modify your form like this

<%= form_for @contact, ... do|f| %>
  ...
  <%= f.fields_for :address do |address_fields| %>
      <%= address_fields.text_field :street, ... %>
      <%= address_fields.text_field :city, ... %>
      ...
  <% end %>

  <%= f.fields_for :email do |email_fields| %>
      <%= email_fields.text_field :display_name, ... %>
      <%= email_fields.text_field :mail_id, ... %>
  <% end %>
  ...

<% end %>

So accepts_nested_attributes helps you validate the child as well as the parent and adds [child]_attributes getters and setters, So normally in your form what was contact[email][display_name] will become contact[email_attributes][display_name]

Upvotes: 1

Related Questions