Let’s pretend that there’s a tags table with a unique field called name.
I have a transaction in which I perform a select to see if a tag with a specific name exists, and if it wasn’t I create it:
START TRANSACTION;
SELECT * FROM TAGS WHERE NAME = "FOO";
-- IF A TAG NAMED "FOO" DIDN'T EXIST THEN
INSERT INTO TAGS VALUES("FOO");
COMMIT;
When two clients run this transaction in the default isolation level (repeatable read), this interleaving will lead one of them to fail with a uniqueness violation:
START TRANSACTION;
START TRANSACTION;
SELECT * FROM TAGS WHERE NAME = "FOO";
SELECT * FROM TAGS WHERE NAME = "FOO";
-- IF A TAG NAMED "FOO" DIDN'T EXIST THEN
INSERT INTO TAGS VALUES("FOO");
-- IF A TAG NAMED "FOO" DIDN'T EXIST THEN
INSERT INTO TAGS VALUES("FOO");
COMMIT;
COMMIT;
I thought if I set the isolation level to serializable, I can avoid this situation, but I noticed that the same interleaving will then lead to a deadlock.
How can I modify the transaction so that it never fails due to uniqueness constraint violation?
For the record, this is the Ruby on Rails (ActiveRecord) code that corresponds to this scenario:
class Tag < ActiveRecord::Base
def self.create_tag(name)
transaction do
# setting isolation level to serializable leads to a deadlock
# Tag.connection.execute("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE")
gets
tag = Tag.find_by_name(name)
if tag.nil?
gets
Tag.create!(:name => name)
end
end
end
end
Looks like this is a type of race condition that Rails does not have any built-in ways for avoiding it out of the box.