Search code examples
ruby-on-railspostgresqltransactionsrails-activerecordupsert

How to create a record only if it doesn't exist, avoid duplications and don't raise any error?


It's well known that Model.find_or_create_by(X) actually does:

  1. select by X
  2. if nothing found -> create by X
  3. return a record (found or created)

and there may be race condition between steps 1 and 2. To avoid a duplication of X in the database one should use an unique index on the set of fields of X. But if you apply an unique index then one of competing transactions would fail with exception (when trying to create a copy of X).

How can I implement 'a safe version' of #find_or_create_by which would never raise any exception and always work as expected?


Solution

  • The answer is in the doc

    Whether that is a problem or not depends on the logic of the application, but in the particular case in which rows have a UNIQUE constraint an exception may be raised, just retry:

    begin
      CreditAccount.find_or_create_by(user_id: user.id)
    rescue ActiveRecord::RecordNotUnique
      retry
    end
    

    Solution 1

    You could implement the following in your model(s), or in a Concern if you need to stay DRY

    def self.find_or_create_by(*)
      super
    rescue ActiveRecord::RecordNotUnique
      retry
    end
    

    Usage: Model.find_or_create_by(X)


    Solution 2

    Or if you don't want to overwrite find_or_create_by, you can add the following to your model(s)

    def self.safe_find_or_create_by(*args, &block)
      find_or_create_by *args, &block
    rescue ActiveRecord::RecordNotUnique
      retry
    end
    

    Usage: Model.safe_find_or_create_by(X)