When we delete a user it’s user_id will be automatically removed from all role.user_ids arrays. And when we delete role that role_id will be removed from all user.role_ids.
inverse_of: nil
But then our our system grows with tens of thousands of users in each role. We do not want to store all those user_ids in role. We can modify has_and_belongs_to_many like this to only store role_ids in user record.
Data is now stored like this:
The problem is when we delete Role record it’s ObjectId will remain in all User.role_ids. Since there is no relationship defined in Role model the default callbacks do not fire. So we need to create a custom callback to remove specific role ObjectId from ALL user.role_ids. Instead of array.push we will use array.pull.
When we look in development.log we will see the DB queries which are the same as if there was a default relationship from Role to User.
Let’s write some tests to make sure it works:
counter_cache
What if we want to sort roles by the number of users in each one? In traditional belongs_to relationship we can use counter_cache. For has_and_belongs_to_many we need to create a custom callback.
The downside with this approach is it will cause count queries against Users collection for specific roles on each user save. We can make this approach smarter by checking if roles changed and then incrementing/decrementing role.users_count.
Having these custom callbacks can complicate the application (lead to bugs) so I prefer using traditional two sided has_and_belongs_to_many.
Roles array
Another way we can store this data is to create a simple array on the user record and store roles as strings. To get users by role we create scopes on User model (User.role1, User.role2)
This approach is fine if we have a fairly fixed number of roles. But what if we want to store something like tags for our users? The only difference is we do not restrict tags field to specific enumeration and change scope to accept tags parameter.