Wednesday, August 14, 2013

Transparently merge duplicate objects on save of new duplicate using ActiveRecord

I had to deal with this issue at work. I was trying to create customers, and I didn't want duplicates. I wanted it so that when I did Customer.create, I would get have some validation or hook that would instead of saving this dupe, turn it into the old one, ie merge and save the values between the two duplicates, and then set the ID on this new customer to that of the old. It would be completely transparent.

The way to do this is to put a before_create hook that tests whether or not there is a duplicate. If there is, return false at the end of before_create. This halts the transaction from creating. You would think, oh, in before_create, I can look up the duplicate, merge the values between this guy and the old one, and then save the database entry - but you can't. This is inside a transaction, it'll be lost on the Rollback.

Next, make an after_rollback hook, this is outside the transaction of save. In it, find the duplicate again, and now do your merging. I tend to take preference on newer values - if the new guy has an email and the old one does, the newer email is kept, but if the old one has a field set the new one doesn't, I merge it. Then I set self.id = old.id, and here is the key part:

@new_record = false

This is the field active record uses to figure out if it should do insert into or update in the database. If you set the id to some number but don't change this to false, it will still try insert into, and blow up.

So you do:

self.id = old.id
@new_record = false
self.save

Now you overwrite the database entry and your object looks like the old one. Your code thinks you created a new one but really it just found the old one and merged it transparently. Beautiful, right?