9

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?

2 Answers 2

16

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:

  1. 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.
  2. Replace INDEX_NAME_GOES_HERE with the real name of the unique index.
  3. 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).
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks, mu. It looks like that worked. I had tried embedding 'next' in the do_nothing method referred to be rescue_with, but that gave me an error. In any event, it's definitely a PG::Error, and I'm not talking directly to the db. It's just a plain old habtm join table with no custom SQL insert. Perhaps ActiveRecord::RecordNotUnique only gets called on model tables?
I spoke too soon. Tested it again and it's definitely still terminating the loop on the first duplicate it encounters. The interesting this is that if I insert the begin/rescue block in the controller method and remove the rescue_with from the controller, then the exception is not caught at all. So, for some reason, this is apparently not caught at the controller method level at all.
You don't want to catch it at the controller level, that's too far from where you can do something about the exception. What does your code look like?
OK, finally got it working. Changed the PG::Error to ActiveRecord::RecordNotUnique (as you originally suggested) and that fixed it. It's strange, though. The console log only makes reference to the PG::Error. No mention of the ActiveRecord exception.
2

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.

2 Comments

A Rails uniqueness validation should always be backed by a uniqueness constraint inside the database. All the ActiveRecord validations are subject to race conditions so the logic has to be in the database and you have to deal with exceptions if you don't want broken data.
You are absolutely correct, I didn't mean to imply otherwise. When talking of model validation, I definitely mean it as in addition to the unique constraint in the database. You need the constraint in the database for integrity, and the constraint in Rails for ActiveRecord/ActiveModel validation.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.