Saving multiple models, how to wrap a transaction and report errors

I have a registration form model that introduces users 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 several models that need to be created, and I'm not sure how to display error messages correctly and how to bubble model error messages back into the user interface.

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

Account.create! as follows:

 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 
  • So, I am confused how to display error messages for all of these models that save
  • how to display or refuse verification if reg_form does not have the correct data for all other models.
  • how to ensure that this is wrapped in a transaction, so I don’t save anything if any model cannot save on registration.
+8
validation ruby-on-rails
source share
2 answers

Let it 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 this, 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, having ActiveModel::Model , our RegForm gets a lot of functionality, including error display and attribute checking (yes, there is no need to include ActiveModel::Validations ). In the next step, we will add some checks:

 validates :email, :password, presence: true 

And we change save to perform these checks:

 def save validate # Save Location and Account here end 

validate returns true if all checks pass and false otherwise.

validate also adds errors to @reg_form (all ActiveRecord models have an errors hash that populates when validation fails). This means that in the view we can do any of them:

 @reg_form.errors.messages #=> { email: ["can't be blank"], password: ["can't be blank"] } @reg_form.errors.full_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 re-displays.

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

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

Points worthy of attention:

  • create! used instead of create . transaction only goes back if an exception occurs (which methods are usually executed using bang).

  • validate just an alias for valid? . To avoid all this indentation, we could instead use the security feature at the top of the save method:

     return if invalid? 
  • We can turn the database exception ( as limiting email uniqueness ) into an error by doing something like:

     rescue ActiveRecord::RecordNotUnique errors.add(:email, :taken) end 
  • We can add an error not directly related to the attribute using the :base symbol:

     errors.add(:base, 'Company and Email do not match') 
+3
source share

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 
0
source share

All Articles