Order of `after_create` callback and `has_many: through` causes a database constraint violation

I’m encountering an issue where the position of the declaration of an after_create callback above or below a has_many: through declaration determines whether or not an Postgres unique constraint violation occurs.

Here is a related comment I made on an old GH issue for posterity: Associations are saved twice, resulting in uniqueness violation · Issue #40459 · rails/rails · GitHub

Contents of my comment below:

I have a User has many roles through user_roles setup

class User < ApplicationRecord

  # setting the after_create here causes a unique constraint violation
  # if we set it below the relation definition then things work fine 
  after_create :assign_default_role # <----- This errors


  has_many :user_roles
  has_many :roles, through: :user_roles

  # this has to be after the user_roles association otherwise
  # it gets called twice and causes unique constraint errors
  after_create :assign_default_role # <----- This works

  def assign_default_role
    contributor_role = Role.find_by!(name: "contributor")
    self.roles << contributor_role unless roles.any?
  end
end

It turns out this random solution in stack overflow worked, but I’m new to rails and couldn’t find any documentation on ordering of callbacks and why this solution worked

The solution in the post is:

Your callback after_create :add_user_role must be defined AFTER the associations

# First: 
has_many :directions
has_many :roles, through: :directions

# Then:
after_create :add_user_role

In the first scenario where the callback is declared before the relation, I see that there are 2 inserts on the join table UserRoles when creating a new user, thus causing the unique violation.

I’m trying to understand what’s going on under the hood. I understand that setting relations via self.roles << contributor_role triggers an immediate insert/commit on the join table, so maybe this commit on the join table insert is somehow triggering the after_create which causes it to call assign_default_role a second time?

My other guess is that defining after_create :assign_default_role after has_many :roles, through: :user_roles somehow doesn’t register the callback the same way?

I’m struggling to find documentation on this behavior, the only thing somewhat related I can find in the rails docs is a call out to avoid calling methods that change state:

Per the docs: Active Record Callbacks — Ruby on Rails Guides

Refrain from using methods like update, save, or any other methods that cause side effects on the object within your callback methods.

For instance, avoid calling update(attribute: "value") inside a callback. This practice can modify the model’s state and potentially lead to unforeseen side effects during commit.

Instead, you can assign values directly (e.g., self.attribute = "value") in before_create, before_update, or earlier callbacks for a safer approach.

As mentioned I’m new to Rails so please let me know if I’m missing anything! Thanks

I put together a small reproducible test case showcasing the error

While I don’t have a reason for why you’re seeing the behavior you’re seeing. I structure all my models like the following

## Associations

## Validations

## Callbacks

## Defaults

## Attributes

## State Machine

## Scopes

## Instance Methods

## Class Methods

We follow the order specified in the rubocop style guide, which is

scopes
constants
attr_accessor/reader/writer
enums
associations
validations
callbacks
other macros

Unfortunately, like the other answer, while giving a reason to avoid the issue you’re seeing, I don’t have an answer to your question of why.

Thanks for the suggestions.

I asked the Ruby discord group as well and the few that responded were also quite perplexed.

I might just need to dig into the source code to understand it further.

Is this bug report worthy on the Rails repo? I want to make sure I do my due diligence before reporting anything.

This has to do with how the through: relations work in regard to the creation of the the join records, and the callback ordering.

has_many through: creates callbacks for creating the join records in an after_create callback itself. So you have two things happening here…

When you add a role to a user not yet created, it creates it when the parent record is persisted. If you add a role to an already persisted user, it creates the join record instantly. Now since the user is created when your after callback gets called, it instantly creates the join record. Then the relations callback gets called, and sees that it needs to create the join record itself.

Moving your callback after the relation, puts your callback after the relations, so it sees no roles it has to create join records for.

Now I am not really sure this should be considered a bug. I personally think its really weird to be adding the role in an after callback (no matter where its located.) I’m not sure the relations should be protecting against stuff being created in other callbacks? Also why should it? The relations after_create callback should be under the assumption that any roles assigned do not have a join record yet (we literally just created the user record.)

Simple enough solution[s];

  1. Use before_create. you want to assign a default role before we go creating stuff. This will ensure the join record gets created as expected. A better option still is probably before_validate. You might want to have a validation that ensures users have at least one role? Or… not the same role twice? This is the approach I would personally take, use before callbacks to ensure everything is in place before AR even opens the transaction to start creating stuff.
  2. Create the join record itself, vs adding the role, but this isn’t as clean I would say. user_roles.create(role: collaborator_role) unless ...

Thanks for the thorough response!

has_many through: creates callbacks for creating the join records in an after_create callback itself […] Then the relations callback gets called, and sees that it needs to create the join record itself.

Do you happen to know where this happens? I poured over the active record source but am having a hard time finding it. This is mostly a curiosity now so I can understand the mechanisms.

I imagine it’s some combo of in these spots

I personally think its really weird to be adding the role in an after callback

Noted! I’m new to Rails and will avoid this going forward :slight_smile: The option 1 solution you described sounds perfect.

Moving your callback after the relation, puts your callback after the relations, so it sees no roles it has to create join records for.

This is the only behavior I’m still confused on. I assumed the location of the callback would have exhibited erroneous behavior no matter where it was defined.

Are you suggesting that since the callback is below the relation definitions that it can’t “see” the relation and it executes differently?

The posters above mention certain order of validations/relations/callbacks in their model files, is there a correct order/place to define callbacks?

Thanks again for your response here and on github