Capture all errors in form and ensure atomicity -rails - ruby-on-rails

I want to capture all the errors in a form and also ensure that the operation is atomic that is the form goes through if there are no errors else it just captures the error and no other correct fields are saved.
ActiveRecord::Base.transaction do
#var_types.each_with_index do |var,index|
begin
var.save!
puts "saving"
rescue
puts "rescued"
end
end
I am using the above block, although this captures all the errors in but atomicity is not guarenteed. Any way to correct this?
EDIT:
Example
An example would be say something like a form where a user has to enter multiple fields and multiple fields may not conform to the db rules, so i expect all the errors to be shown at once so the save as to keep happening to get all the errors but at the same time ensure that no correct changes get saved if there is even one error.

You have to catch the exception outside the transaction.
The transactions are rollbacked when an exception goes out the transaction block
begin
ActiveRecord::Base.transaction do
#var_types.each_with_index do |var,index|
var.save!
puts "saving"
end
end
rescue
puts "rescued"
end
UPDATE after reading your comment:
ActiveRecord::Base.transaction do
raise ActiveRecord::Rollback unless #var_types.map(&:save).all? #passing here a block like { |res| res == true } is redundant.
redirect_to some_index_path, notice: "Everything saved"
end
render action: 'edit' # something didn't pass the validation, re-render the view
Things to note here:
If you raise ActiveRecord::Rollback inside a transaction block, you don't need a rescue for it (read the docs at http://api.rubyonrails.org/classes/ActiveRecord/Rollback.html)
Someone might say that you should not drive the flow based in exception-throwing, if you don't feel comfortable with it, you can do something like this:
all_saved = false # need to define this var outside, or it will a block-level variable, visible only in the block
ActiveRecord::Base.transaction do
all_saved = #var_types.map(&:save).all?
raise ActiveRecord::Rollback unless all_saved
end
if all_saved
redirect_to some_index_path, notice: "Everything saved"
else
render action: 'edit' # something didn't pass the validation, re-render the view
end

Related

Is this an error-handling anti-pattern?

