I have already worked in different projects since 2009, when I started to work as software developer, and there is a problem that appears to be a common issue for a lot of projects: unifying two records into only one.
For example, sometimes a user can create an account logging in using his google account, forget about it, and in the next time creating another account logging in using facebook account. Normally, this user appears in support asking where are his history, his activities, and other stuff for the first account, because he forgot about the google login, but did not forgot about the past use and informations in the platform.
A real example happened with me, some years ago I created an account in Meliuz platform using my email and choosing a custom password and register my CPF(CPF is a unique number that identifies each person in Brazil). I did not use this site for a while, and when I logged again I used my facebook account to log in. I started to use with this account, and in some point I was asked for my CPF in this new account to be able to perform a action in the system, but when I tried to input my CPF I got an error telling me that my CPF was already registered. I had to send a message to theirs support and probably they had to unify my two accounts so I could keep using the facebook account.
The problem unifying records is that in a big project you have a lot of dependencies. For example, let's imagine that we have a project/task manager platform and the below structure.
So, in this case, when we have to unify an user, we have to change projects and tasks too. In this little example, we can think it is not a big deal have to add methods to three models and finish the feature. But when you have a lot bigger project, you will have to deal with lots of models that has to change when you are unifying a record.
Another scenario is that after you build the unify user feature, someone works on a task that adds a new model with user_id and forgets to adds this model in unify user feature. The next time you run the unify users it will break because you have a new model that is not prepared to be changed and keep working after you unify two users.
So, when I get a task in my current job to build an unify users feature, I put two requisites for this to be done:
1 — It has to be easy to run this, only one command, one service to deal with this and the code has to be short and easy to understand;
2 — Every new model created with user_id has to be easy to be added in this unify users flow.
To accomplish the first item, we reach a code like this:
ApplicationRecord.descendants.each do |model\_class|
next unless model\_class.column\_names.include?('user\_id')
model\_class.unify\_users!(user\_to\_empty, user\_to\_keep)
end
user\_to\_empty.delete
Using ApplicationRecord.descendants we had a list of all models, since all of them inherits from ApplicationRecord . So, we iterate on this list and first check for each model if it has a column named user_id. If does'nt, just go next, if has we call unify_users! method. After iterate on all models,, we just delete the duplicated user. So, with this we were satisfying about accomplish the first goal, the first requisite, we had a nice, short and easy to understand code.
But how about the second? We have a problem that for each new model we create with user_idwe would have to remember to create a method named unify_users! , if we forget the code above would break. And even for the existent models with user we would have to create a lot of unify_users method for each model. So, we were not complete satisfied yet, we have to acomplish the second goal.
So, we create a concern and put a rule in the team, we can never adds a belongs_to :user
directly in a model, we should always use our new module named UserBelongs :
module UserBelongs
extend ActiveSupport::Concern
included do
belongs\_to :user, optional: true
end
# Add the unify\_users! method to the model class.
module ClassMethods
def unify\_users!(from, to)
# rubocop:disable Rails/SkipsModelValidations
where(user: from).update\_all(user\_id: to.id)
# rubocop:enable Rails/SkipsModelValidations
end
end
end
So, now we have only one implementation for unify_users! that is enough for almost every model. For some models that needs specific rules for this unify we still need to implement if on each model file, but it is ok, since it is specific rules about that model anyway. Another observation is that we use update_all on this method, and it was on purpose. Update all is the fast way we find to update a lot of records in one single transaction and skipping validations, what was needed for this specific case, because speed was the main concern here since a user could have a lot of records and if we run validations and run a transaction for each one it could take a long time to finish this task.
The most advantage of this approach was seen last month, when we had to create another unify feature, for another model, and with that already built, was really easy and quick to apply the same idea and concept for another model unifying.
So, if you have any suggestions about this, please leave a comment, we would love to discuss and find an even better way to implement this.
Top comments (0)