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"
) inbefore_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