I have a simple exception handler as follows
begin
# code
rescue Exception
# Write to database
raise
end
The write to database is rolled back if raise is called. Is what I'm attempting to do possible?
Edit
Write to database does the following
Question.create(
notification_id: 1,
text: 'test'
)
Very simple.
You cannot rollback unless you use a transactions like follows
raise ActiveRecord::Rollback, "Call tech support!"
In your case, May be you have validated attributes in model (Question) and it get failed.
You can check errors like :
questions=Questions.new(...)
errors = questions.errors.full_messages if questions.invalid?
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
Because I have unique key for the model,
If a new record trying to create with a existed ** unique key**
It should be fail.
But I want the behavior be that
If (Create file) Then (Update the exsited record).
begin
Model.create(DATA)
rescue Exception => e
Model.update(DATA)
end
Is there any side-effect for my code ?
You can use the find_or_initialize_by method:
Model.find_or_initialize_by(key: :my_key) do |model|
model.attr_1 = "New attribute 1."
model.attr_2 = "New attribute 2."
model.save
end
Also notice that it's generally not considered a good practice to rescue Exception. You should probably be rescuing ActiveRecord::RecordInvalid or ActiveRecord::ActiveRecordError instead, or at the very least StandardError.
The answer depends a bit on your Rails version. In Rails 4 you would do something like this:
ModelName.where(attributes_hash).first_or_create
see Documentation: http://apidock.com/rails/ActiveRecord/Relation/first_or_create
I have a situation where a user can create multiple records at the same time, so my controller action looks like this:
if Unit.create multiple_unit_params.values
redirect_to units_path
else
render :new
end
The validation can fail for any one of the records. So how am I supposed to render errors in the view if I don't know what record validation failed since there are multiple records?
One way to conquer this is to use the create! method that raises exceptions. The exception object contains information on the model that failed.
The pattern I tend to use looks like this:
def create
#unit = Unit.new(multiple_unit_params.values)
#unit.save!
redirect_to(units_path)
rescue ActiveRecord::RecordInvalid => e
# ... Deal with exception
end
If you're creating multiple records you may want to encapsulate that in a transaction so you don't end up with some created, some uncreated. It'll be an all-or-none thing. Exceptions automatically unwind transactions.
I want to check that a user is included in a group. In an attempt to do this, I have the following statement:
user_id = current_user.id
unless (group.user.find(user_id))
redirect_to different_path
end
Looking at the docs, "A failure to find the requested object raises a ResourceNotFound
exception if the find was called with an id." How do I write the unless statement so that it operates properly?
If you defined a relation between group and users, you can call current_user.group.present? This will return false if there is no related object.
You can handle the redirect as part of Exception handling
redirect_to path if group.user.find(user_id)
rescue ResourceNotFound
redirect_to differenct_path
end
alternatively and probably a better way would be to build your logic around
user.groups.include?(group)