Rails 3 ignore Postgres unique constraint exception - ruby-on-rails

What's the correct way to rescue an exception and simply continue processing? I have an app that has Folders and Items, with a habtm relationship through a join table called folders_items. That table has a unique constraint ensuring that there are no duplicate item/folder combinations. If the user tries to add an item to the same folder several times, I obviously don't want the additional rows added; but I don't want to stop processing, either.
Postgres automatically throws an exception when the unique constraint is violated, so I tried to ignore it in the controller as follows:
rescue PG::Error, :with => :do_nothing
def do_nothing
end
This works fine on single insertions. The controller executes the render with a status code of 200. However, I have another method that does bulk inserts in a loop. In that method, the controller exits the loop when it encounters the first duplicate row, which is not what I want. At first, I thought that the loop must be getting wrapped in a transaction that's getting rolled back, but it isn't -- all the rows prior to the duplicate get inserted. I want it to simply ignore the constraint exception and move to the next item. How do I prevent the PG::Error exception from interrupting this?

In general, your exception handling should be at the closest point to the error that you can do something sensible with the exception. In your case, you'd want your rescue inside your loop, for example:
stuff.each do |h|
begin
Model.create(h)
rescue ActiveRecord::RecordNotUnique => e
next if(e.message =~ /unique.*constraint.*INDEX_NAME_GOES_HERE/)
raise
end
end
A couple points of interest:
A constraint violation inside the database will give you an ActiveRecord::RecordNotUnique error rather than the underlying PG::Error. AFAIK, you'd get a PG::Error if you were talking directly to the database rather than going through ActiveRecord.
Replace INDEX_NAME_GOES_HERE with the real name of the unique index.
You only want to ignore the specific constraint violation the you're expecting, hence the next if(...) bit followed by the argumentless raise (i.e. re-raise the exception if it isn't what you're expecting to see).

If you put a Rails validator on your model, then you can control your flow without throwing an exception.
class FolderItems
belongs_to :item
belongs_to :folder
validates_uniqueness_of :item, scope: [:folder], on: :create
end
Then you can use
FolderItem.create(folder: folder, item: item)
It will return true if the association was created, false if there was an error. It will not throw an exception. Using FolderItem.create! would throw an exception if the association is not created.
The reason you are seeing PG errors is because Rails itself thinks that the model is valid on save, because the model class does not have a uniqueness constraint in Rails. Of course, you have a unique constraint in the DB, which surprises Rails and causes it to blow up at the last minute.
If performance is critical then perhaps ignore this advice. Having a uniqueness constraint on a Rails model causes it to perform a SELECT before every INSERT in order for it to do uniqueness validation at the Rails level, potentially doubling the number of queries your loop is performing. Just catching the errors at the database level like you are doing might be a reasonable trade of elegance for performance.
(edit) TL;DR: Always have the unique constraint in the DB. Also having a model constraint will allow ActiveRecord/ActiveModel validation before the DB throws an error.

Related

Enforcing a uniqueness constraint for HABTM

I'm trying to manage a HABTM relationship with a uniqueness constraint.
ie. I want my User to
has_and_belongs_to_many :tokens
But I don't want the same token to be associated with a given user more than once.
I put a unique index on the join table
add_index users_tokens [:user_id, :token_id], unique: true
which correctly results in a ActiveRecord::RecordNotUnique exception being thrown if the code tries to add the same token to a given user more than once.
In my code I was hoping to just silently catch/swallow this exception, something like this:
begin
user << token
rescue ActiveRecord::RecordNotUnique
# nothing to do here since the user already has the token
end
However, I'm running into a problem where the RecordNotUnique exception gets thrown much later in my code, when my user object gets modified for something else.
So some code calls something like
...
# The following line throws ActiveRecord::RecordNotUnique
# for user_tokens, even though
# we are not doing anything with tokens here:
user.update_counters
It's as if the association remembers that it's 'dirty' or unsaved, and then tries to save the record that didn't get saved earlier, and ends up throwing the exception.
Any ideas where to look to see if the association actually thinks it's dirty, and/or how to reset its 'dirty' state when I catch the exception?
ActiveRecord maintains in the application layer an object representation of the records in the database including relationships to other objects, and endevours to keep the application layer data representation in sync with the database. When you assign the token to the user like this:
user.tokens << token
first ActiveRecord looks for any application-level validations that would prevent the assignment, finding none it links the token to the user in the application layer, then it goes on to issue the DB request necessary to also make this connection in the DB layer. The DB has a constrant that prevents it, so an error is raised. You rescue from the error and continue, but the application level connection of the two objects is still in place. The next time that you make any edit to that same user object through ActiveRecord it will again try to bring the DB into sync with how the object is represented in the application, and since the connection to the token is still there it will make another attempt to insert this connection in the DB, but this time there is no rescue for the error that arises.
So when you do rescue from the database error you must also undo the application level change like this:
begin
user.toekns << token
rescue ActiveRecord::RecordNotUnique
user.tokens.delete(token)
end

What can cause ActiveRecord::RecordInvalid on a field with no uniqueness constraint?

I have a copy method that duplicates an object, and then changes some of it's attributes. When saving this, it gives me an ActiveRecord::RecordInvalid error on Name. However, the name attribute does not have a uniqueness constraint, so this should not be failing.
Furthermore, the name HAS been changed so it is unique, and debugging the method indicates this is the case. How can I be getting this error on a field that doesn't have a uniqueness constraint, and IS unique?
I've seen a bunch of questions about this related to RSpec, but this is not in a testing environment, so it's not a DB problem.
I realize I haven't posted code - I'm looking for general answers on what could possibly cause something like this.
It would be much easier to pinpoint actual problem if you could show your code and also Rails version. But if you are looking for general answer, the general answer is that RecordInvalid is raised by bang methods, mainly save! and validate! and other methods that calls those two underneath like create!, update!. This exception is raised by those methods when validation fails. And validation can fail from million of reasons that depends on your validation setup.
This exception can also be raised when those methods are called on invalid associated records.
I also think that validation may fail when you defined your own validation and return false.

