I'm trying to manage a HABTM relationship with a uniqueness constraint.
ie. I want my User
to
has_and_belongs_to_many :tokens
But I don't want the same token to be associated with a given user more than once.
I put a unique index on the join table
add_index users_tokens [:user_id, :token_id], unique: true
which correctly results in a ActiveRecord::RecordNotUnique exception being thrown if the code tries to add the same token to a given user more than once.
In my code I was hoping to just silently catch/swallow this exception, something like this:
begin
user << token
rescue ActiveRecord::RecordNotUnique
# nothing to do here since the user already has the token
end
However, I'm running into a problem where the RecordNotUnique exception gets thrown much later in my code, when my user object gets modified for something else.
So some code calls something like
...
# The following line throws ActiveRecord::RecordNotUnique
# for user_tokens, even though
# we are not doing anything with tokens here:
user.update_counters
It's as if the association remembers that it's 'dirty' or unsaved, and then tries to save the record that didn't get saved earlier, and ends up throwing the exception.
Any ideas where to look to see if the association actually thinks it's dirty, and/or how to reset its 'dirty' state when I catch the exception?
CodePudding user response:
ActiveRecord maintains in the application layer an object representation of the records in the database including relationships to other objects, and endevours to keep the application layer data representation in sync with the database. When you assign the token
to the user
like this:
user.tokens << token
first ActiveRecord looks for any application-level validations that would prevent the assignment, finding none it links the token
to the user
in the application layer, then it goes on to issue the DB request necessary to also make this connection in the DB layer. The DB has a constrant that prevents it, so an error is raised. You rescue from the error and continue, but the application level connection of the two objects is still in place. The next time that you make any edit to that same user
object through ActiveRecord it will again try to bring the DB into sync with how the object is represented in the application, and since the connection to the token
is still there it will make another attempt to insert this connection in the DB, but this time there is no rescue for the error that arises.
So when you do rescue from the database error you must also undo the application level change like this:
begin
user.toekns << token
rescue ActiveRecord::RecordNotUnique
user.tokens.delete(token)
end