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
- So I'm confused how to display error messages for all these models that are saving
- how to display or fail validation if the reg_form doesn't have the correct data for all the other models.
- 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:
create!
is used instead ofcreate
. The transaction is only rolled back if an exception is raised (which methods with a bang usually do).validate
is just an alias forvalid?
. To avoid all that indentation you could instead use a guard at the top of thesave
method:return if invalid?
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
0 comments:
Post a Comment