Saturday, August 13, 2016

Multi model saving, how to wrap in transaction and report errors

Leave a Comment

I have a registration form model that takes the users input during registration:

class RegForm     include ActiveModel::Model     include ActiveModel::Validations      attr_accessor :company_name, :email, :password     validates_presence_of # ...  end 

During this registration process I have multiple models that need to be created, and I am not sure how to properly display error messages and how to bubble the model error messages back to the UI.

if @reg_form.valid?    account = Account.create!(@reg_form) else  ... 

Account.create! looks like:

def self.create!(reg_form)   account = Account.create_from_reg!(reg_form)   location = location.create_from_reg!(account, reg_form)   ..   ..   account.location = location   ..   account.save!    account end 
  1. So I'm confused how to display error messages for all these models that are saving
  2. how to display or fail validation if the reg_form doesn't have the correct data for all the other models.
  3. how to ensure this is wrapped in a transaction so I don't save anything if any model fails to save during registration.

2 Answers

Answers 1

Let's start from the beginning.

We want our registration form object to have the same API as any other ActiveRecord model:

// view.html <%= form_for(@book) do |f| %> <% end %>  # controller.rb def create   @book = Book.new(book_params)    if @book.save     redirect_to @book, notice: 'Book was successfully created.'   else     render :new   end end 

To do that, we create the following object:

class RegForm   include ActiveModel::Model    attr_accessor :company_name, :email, :password    def save     # Save Location and Account here   end end 

Now, by including ActiveModel::Model, our RegForm gains a ton of functionality, including showing errors and validating attributes (yes, it's unnecessary to include ActiveModel::Validations). In this next step we add some validations:

validates :email, :password, presence: true 

And we change save so that it runs those validations:

def save   validate   # Save Location and Account here end 

validate returns true if all validations pass and false otherwise.

validate also adds errors to the @reg_form (All ActiveRecord models have an errors hash which is populated when a validation fails). This means that in the view we can do any of these:

@reg_form.errors.full_messages #=> ["Email can't be blank", "Password can't be blank"] @reg_form.errors.messages #=> { email: ["can't be blank"], password: ["can't be blank"] } @reg_form.errors[:email] #=> ["can't be blank"] @reg_form.errors.full_messages_for(:email) #=> ["Email can't be blank"] 

Meanwhile, our RegistrationsController should look something like this:

def create   @reg_form = RegForm.new(reg_params)    if @reg_form.save     redirect_to @reg_form, notice: 'Registration was successful'   else     render :new   end end 

We can clearly see that when @reg_form.save returns false, the new view is re-rendered.

Finally, we change save so that our models save calls are wrapped inside a transaction:

def save   if valid?     begin       ActiveRecord::Base.transaction do         location = Location.create!(location_params)         account = Account.create!({ location: location}.merge(account_params))       end       true     rescue ActiveRecord::StatementInvalid => e       # Handle DB exceptions not covered by validations.       # e.message and e.cause.message can help you figure out what happened     end   end end 

Points worthy of note:

  1. create! is used instead of create. The transaction is only rolled back if an exception is raised (which methods with a bang usually do).

  2. validate is just an alias for valid?. To avoid all that indentation you could instead use a guard at the top of the save method:

    return if invalid? 
  3. You can turn a DB exception (like an email uniqueness constraint) into an error by doing something like:

    rescue ActiveRecord::RecordNotUnique   errors.add(:email, :taken) end 

Answers 2

I think transactions and error handling will help you solve your problem.

def save_or_rescue   ActiveRecord::Base.transaction do     account = Account.create_from_reg!(reg_form)     location = location.create_from_reg!(account, reg_form)     ...   end  rescue ActiveRecord::RecordInvalid => exception   puts exception end 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment