I recently worked on a project for an Australian company who wanted to ensure they were GDPR compliant in how they handled personal data through their website and requests to delete user accounts.
If we want the user and all the associated data (blog posts, comments, etc) deleted, it is easy, we just need to add dependent: :destroy
to the relations and Rails takes care of that for us.
But in this scenario, the client wanted to keep the user-related data after a user account was deleted.
General Data Protection Regulation (GDPR) background:
By way of background, since the GDPR was approved for the European Union in May 2018, it is more and more common for users to ask for their accounts to be deleted. The EU General Data Protection Regulation (GDPR) generally applies to the data processing activities of data processors or controllers where:
- an establishment of the controller or processor is in the EU
- the controller or processor is outside the EU, and the processing activities are related to:
Offering goods or services to individuals in the EU (irrespective of whether a payment is required)
Monitoring the behaviour of individuals in the EU, where that behaviour takes place in the EU (see Article 3).
So a US or Australian business of any size may need to comply if they have an establishment in the EU, if they offer goods and services in the EU, or if they monitor the behaviour of individuals in the EU. And in this day and age this encompasses a lot of companies.
Australian privacy law doesn’t include the “right to be forgotten” which is covered in the GDPR. So I had to ensure this was addressed in our client’s code.
Deleting Accounts:
Let’s say you have an awesome blog made in Ruby on Rails, where people can create accounts and add posts. What happens if someone who had posted articles on your blog, now asks for their account to be deleted?
There are several ways to deal with this, but in this article, I’d like to cover the null object way.
For this, let’s imagine we have a normal Ruby on Rails blog, with posts and users, and we need to set it up so users can be deleted. Let’s see what the code could look like, and what changes we need to add in order to be able to delete users and have the application still working properly.
On the model User, generically, we have something like this:
# app/models/user.rb
Class User < ApplicationRecord
has_many :posts
def full_name
"#{first_name} #{last_name}"
end
end
And the Post model should look like this:
# app/models/post.rb
Class Post < ApplicationRecord
belongs_to :user
def user_name
user.full_name
end
end
The issue to address is when users ask to delete their accounts, their posts become orphans, and unless we want to delete them, we have to deal with this situation.
If, on the page where we display the posts, we also display the author name, picture, or any other user attribute, then after we delete an account, these pages will be broken, since calling all these methods on nil
will cause a method missing error.
So, how do we fix this using the Null Object Pattern?
We should start by adding a nulls
folder under the app
directory. From there add the following file:
# app/nulls/null_user.rb
Class NullUser
end
Then, on the Post model, change the relation to read the following:
belongs_to :user, optional: true
This will allow us to safely delete users with posts without getting any constraint errors.
If you have any custom validation on the user_id
presence, please remove them as well.
Ok, so now that we can remove users with posts, we need to make sure the post pages (index, show) won’t break when we visit them due to calling methods on nil.
Let’s continue our example using the user_name
method.
On the Post model file, add the following changes:
# app/models/post.rb
Class Post < ApplicationRecord
belongs_to :user, optional: true
def user_name
user&.full_name || NullUser.new.user_name
end
end
Now let’s implement the user_name
method on the NullUser
class:
# app/nulls/null_user.rb
Class NullUser
def user_name
'User deleted'
end
end
Now when calling user_name
on a post without user, it will return ‘User deleted’ instead of breaking. Feel free to customise the message you want to return.
But what if we have multiple methods on the Post model for retrieving its author details (eg. age, avatar, posts count, and so on)? To keep the code DRY, we can edit the NullUser
class like this:
# app/nulls/null_user.rb
Class NullUser
def method_missing(*args, &block)
'User deleted'
end
end
Then edit the Post model to the following:
# app/models/post.rb
Class Post < ApplicationRecord
belongs_to :user, optional: true
def user
super || NullUser.new
end
end
And that’s how you can use the null object pattern in Ruby on Rails to DRY your code and preventing method missing errors.