I was working on a rake task that needed to create a record in the assignments table if it did not exist. The assignments table had two columns: lecturer_id and student_id. There was also a unique index on these two columns in the database.

The Assignment model had a validation that ensured the uniqueness of the student_id in the scope of the lecturer_id:

class Assignment < ApplicationRecord
  belongs_to :lecturer
  belongs_to :student

  validates :student_id, uniqueness: {scope: :lecturer_id}
end

The goal of the rake task was to assign a random lecturer to each student who did not have one yet. Usually, I use find_or_create_by to achieve this functionality. This method runs a select query and returns the record if it exists, or creates a new record in the database and returns it if it does not exist.

However, for some reason, I used create_or_find_by instead.

When I finished writing the logic and the tests, I noticed that the tests were failing. The create_or_find_by method was returning an object without an ID and the record was not persisted in the database. I tried to debug the issue.

Why My Logic Did Not Work

I thought this might be related to an issue in Rspec or something else that did not make sense. So, while debugging, I added a bang to create_or_find_by to make it create_or_find_by!. This raised an ActiveRecord::RecordInvalid error and said that the record was not unique. This was the error coming from the Rails validation that I added in the model.

I decided to remove the validation from the model and run the tests again. To my surprise, they worked fine. But this did not make sense to me. Why would a model validation prevent a Rails method from working properly?

After searching online, I found out that create_or_find_by relies on rescuing the ActiveRecord::RecordNotUnique exception that is raised from the database when there is a duplicate record that violates the unique constraint. This means that the model validation was actually causing the problem because it raised a different exception: RecordInvalid instead of RecordNotUnique.

I also found that there was already a GitHub issue for this. It was closed by a pull request that updated the documentation to state explicitly that a database constraint was required to use this method. But this still did not explain why having a model validation would break it. Obviously the implementation of create_or_find_by is causing a confusion.

How I Fixed The Logic

Then it dawned on me that I had used the wrong method. I had swapped the words create and find. Instead of using find_or_create_by, I had used create_or_find_by. I replaced create_or_find_by with find_or_create_by and added back the model validation. The tests passed successfully.

Using create_or_find_by was a nice mistake that I learned from. But it also left me wondering why Rails had a method that contradicted its own model validation.

You can read more about the benefits and drawbacks of each method in the sources below.

References