Home > other >  Enforcing a uniqueness constraint for HABTM
Enforcing a uniqueness constraint for HABTM

Time:11-30

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
  • Related