I have the following code block:
unless User.exist?(...)
begin
user = User.new(...)
# Set more attributes of user
user.save!
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e
# Check if that user was created in the meantime
user = User.exists?(...)
raise e if user.nil?
end
end
The reason is, as you can probably guess, that multiple processes might call this method at the same time to create the user (if it doesn't already exist), so while the first one enters the block and starts initializing a new user, setting the attributes and finally calling save!, the user might already be created.
In that case I want to check again if the user exists and only raise the exception if it still doesn't (= if no other process has created it in the meantime).
The problem is, that regularly ActiveRecord::RecordInvalid exceptions are raised from the save! and not rescued from the rescue block.
Any ideas?
EDIT:
Alright, this is weird. I must be missing something. I refactored the code according to Simone's tip to look like this:
unless User.find_by_email(...).present?
# Here we know the user does not exist yet
user = User.new(...)
# Set more attributes of user
unless user.save
# User could not be saved for some reason, maybe created by another request?
raise StandardError, "Could not create user for order #{self.id}." unless User.exists?(:email => ...)
end
end
Now I got the following exception:
ActiveRecord::RecordNotUnique: Mysql::DupEntry: Duplicate entry 'foo#bar.com' for key 'index_users_on_email': INSERT INTO `users` ...
thrown in the line where it says 'unless user.save'.
How can that be? Rails thinks the user can be created because the email is unique but then the Mysql unique index prevents the insert? How likely is that? And how can it be avoided?
In this case, you might want to use a migration to create an unique index on a user table key, so that the database will raise an error.
Also, don't forget to add a validates_uniqueness_of validation in your user model.
The validation doesn't always prevent duplicate data (there's a really minimum chance that two concurrent requests are written at the same millisecond).
If you use the validates_uniqueness_of in combination with an index, you don't need all that code.
unless User.exist?(...)
begin
user = User.new(...)
# Set more attributes of user
user.save!
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e
# Check if that user was created in the meantime
user = User.exists?(...)
raise e if user.nil?
end
end
becomes
user = User.new(...)
# Set more attributes of user
if user.save
# saved
else
# user.errors will return
# the list of errors
end
Rails validations aren't able to detect race conditions in the database; the solution we use is to also add database constraints.
Here's our brief page of links about this: Rails ActiveRecord Validations: validates_uniqueness_of races
Related
I have a method called invite! in my user model which will generate a unique token for a user and email them an invitation to join / sign up for my app.
The simplified implementation is -
class User < ActiveRecord::Base
# ...
# ...
def invite!
transaction do
generate_invitation_token
mark_as_invited
save!
end
WelcomeMailer.delay.send_invitation(id)
end
end
As you can see, it does some setup steps inside a transaction and then sends a WelcomeMailer invitation.
The key here is that I do NOT want an email delivered if there is any issue in updating the database during the transaction.
How do I best implement some error handling here?
The ruby-level method could throw an error. For example save! might error because of some model validation. In that case I can rescue it and return before it gets to the mailer.
What happens if there's some DB level transaction that errors out? Does it bubble up and throw an error in ruby? Will the same begin..rescue approach work there as well?
Thanks!
Two points about your case:
"RecordInvalid - raised by ActiveRecord::Base#save! and ActiveRecord::Base.create! when the record is invalid." (link)
"Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you should be ready to catch those in your application code." (link)
That means, what in a case of an error inside the transaction block in your current simplified code next things will happen:
the transaction will be rolled back
a mail will not be sent
the error will be throwed upper from invite!
If it's necessary to make some actions in a case of an error inside the transaction block, you may change the code like this:
def invite!
transaction do
generate_invitation_token
mark_as_invited
save!
end
rescue RecordInvalid => e
# some actions
raise e
else
# code that runs only if *no* exception was raised
WelcomeMailer.delay.send_invitation(id)
end
I am trying to use an ActiveRecord::Base.transaction to ensure that a Customer, CustomerAccount and StockNotification all get created at once, or none gets created at all if one of them fails
Here is my transaction
stock_notification.rb:
validates_presence_of :email
def self.make parameters
ActiveRecord::Base.transaction do
shop_id = ProductVariant.find(parameters[:product_variant_id]).shop_id
parameters[:customer_account_id] = CustomerAccount.find_or_make!(parameters[:email], shop_id).id
#stock_notification = StockNotification.create(parameters) # reference A
end
#stock_notification
end
You might need this as well
customer_account.rb:
def self.find_or_make! email, shop_id
customer = Customer.where(email: email).first_or_create!
CustomerAccount.where(shop_id: shop_id, customer_id: customer.id).first_or_create!
end
If I call StockNotification.make with a blank email, the create fails (reference A) and no stock notification is created, but the problem is that a Customer/CustomerAccount is still being created.
So the transaction is not doing it's job at all, or am I missing something?
Yep. Transaction will fail if an exception is raised. You should use not create, but create! that will do that in case of failure. See here.
I cannot figure this out for the life of me. I am using CanCan and InheritedResources. I want to delete a group, but revoke the users in the group (not delete them from the database). The revocation is done by setting revoked to true on a user. In my tests there are 2 users at the beginning, and one group.
class GroupsController < InheritedResources::Base
load_and_authorize_resource
def destroy
p User.all # shows the correct value, 2!
#group.users.each do |user|
user.revoked = true
p User.all # still shows 2 on the first loop iteration
user.save!
p User.all # shows 1 on the first iteration! The user was deleted?!
end
super # InheritedResources call to destroy the group
end
Why are my users being deleted? At the end of all this, I have no group, and no users! .save! is not raising an exception, I have tried if user.save as well, and it returns true. I have tried with and without super, so I don't think it is anything InheritedResources related. In my group model, I have:
has_many :users
There is no :dependent => ":destroy". What is going on here? I am surprised and confused that save! is silently deleting my records.
It's not clear to me, from the context above, why this is happening...
Try using user.update_column :revoked, true instead of user.revoked = true and user.save!. This will save it without callbacks, which could potentially (likely?) be interfering with something.
If a user tries to enter a duplicate entry into a table, they get a full page nasty error
ActiveRecord::RecordNotUnique in Admin::MerchantsController#add_alias
Mysql2::Error: Duplicate entry '2-a' for key 'merchant_id': ..
Is there a way to give a flash message instead like "This record already exists"?
This thread from railsforum can help you
I wouldn't recommend checking for the uniqueness and specifically responding to this validation rule via a flash message, this conventionally should be handled by the form's error messages instead.
Nonetheless, in the controller actions for creating and updated you could wrap the conditional block which decides where to send the user, with a check for uniqueness first:
def create
#merchant = Merchant.new params[:merchant]
if Merchant.where(:merchant_id => #merchant.id).count > 0
flash[:error] = "The merchant id #{#merchant.id} already exists"
render :create # amend this to :update when applying to the update action
else
# your normal generated save and render block
end
end
This isn't the cleanest way of achieving your goal, but I think it'll be easiest to understand.
Would really recommend the model validations and form error messages instead, which if you are usung the generated scaffolding, all you need to do is add a model validation and the form throw out the error messages to the user for you:
# app/models/merchant.rb
class Merchant < ActiveRecord::Base
validates_uniqueness_of :merchant_id
end
I have an ActiveRecord model which is returning true from valid? (and .errors is empty), but is returning false from save(). If the model instance is valid, how can I find out what's causing the save to fail?
If #user.save (for example) returns false, then just run this to get all the errors:
#user.errors.full_messages
Try using the bang version save! (with an exclamation mark at the end) and inspecting the resulting error.
Check all your callbacks.
I had a problem like this where I had and "after_validate" method that was failing after I had made a bunch of changes to the model. The model was valid but the "after_validate" was returning false, so if I used model.valid it said true, but then if I saved it gave me validation errors (passed through from the after_validate callback). It was weird.
Look at the application trace and you should be able to see what line of code is raising the exception.
Yea, I fixed this issue by making sure I return true in all my before_* callbacks then it starts working :)
Make sure you aren't trying to save a deleted record.
I had the same issue. But unlike the selected answer - my issue wasn't callbacks related.
In my case I had tried to save to a deleted record (deleted from DB).
#user = User.new
#user.save! # user saved to DB
#user.persisted? # true
#user.destroy # user deleted from DB
#user.persisted? # false, user still has its id
#user.valid? # return true
#user.errors # empty
#user.save # return false
#user.save! # raise ActiveRecord::RecordNotSaved
The problem I had was that I had forgotten to add the validation to the model.
class ContactGroup < ActiveRecord::Base
validates_presence_of :name
end