if one object fails all objects rollback - ruby-on-rails

begin
ActiveRecord::Base.transaction do
installations.each do |installation|
id = installation.id
installation = current_user.installations.find_by(:id=> id)
#ticket = installation.tickets.new(ticket_params)
unless #ticket.save
raise ActiveRecord::Rollback
end
end
end
redirect_to '/tickets', notice: "done"
rescue ActiveRecord::Rollback
render action: "new", notice: "problem" and return
end
Imagine that we have two tickets, the first ticket is valid and the second is invalid. In this code the first ticket will be saved. But I want to rollback all the tickets when one fail.
How can I do that ?

You could build a collection of #ticket.save's return values, call .all? on them, and if you get a false raise the rollback:
ActiveRecord::Base.transaction do
installations.each do |installation|
tickets_saved = []
id = installation.id
installation = current_user.installations.find_by(:id=> id)
ticket = installation.tickets.new(ticket_params)
tickets_saved << ticket.save
end
unless tickets_saved.all?
raise ActiveRecord::Rollback
end
end

Related

How to display model error messages within notice in ruby

I did a date validation in model which should display a message on page, but for some reason it isn't. I need this message to be shown in notice on error. Currently I just see the message in controller (Order not registered) but I need to see the reason (Delivery date should be from now). How can I do that?
model
def delivery_date_from_now_only
if self.delivery_date.present? && self.delivery_date <= Date.today
self.errors.add(:delivery_date, messsage: 'Delivery date should be from now')
end
end
controller
def create
#order = Order.new(order_params)
#order.user = current_user
if #order.save
flash[:notice] = 'Order registered successfully'
return redirect_to #order
end
#warehouses = Warehouse.all.order(:name)
#suppliers = Supplier.all.order(:brand_name)
#users = User.all.order(:name)
flash.now[:alert] = 'Order not registered'
render :new, status: 422
end
if you call #order.valid? it will validate your model an also populate errors for that object. that means #order.errors.messages will be a hash with { :delivery_date => ["Delivery date should be from now"]} etc (other errors will be in there as well) which u can use to do some flashes.

Display Error Message from Private Method (Rails 6)

I have a method, create, which calls a private method get_title:
def create
#article = #user.articles.new(article_params)
#article.title = get_title(#article.link)
if #article.save
redirect_to user_path, success: "Article Saved."
else
flash.now[:danger] = "Failed to save article."
end
end
The get_title method works so long as there are no errors in the get request:
def get_title(url)
request = HTTParty.get(url)
document = Nokogiri::HTML(request.body)
title = document.at_css "title"
return title.text
end
I can catch the error with a begin/rescue before the Nokogiri call:
begin
HTTParty.get(url)
rescue HTTParty::Error
...
rescue StandardError
...
else
request = HTTParty.get(url)
end
I don't know how to stop the method and return the error to the create method.
How do I return an error from the rescue statement to the create method and display that error message in a flash?
What you probably want to do is "harden" this method so it doesn't explode unless something really unusual happens:
class TitleFetchError < StandardError
end
def get_title(url)
request = HTTParty.get(url)
Nokogiri::HTML(request.body).at_css('title').text
rescue HTTParty::Error => error
raise TitleFetchError, error.to_s
rescue StandardError => error
raise TitleFetchError, error.to_s
rescue ... (some other type of error, e.g. Nokogiri parse errors)
end
Where in my opinion that should actually be a method on the #article model, like #article.fetch_title which would do the same thing, using its own link property.
One thing to note is this pattern for writing controller actions helps simplify things:
def create
#article = #user.articles.new(article_params)
#article.title = get_title(#article.link)
#article.save!
redirect_to user_path, success: "Article Saved."
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved
redirect_to user_path, danger: "Failed to save article."
rescue TitleFetchError => error
redirect_to user_path, danger: "Failed to save article. Error: #{error}"
end
Where you have a nice clean "normal" path, and then your exceptions are handled on the side if/when that becomes necessary. This keeps the two flows separated and avoids additional nesting from the if.

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.

Using a transaction to roll back saves

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 :)

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

Resources