Using a transaction to roll back saves - ruby-on-rails

My registration form is complex and fragile. I am trying to use transaction, but it doesn't appear to work the way I think it does.
My intention is to roll back all the saves if something fails down the line. Am I doing this incorrectly?
if #little_class_schedule.valid?
User.transaction do
if #user.save
#little_class.user_id = #user.id
if #little_class.save
if #location.save
if little_class_schedule_form_params["schedule_type"].to_i == 2 || #little_class_schedule.save
if #little_class_session_validation.valid?
sessions.each do |s|
if s.save
next
else
raise ActiveRecord::Rollback
render 'class_account_registration/new'
end
end
ApprovalMailer.request_approval(#user, #little_class).deliver
redirect_to dashboard_path, notice: 'Success!'
else
raise ActiveRecord::Rollback
render 'class_account_registration/new'
end
else
raise ActiveRecord::Rollback
render 'class_account_registration/new'
end
else
raise ActiveRecord::Rollback
render 'class_account_registration/new'
end
else
raise ActiveRecord::Rollback
render 'class_account_registration/new'
end
else
render 'class_account_registration/new'
end
end
else
render 'class_account_registration/new'
end

I would highly recommend you to get a structure in your code and keep it DRY!
Since I don't know what you want to do, I just improved your code. It still isn't the best solution, but since I don't know what you really want to do, I can't change the logic.
successful = false
if #little_class_schedule.valid?
User.transaction do
if #user.save
#little_class.user_id = #user.id
if #little_class.save && #location.save
if little_class_schedule_form_params["schedule_type"].to_i == 2 || #little_class_schedule.save
if #little_class_session_validation.valid?
sessions.each do |s|
if s.save
next
else
raise ActiveRecord::Rollback
end
end
successful = true
end
end
end
end
unless successful
raise ActiveRecord::Rollback
end
end
end
if successful
ApprovalMailer.request_approval(#user, #little_class).deliver_now
redirect_to dashboard_path, notice: 'Success!'
else
render 'class_account_registration/new' unless successful
end
I haven't tested this code so it could have bugs in it. Please try it out and give us feedback if it works or if not what errors you have.
Hope this helps!
Happy coding :)

Related

Best way to do a rescue block

Is this the correct way to do a rescue in a block?
Also, is it the shortest?
def rescue_definition
begin
user.begin_rescue
rescue Example::ParameterValidationError => e
redirect_to :back, error: e.message_to_purchaser
rescue Example::ProcessingError => e
redirect_to :back, error: e.message_to_purchaser
rescue Example::Error
redirect_to :back, error: e.message_to_purchaser
else
if user
flash['success'] = 'OK'
else
flash['error'] = 'NO'
end
end
redirect_to :back
end
The idea to use begin, rescue, ensure is to put the methods than can generate an error in the begin block, then you can handle the errors in one o more rescue blocks and finally you can put an optional ensure block for sentences that you want to execute on both success or fail scenarios for example release resources.
When you want all the method be handled by begin, rescue, ensure blocks, then begin keyword is optional, you can use or not the keyword, since you are asking for the shortest option, you will need to do some minor changes to your code:
def rescue_definition
user.begin_rescue
# and any other stuff you want to execute
if user
flash['success'] = 'OK'
else
flash['error'] = 'NO'
end
redirect_to :back
rescue Example::ParameterValidationError => e
redirect_to :back, error: e.message_to_purchaser
rescue Example::ProcessingError => e
redirect_to :back, error: e.message_to_purchaser
rescue Example::Error
redirect_to :back, error: e.message_to_purchaser
end
If you want it even shorter you can rescue multiple exception types in one rescue block:
def rescue_definition
user.begin_rescue
# and any other stuff you want to execute
if user
flash['success'] = 'OK'
else
flash['error'] = 'NO'
end
redirect_to :back
rescue Example::ParameterValidationError, Example::ProcessingError, Example::Error => e
redirect_to :back, error: e.message_to_purchaser
end
Aguar's post handles the current implementation. If you handle all the errors the same way then you can have them inherit from a custom class and just catch them all with 1 rescue call.
def stuff
user.stuff
if user
flash['success'] = 'Ya'
else
flash['error'] = 'Nah'
end
redirect_to ....
rescue CustomClassError => e
redirect_to :back, error: e.message_to_purchaser
rescue NonCustomErrors => e
#handle it
end

Rolling back Model-changes from action

I have the action, in where I assign groups to the members that apply. Basically, I just get a list of emails, from the view in a form, and then I have the action to catch it:
What I'm wondering is, that if I can rollback the changes that's been made already, if say the second member doesn't exist, or has a group already, how can I rollback these?
def group_create
#group = Group.new
params[:member].each { |m|
v = Volunteer.find_by_email(m[1])
raise "#{m[1]} doesn't exist" unless v.present?
raise "#{v.email} is already in a group" if v.group.present?
v.group = #group
v.save!
}
#group.save
rescue => error
flash[:error] = "Error: #{error}"
ensure
respond_to do |format|
unless flash[:error].present?
flash[:notice] = 'Group Application succeded.'
flash[:joined] = true
format.html { redirect_to apply_group_path }
else
flash.discard
format.html { render :group }
end
end
end
What I've thought about already, was move the v.save and #group.save to the end, and make another loop of params[:member].each...., but that would be quite a waste of ressources, to do the find_by_email-method twice as many times, as needed.
I suggest move your non-controller logic into model and wrap it in ActiveRecord::Base.transaction:
ActiveRecord::Base.transaction do
#group = Group.new
params[:member].each { |m|
v = Volunteer.find_by_email(m[1])
raise "#{m[1]} doesn't exist" unless v.present?
raise "#{v.email} is already in a group" if v.group.present?
v.group = #group
v.save!
}
#group.save
end
Use transactions as a protective wrapper around SQL statements to ensure changes to the database only occur when all actions succeed together.

Returning a 404 if a user doesn't exist

Why would this not generate a 404 response?
def index
if find_user
#documents = #client.documents
respond_to do |format|
format.html
format.atom { render layout: false }
end
else
flash[:error] = "#{params[:client_code]} is not a client."
render 'error', status: '404'
end
end
def find_user
#client = User.find_by_client_code(params[:client_code]) if valid_user?
end
def valid_user?
User.all.each.map(&:client_code).include?(params[:client_code])
end
Like, if the code is incorrect it should return a 404, right? And not an exception? Can't quite get it to work.
EDIT: sorry, here's the error:
An ActionView::MissingTemplate occurred in share#index:
* Parameters : {"controller"=>"share", "action"=>"index", "client_code"=>"ampNDHEDD", "format"=>"atom"}
If you don't use the valid_user? or the find_user methods elsewhere, they can be removed and you could do the following
def index
#client = User.find_by_client_code(params[:client_code]) # returns nil or a record
if #client
#documents = #client.documents
respond_to do |format|
format.html
format.atom { render layout: false }
end
else
flash[:error] = "#{params[:client_code]} is not a client."
render status: 404
end
end
However, your previous comment states you're getting a template error which indicates that you may not have an index.atom template available to render.
do this
def index
if find_user
#documents = #client.documents
respond_to do |format|
format.html
format.atom { render layout: false }
end
else
flash[:error] = "#{params[:client_code]} is not a client."
raise ActionController::RoutingError.new('Not Found')
end
end
def find_user
#client = User.find_by_client_code(params[:client_code]) if valid_user?
end
def valid_user?
User.where(client_code: params[:client_code]).present?
// User.all.each.map(&:client_code).include?(params[:client_code]) // this is terrible (Read my comment )
end
First, your valid_user? method is a really bad idea - it loads the entire user database just to see if the code is present... which is the same result as what User.find_by_client_code does, but without loading every record! I'd just nuc the method and the if clause. If there is no matching record, it should return nil, which should take the else path and render the error.
As for why it's not rendering the error... I'm not sure if the atom format has anything to do with it, but when code doesn't branch the way I expect, I always put a Rails.logger.debug ... before the branch I have an issue with, and/or put a bad method in the branch it's supposed to take. That helps narrow it down. :D

Render failing to render correct template in rescue_from ActiveRecord::Rollback method

I'm building the checkout page for an e-commerce site, and I have a fairly long transaction that creates a new User model and a new Order model. I wrapped the creation of these models in a transaction so that if validation for one fails, the other isn't hanging around in the database. Here's the trimmed-down code in my OrdersController:
rescue_from ActiveRecord::Rollback, with: :render_new
def render_new
render action: 'new'
end
ActiveRecord::Base.transaction do
#user = User.new params[:user]
unless #user.save
raise ActiveRecord::Rollback
end
//More stuff
...
#order = Order.new params[:order]
...
unless #order.save
raise ActiveRecord::Rollback
end
end
The error I'm seeing is this:
Missing template orders/create, application/create with {:locale=>[:en], :formats=>[:html], :handlers=>[:erb, :builder, :coffee]}
I'm confused as to why its trying to render the templates orders/create and application/create instead of rendering orders/new.
Is there a better way to force the transaction to fail so that the rollback will occur?
I think the intention is a bit clearer when wrapping the transaction in a begin/rescue block.
def create
begin
ActiveRecord::Base.transaction do
#user = User.new params[:user]
unless #user.save
raise ActiveRecord::Rollback
end
//More stuff
...
#order = Order.new params[:order]
...
unless #order.save
raise ActiveRecord::Rollback
end
end
rescue ActiveRecord::Rollback
render action: "new" and return
end
end
You need to return in the create method, otherwise it's execution will continue to the end of the method and Rails default render will occur (in this case it means attempting to find a create.___ template).
If you don't like the begin/rescue block you can just add an and return to the raise lines
raise ActiveRecord::Rollback and return
Above answer is correct but some modification is required for rendering action.
Do it like this:-
def create
is_project_saved = false
is_proposal_saved = false
ActiveRecord::Base.transaction do
is_project_saved = #project.save
is_proposal_saved = #proposal.save
if is_project_saved && is_proposal_saved
# Do nothing
else
raise ActiveRecord::Rollback
end
end
if is_project_saved && is_proposal_saved
# You can add more nested conditions as per you need.
flash[:notice] = "Proposal Created Successfully."
redirect_to project_show_path(:job_id => #project.job_id)
else
render :new
end
end
ActiveRecord::Rollback will not be caught by resque. So it needs to be done outside transaction block.
You can also use save_point using :requires_new => true in nested ActiveRecord::Base.transaction.
You need to raise the ActiveRecord::Rollback and manage the render/redirect as you desire. As #WasimKhan said, the ActiveRecord::Rollback will not be caught by rescue.
def create
#user = User.new params[:user]
ActiveRecord::Base.transaction do
if #user.save
#order = Order.new params[:order]
if #order.save
redirect_to :index
else
raise ActiveRecord::Rollback
end
else
render :new
end
end
render :new if #user.id.nil?
end

How to show errors for two objects and don't save unless both are valid

I have a form for two object User Board
Here is my controller:
def create
#board = Board.new(params[:board])
#user = User.new(params[:user])
respond_to do |format|
if (#user.save and #board.save)
format.js {redirect_to some_path}
else
format.js {render :action => "new" }
end
end
end
I don't want to save either one unless both are valid. And I want to show the error messages for both at one time on the form.
I have tried all types of combinations of '&&' '&' 'and' but they don't give me the result I want. They show the errors of one object while saving the other.
How can I do this properly?
&& doesn't work like && in Linux.
You have two alternatives. You can check whether the records are valid?, then perform the save.
def create
#board = Board.new(params[:board])
#user = User.new(params[:user])
respond_to do |format|
if #user.valid? && #board.valid?
#user.save!
#board.save!
format.js { redirect_to some_path }
else
# do something with #errors.
# You check the existence of errors with
# #user.errors.any?
# #board.errors.any?
# and you access the errors with
# #user.errors
# #board.errors
format.js { render :action => "new" }
end
end
end
or if your database supports transactions, use transactions.
def create
#board = Board.new(params[:board])
#user = User.new(params[:user])
respond_to do |format|
begin
transaction { #user.save! && #board.save! }
format.js { redirect_to some_path }
rescue ActiveRecord::RecordInvalid
format.js { redirect_to some_path }
end
end
end
Personally, I would check for valid?.
If you look in the source code of the save method of ActiveRecord you see:
def save(options={})
perform_validations(options) ? super : false
end
What you want to do, is running the perform_validations manually before calling save. For this, you can use the valid? method from ActiveResource. You can find the documentation here:
http://api.rubyonrails.org/classes/ActiveRecord/Validations.html#method-i-valid-3F

Resources