Uniqueness validation in database when validation has a condition

Using uniqueness validations in Rails is not safe when there are multiple processes unless the constraint is also enforced on the database (in my case a PostgreSQL database, so see this blog post).
In my case, the uniqueness validation is conditional: it should only be enforced if another attribute in the model becomes true. So I have
class Model < ActiveRecord::Base
validates_uniqueness_of :text, if: :is_published?
def is_published?
self.is_published
end
end
So the model has two attributes: is_published (a boolean) and text (a text attribute). text should be unique across all models of type Model if is_published is true.
Using a unique index (as suggested in the linked blog post) is too constraining because it would enforce the constraint regardless of the value of is_published.
Is anyone aware of a "conditional" index on a PostgreSQL database? Or another way to fix this?
Yes, use a partial UNIQUE index.
CREATE UNIQUE INDEX tbl_txt_is_published_idx ON tbl (text) WHERE is_published;
Example:
How to add a conditional unique index on PostgreSQL
I think that - given speed is not your main concern - you can achieve proper uniqueness validation without creating additional db indexes. Goal can be achieved at the application level. This is especially valuable if you want conditional uniqueness, as some dbs (e.g. versions of MySQL < 8) does not support partial indexes (or so called filtered indexes).
My solution is based on following assumption:
uniqueness check (validator) is run by Rails in the same transaction as save/destroy action that relies on it.
This assumption seems to be true: https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
Both #save and #destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks will happen under its protected cover.
transaction calls can be nested. By default, this makes all database statements in the nested transaction block become part of the parent transaction.
Having that you can use pessimistic locking (https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html) to exclusively lock records that you want to evaluate for uniqueness in validator. That will prevent another, simultaneously running validator - and actually anything that happens after it - from executing until lock is released at the end of transaction. That ensures atomicity of validate-save pair and proper uniqueness enforcement.
In your code it would look like that:
class Model < ActiveRecord::Base
validates :text, uniqueness: {
conditions: ->{ lock.where(is_published: true) }
}
end
The only downside I can see is having db records locked for the whole validate-save process. That won't work well under heavy load, but then many applications don't work under such conditions anyway.

Active Record object loaded from database is invalid

I have an ActiveRecord object that I load from database.
When I call valid? on this object it returns false due to a rails unique constraint not met, at least so the validation says.
I checked the database schema and the unique field also has an index defined, so the uniqueness is also ensured on the database level.
What is going on here and how is this even possible in the first place?
You should check #object.errors.inspect for inspection of what's going on and then fix accordingly.
Also it does matter that when are you checking the validity of an object i.e. before save or after save.
The more elegant way is to use #object.save!
Ruby should tell you what went wrong during the attempt to save the object.
If you do not have unique indexes defined on your database tables, this is what happens!
To be a bit more elaborate: I thought the database had a unique index on the column, but that turned out to be a 'regular' index.
The problem occurred, because at some point in the application, the model got saved without validating it first. Which led to non unique entries in the database. By calling valid? triggers the rails internal routine that checks for uniqueness (however that is implemented) , which returned false, correctly.
Lesson learned: Always make sure to add a unique index at the database level.

rails model validation in the database

I have a table and have the validation for uniqueness setup in the table. eg.
create table posts (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY UNIQUE,
title varchar(255) unique,
content text
);
Here title is unique. Do also need to inform the model class about this uniqueness? If not when i insert a duplicate title, it gives me error. How do I catch that. Currently rails shows me the backtrace and i could not put my own error messages
def create
#f = Post.new(params[:post])
if #f.save
redirect_to posts_path
else
#flash['message'] = "Duplicated title"
render :action=>'new'
end
end
I am not being redirected to the new and instead show a big backtrace.
Use the validates_uniqueness_of validation. "When the record is created, a check is performed to make sure that no record exists in the database with the given value for the specified attribute (that maps to a column)"
You will have to add all of the validations to your models. There is a gem called schema_validations, which will inspect your db for validations and create them in your models for you. https://github.com/lomba/schema_validations
Yes you do as noted in other answers, the answer is validate_uniqueness_of - http://ar.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#M000086. Note, even though you have a validation in your model a race condition does exist where Rails may try and do two inserts unaware of there being a unique record already in the table
When the record is created, a check is performed to make sure that no
record exists in the database with the given value for the specified
attribute (that maps to a column). When the record is updated, the
same check is made but disregarding the record itself.
Because this check is performed outside the database there is still a
chance that duplicate values will be inserted in two parallel
transactions. To guarantee against this you should create a unique
index on the field. See add_index for more information.
So what you have done, by creating a unique index on the database is right, though you may get database driver exceptions in your exception log. There are workarounds for this, such as detecting when inserts happen (through a double click).
The Michael Hartl Rails Tutorial covers uniqueness validation (re. the "email" field) here. It appears the full uniqueness solution is:
Add the :uniqueness validation to the model.
Use a migration to add the unique index to the DB.
Trap the DB error in the controller. Michael's example is the Insoshi people_controller--search for the rescue ActiveRecord::StatementInvalid statement.
Re. #3, it looks like Michael just redirects to the home page on any DB statement exception, so it's not as complex (nor as accurate) as the parsing suggested by #Ransom Briggs, but maybe it's good enough if, as #Omar Qureshi says, the uniqueness constraint covers 99% of the cases.

Resources