Multitenancy architecture with discriminator column — Part 1
Using Java, Spring Web, Spring Data JPA (Hibernate) and Spring Security
Versão em português do Brasil: https://link.medium.com/P5ZWbWtf43
Good morning, good afternoon or good night!
This is my first article, so I will introduce myself: My name is Filipe Martins, husband, father and senior software analyst at Logicalis Brazil.
Introduction
In one of my personal projects I was in need of a multitenancy architecture and that is what moved everything did and present it to you in this article. Let’s explain:
What is multitenancy?
The term multitenancy, in general, is applied to software development to indicate an architecture in which a single running instance of an application simultaneously serves multiple tenants (customers). Isolating information (data, customizations, etc.) belonging to the various tenants (customers) is a particular challenge in these systems. This includes the data pertaining to each tenant (customer) stored in the database.
How many ways to do multitenancy are there? The most common are:
Separate databases:
The data for each tenant (customer) is kept in separate database instances.
Separate schemes:
The data for each tenant (client) is kept in separate schemas, however the same database instance is used.
Data partitioned through a discriminating column:
The data of each tenant (client) is kept in a single schema of a single instance of the database and they are partitioned using a column that indicates which tenant (client) owns that record.
There are pros and cons to each approach that are outside the scope of this article.
Which use case did I need?
Well, the title of the article is already spoiled, but for my use case i needed the third approach, but Hibernate doesn’t have native support yet, as you can see in the documentation.
DISCRIMINATOR
Correlates to the partitioned (discriminator) approach. It is an error to attempt to open a session without a tenant identifier using this strategy. This strategy is not yet implemented and you can follow its progress via the HHH-6054 Jira issue.
And with that started my search to find implementations, articles and examples. In those I found, when I went to test, it was possible for a tenant (customer) to see data from another! Serious failure!
Well, that’s where the project that I made available on GitHub was born. The reason for sharing it and writing this article are:
- Share knowledge.
- Put my implementation to the test in any ways: Security, scalability, readability and etc. and, with that, if there is something to improve, improve, simple as that.
Project repository
Running the project “AS-IS”
For this you will need:
- Java SE Development Kit 8
- PostgresSQL 9+
- Some development IDE that allows you to use the Lombok plugin.
I recommend the excellent IntelliJ IDEA. - For testing APIs I recommend using Postman.
The easiest way to run PostgreSQL is using Docker. With it installed, just run the command docker run --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres
Now just run the DemoApplication
class. In the “IntelliJ IDEA” right click on it and click on Run 'DemoApplication.main()'
.
Main points of the project development
If you got here, you didn’t just want to get the code ready and use it, so let’s go through the main points together.
Tenantable
is a class whose mapping information is applied to the entities that inherit from it, therefore, it does not have a table in the database for it. We do this by annotating the class with @MappedSuperclass
.
- Lines 3 to 6: This is responsible for inserting filters in the queries (selects) of all classes that inherit from it. We will see later on how these filters will be populated automatically.
- Line 7: Class responsible for “listening” to operations performed in all classes that inherit from it. Let’s see the implementation below.
- Lines 6 to 10: Before an insert (
@PrePersist
) or update (@PreUpdate
) theprePersistOrUpdate
method will be executed. If the object is an instance of theTenantable
class, thetenantId
attribute will have thetenantId
of thetenantContext
as value.
Resume: Here we are ensuring that data inserted or updated is in the tenant of the authenticated user. - Lines 14 to 17: Before a delete (
@PreRemove
) thepreRemove
method will be executed and if the object is an instance of theTenantable
class AND thetenantId
attribute of the object trying to be deleted is different from thetenantId
attribute from thetenantContext
, anEntityNotFoundException
error will be throw. Otherwise, it is successfully deleted.
Resume: Here we are protecting ourselves from attempts by a tenant user to erase data from another tenant.
We mentioned the TenantContext
class above. Let’s see what the purpose of it below.
Simple isn’t it? It is responsible for encapsulating the way we retrieve the tenantId
of the authenticated user.
The same is obtained from the Authentication
object of SecurityContextHolder
, that is, added through a Spring Security filter. Let’s see how this is done below.
The above class is a Spring Security filter. There are several available depending on the use case, but for this article we will use the one that simulates an authenticated user with the email foo@bar.com
from tenant 1
.
TIP: This is where we can put the logic of obtaining user credentials, especially one that is widely used in REST APIs, the JWT, but that is outside the scope of this article … but who knows in the next one, right? ; )
The class above is the one who enables and allows configuring the security provided by Spring Security.
- Line 10: The customized filter we created is placed in the security filter chain.
TIP: This is where we can define authorization rules for accessing routes from our APIs, using antMatchers
, but that’s for the next article or just search and DYI! ; )
Now one of the most interesting classes. It uses Spring AOP, which you can read in more detail at: https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop
- Line 11: When any method that starts with
find
, with any number of arguments, from a repository that inherits fromTenantableRepository
is executed, then thebeforeFindOfTenantableRepository
method will be executed. - Lines 13 to 16: We take the persistence session, enable the filter we saw in the first class (
Tenantable
) and fill it with the value of the authenticated user’stenantId
.
Resume: When a SELECT is made on a repository that inherits from TenantableRepository
, the “WHERE tenant_id =?” will be inserted in the SELECT and the ? will be replaced by the authenticated user’s tenant ID.
In line 11, we have a reference to the class TenantableRepository
. We will see below an extra reason for its existence besides allowing what we have seen so far.
This is the class that every repository needs to inherit if it wants to deal with multi tenant entities. Finally, let’s look at the UserService
class, whose UserRepository
inherits from the class above.
Almost all methods in this class speak for themselves, so we will only talk about updateById
. The reason we need to talk about this specific point is that someone may ask:
Hey! Why didn’t you use the
findById
method that is already implemented?
It would be great, but it wasn’t possible because of this: https://stackoverflow.com/questions/45169783/hibernate-filter-is-not-applied-for-findone-crud-operation
Explaining: The findById
implementation does not do an entity query, but a direct search, so the filters that we want to be filled in automatically, in this case, are not.
Resume: When searching for the entity with findById
it will not make a WHERE for tenantId
in SELECT, thus being able to get a record from another tenant to UPDATE. And that can’t happen!
Going back to the UserService
class.
- Line 16: We use the
findOneById
method, which is now an entity query, that starts withfind
and that ourTenantAspect
class will be able to intercept and fill thetenantId
filter. If the user is trying to update another tenant’s records, he will simply receive anEntityNotFoundException
.
Conclusion
We saw the main points of the project that allows to have a multitenancy application using a discriminator column, which in most cases, and in mine, is called tenant_id
.
Testing
Do you remember the TenantAuthorizationFilter
class?
It is in it that we simulate that the user foo@bar.com
of tenant 1
is authenticated in the system, then:
- Create as many tenants as you want.
- List them and get any
id
. - Place the chosen
id
in the second parameter,tenantId
, of the constructor of the classTenantAuthenticationToken
. - Restart the application to simulate authentication with the tenant chosen in step 2.
- Create users.
- Repeat step 2, get another
id
and repeat steps 3 and 4. - Now try to list, update or delete a user from another tenant.
- Do these and other tests as you wish.
List of available APIs and body examples (JSON)
Tenants
POST http://localhost:8080/api/tenants
Body example: {“name”: “Tenant 1”}
GET: http://localhost:8080/api/tenants
Users
POST http://localhost:8080/api/users
Body example: {“name”: “User 1”}
GET http://localhost:8080/api/users
PATCH http://localhost:8080/api/users/1
Body example: {“name”: “User 1 v2”}
DELETE: http://localhost:8080/api/users/1
Project bonus
I implemented features, listed below, that are not part of the scope of this article and that would make it bigger than it already is.
JPA Auditing
: Makes all entities that inherit fromAuditable
have thecreatedBy and createdDate
fields populated when inserting an entity andlastModifiedBy and lastModifiedDate
populated when updating an entity, automatically.
In the “application.properties” file
- Lines 1 to 3: Setting up the database connection through environment variables.
Line 5: Setting upAllowed Origins
ofCORS
through environment variable.
Well, that’s it folks! I hope you enjoyed! The second and last part of the article is available here.