A colleague recently told me this is an anti-pattern (record is an ActiveRecord):
begin
record.save!
do_something_else
rescue => e
puts "Unable to save"
end
...and I should do this instead:
if record.save
do_something_else
else
puts "Unable to save"
end
His argument is that I'm using an exception for flow control (which I agree is bad) but I believe this is a typical error-handling pattern.
Thoughts?
This is an antipattern, because there are better ways to check the validity of a record than running save! (which raises an error if it's not valid).
This is probably what your colleague was getting at:
if record.save
do_something_else
else
puts record.errors.full_messages
end
You see, there is just no benefit here of using the error as control flow because there are less roundabout ways to do it.
You can also run the validations independently of the save attempt. The errors.full_messages array (of strings) is populated when record.valid? gets called. record.save calls record.valid? internally.
if record.valid?
record.save # or save!; by this point you can assume the record is valid
else
puts record.errors.full_messages
end

self.errors[:base] << inside an ActiveRecord transaction block

I want to update #some_instance unless the user does not meet some criteria. I put the criteria check and the #some_instance.save! in a transaction block so that if either of them fail, no transaction is made. This works well, but I am having trouble returning the correct error message. If the user does not meet the criteria, I want to return the reason why, OR if the #some_instance doesn't save, I want to return that error.
My code:
#some_controller.rb
begin
Some_Class.transaction do
return render json: { error: #user.errors }, status: :payment_required,
location: new_payment_path unless #user.meets_criteria
#some_instance.save!
end
rescue ActiveRecord::RecordInvalid => exception
render :json => { :error => exception.messages }, status: :unprocessable_entity
rescue => error
# handle some other exception
end
#User.rb
def meets_criteria
self.errors[:base] << "Does not meet criteria"
return false
end
The problem I'm facing is this: When the meets_criteria method returns false, I expect the return render json line to execute. Instead it catches an error in "rescue => error".
The return render json is never executed.
UPDATE:
#Gen suggested using a before_action instead of calling the meets_criteria in the transaction do block. I think this is a much better implementation, however I'm still curious why the return render is never called. Is it because ActiveRecord raises an error? If so shouldn't that be caught in the RecordInvalid exception?
#TheJKFever, I am not sure that your code supposed to jump to any rescue block (well, and I actually confirmed by running it).
Your some_validation method returns false, and therefore "unless #user.some_validation" evaluates to true, and the render is executed with the following log output:
Completed 402 Payment Required in 128ms
{"error":{"base":["Some error message"]}}
You can refer to ActiveRecord::RecordInvalid API for details about RecordInvalid. Namely, "Raised by save! and create! when the record is invalid".
So, your "rescue ActiveRecord::RecordInvalid => exception" is supposed to handle exceptions in the "#some_instance.save!" statement and not in your custom validation.
In your validation you don't actually have the code that raises the ActiveRecord::RecordInvalid exception and probably fails with another error, which is easy to check by outputing it in details.
In order to use some_validation with "self.errors[:base] <<" properly first your need to add the following statement to your user model:
validate :some_validation
In this case, if you call "#user.save!" instead of "#some_instance.save!", you would fall into that "rescue ActiveRecord::RecordInvalid => exception" block.
PS: #TheJKFever, I saw one of your comments below and wanted to clarify something. A validation has a well defined purpose to validate a model before saving, and what you need then is not a model validation. What you actually need is a before_action on your controller that will check that your user is ready to be used in such and such action (consider it as controller validation). And yes, you probably will need some method on your user model to do that check.
Updated (after question update)
#TheJKFever, as I mentioned earlier, when I implemented your code I was able to execute "return render json: { error: #user.errors }...". So, if it fails for you, it must be due to some exception during meets_criteria call, but it is not RecordInvalid exception. Since you wrapped meets_criteria into transaction, it means that it probably does some database changes that you want to rollback if #some_instance.save! was unsuccessful. It would be a good idea to wrap your meets_criteria with the same rescue blocks too and find out. Do you create! or save! something in meets_criteria? If so, then it also can throw RecordInvalid exception. The problem is that in your code RecordInvalid can be thrown by meets_criteria and by #some_instance.save!, and there is no way to see in the code which one. In any case, if you wrap your meets_criteria with rescue, you will be able to send your render errors request from it. And if you decide to go with before_action filter then you will have to move whole your transaction into it (assuming that it requires the integrity of the data).
The point is that ActiveRecord::RecordInvalid exception will only be thrown in case of save! or create! failure due to invalid data. It might not be the case in your code, and some other exception is thrown, and you end up in "rescue => error" block.
You are adding error to #user model and handling exception raised on #some_instance. Try #user.errors.messages[:base]
Custom validations aren't usually invoked via a public instance method. They're usually written as a private method and invoked by ActiveRecord during valid? if you register them in your model with, e.g. validate :some_validation.
In your case, the User model would need the following:
validate :some_validation
…
private
def some_validation
errors.add :base, "Some error message" if some_error_happens?
end
Then, in the controller, you would do:
Some_Class.transaction do
return render json: { error: #user.errors }, status: :payment_required,
location: new_payment_path if #user.valid?
#some_instance.save!
end

atomic transaction not working - ruby rails

So I am trying to achieve atomicitiy in while doing my saves (which are essentially updates on each rows).
params[:player_types].each do |p_type_params|
if p_type_params[:id]
player = #player_types.find(p_type_params[:id])
player.assign_attributes(p_type_params)
#player_types << player
end
end
ActiveRecord::Base.transaction do
#player_types.each do |player_type|
if player_type.save
"DO Something.."
else
"DO something else.."
errors = true
end
end
end
Inspite of saving withing the transaction block, I can see partial saves also i.e. one of the rows get updated and the erroneous one does not (obviously) where as I would have wanted the updated row to be rolled back since there was atleast one row that could not be updated due to an error. Is my interpretation of transaction block correct in this case? How do I achieve an atomic save in my case?
EDIT: The model validates for uniqueness of one of the columns, which would be the reason for failing to update in the Database at this point.
You need to raise an error inside your transaction block to abort the transaction; setting errors doesn't impact the transaction.
For instance:
ActiveRecord::Base.transaction do
#player_types.each do |player_type|
if player_type.save
"DO Something.."
else
"DO something else.."
raise "save failed!"
end
end
end
The more conventional way of doing this, of course, is to use save! which raises an exception for you when it fails:
ActiveRecord::Base.transaction do
#player_types.each do |player_type|
player_type.save!
end
end
If you really need to "DO Something" on failure (besides aborting the transaction), you'll have to use the first method.

How to avoid a 'frozen hash' error in Rails without using an empty begin-rescue-end loop?

I have the following method which is called via Ajax:
def decrement
#cart = current_cart
#line_item = LineItem.find(params[:id])
#line_item.quantity -= 1
if #line_item.quantity == 0
#line_item.destroy
end
begin #line_item.update_attributes(params[:line_item])
rescue
end
respond_to do |format|
format.js {#current_item = #line_item}
end
end
Originally, I had #line_item.update_attributes(params[:line_item]) in an if statement but that would return a "frozen hash" runtime error instead of merely returning false. I think that my begin-rescue-end loop is absolutely ridiculous, though it works, and I am here to ask the proper way to handle this situation in Rails.
Doing a blind rescue and then discarding the exception is really not the way to go about solving problems. Anything could be going wrong in there and you're ignoring it which is an extremely dangerous practice.
I think part of the problem here is you're potentially destroying a record and then modifying it after it has been destroyed, which is an invalid operation. You may want to specify this a different way:
if #line_item.quantity > 0
#line_item.update_attributes(params[:line_item])
else
#line_item.destroy
end
If you're still having "frozen hash" errors, you probably need to investigate what's being frozen and why instead of simply disregarding the error and continuing on as if nothing's the matter.
You may want to use update_attributes! and rescue from ActiveRecord::RecordInvalid if there's something that will preclude this from being saved properly. Right now you're ignoring the result of update_attributes, successful or not.

show error messages from two models when updating Rails

I am updating the attributes of two models from one form.
User.transaction do
begin
#user.update_attributes!(params[:user])
#board.update_attributes!(params[:board])
rescue ActiveRecord::RecordInvalid
end
end
When the #user.update_attributes produces an error the transaction is stopped and the error message is shown in the view.
However I want to try to update both #user and #board and get the error messages for both so that the user can correct all their mistakes one time.
How can I do this?
Thank you very much in advance.
All you need to do is not use the bang (!) version of update_attributes. Try:
User.transaction do
if #user.update_attributes(params[:user]) && #board.update_attributes(params[:board])
...do stuff...
else
render ... (go back to the action the user got here from)
end
end
Then in your view code put an errors block using a conditional like if #user.errors.any?.
The bang version of update_attribtues and most activerecord methods means it will raise an error if it fails, the regular version just returns false and doesn't save, so you can use that whenever it's ok to fail you just need to take action based on success/failure. Only use the bang version when it's not ok to fail, or when you want to show a 500 screen or something for some reason...
You could do the following:
User.transaction do
#user.update_attributes(params[:user])
#board.update_attributes(params[:board])
raise ActiveRecord::Rollback unless #user.valid? && #board.valid?
end
This will ensure that both update attribute methods run so that you can get error messages for both objects. If either object is invalid though no changes will be persisted to the database.
I think there is some mistake in the method name #user.update_attributes!(params[:user]) it should be like this #user.update_attributes(params[:user]) or once you need cross check your params values whether it is correct or not else your code looks correct. You can have a help with this update_attributes

Resources