Blogg

Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på Twitter

Callista medarbetare Björn Beskow

Dynamic Multi Tenancy with Spring Boot, Hibernate and Liquibase Part 8: Shared Database pattern with Hibernate 6

// Björn Beskow

In part 5, we implemented the Shared Database with Discriminator Column pattern using Hibernate filters. Since then, Hibernate has implemented native support for Discriminator-based multitenancy. Hence in this final part, we’ll implement the Shared Database with Discriminator Column pattern using Hibernate 6 and Spring Boot 3.

Blog Series Parts

Shared Database pattern in Hibernate 6

Support for the Shared Database with Discriminator Column pattern was for a long time described in the Hibernate documentation as “planned for the next version”. That forced us to use the Hibernate Filter mechanism together with some AspectJ magic to implement Shared Database with Discriminator Column. With Hibernate 6 (which is default in Spring Boot 3), the mechanism is finally available. The implementation uses a @TenandId annotation in the entity classes, together with the usual CurrentTenantIdentifierResolver strategy.

As usual, a fully working, minimalistic example can be found in the Github repository for this blog series, in the shared_database_hibernate6 branch.

SingleDatabase

First off, in order to use Hibernate 6 we need to upgrade the sample application to use Spring Boot 3. This is because Spring Boot 2.x uses the old javax.* package names, whereas Hibernate 6 requires the new package names jakarta.*.

Similar to the Database per Tenant and Schema per Tenant patterns, we need to provide an implementation of CurrentTenantIdentifierResolver to instruct Hibernate how to get the current tenant for a request. The implementation is more or less identical to the other pattern implementations, except that we also need to implement HibernatePropertiesCustomizer:

@Component
class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver, HibernatePropertiesCustomizer {

    @Override
    public String resolveCurrentTenantIdentifier() {
        String tenantId = TenantContext.getTenantId();
        if (!ObjectUtils.isEmpty(tenantId)) {
            return tenantId;
        } else {
            // Allow bootstrapping the EntityManagerFactory, in which case no tenant is needed
            return "BOOTSTRAP";
        }
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }

    @Override
    public void customize(Map<String, Object> hibernateProperties) {
        hibernateProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, this);
    }

}

Setting the MULTI_TENANT_IDENTIFIER_RESOLVER property reference is necessary, in order for the resolver implementation to be available to the EntityManager. One could argue that Hibernate should be able to retrieve the CurrentTenantIdentifierResolver bean from the Spring BeanContainer automatically, but that doesn’t currently work so the explicit config is necessary.

The next step is to decorate our Entity classes with a @TenantId annotation to indicate a property to be used as tenant discriminator. We do that in our abstract Entity base class:

@MappedSuperclass
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public abstract class AbstractBaseEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    @Size(max = 30)
    @Column(name = "tenant_id")
    @TenantId
    private String tenantId;

}

This is all Hibernate-specific configuration needed for implementing the Shared Database with Discriminator column pattern. It is indeed a much more elegent, robust and less intrusive implementation than the previous attempt (which used Hibernate Filters).

Remaining configuration in application.yml is plain vanilla Spring Boot config:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/master
    username: postgres
    password: secret
  jpa:
    hibernate:
      ddl-auto: none
    open-in-view: false
  liquibase:
    enabled: true
    changeLog: classpath:db/changelog/db.changelog-tenant.yaml

A fully working, minimalistic example can be found in the Github repository for this blog series, in the shared_database_hibernate6 branch.

Test-driving the solution

Let’s test-drive the solution! Start the multi tenant service in a terminal windows.

Insert some test data for different tenants:

curl -H "X-TENANT-ID: tenant1" -H "Content-Type: application/se.callista.blog.service.api.product.v1_0+json" -X POST -d '{"name":"Product 1"}' localhost:8080/products
curl -H "X-TENANT-ID: tenant2" -H "Content-Type: application/se.callista.blog.service.api.product.v1_0+json" -X POST -d '{"name":"Product 2"}' localhost:8080/products
curl -H "X-TENANT-ID: tenant3" -H "Content-Type: application/se.callista.blog.service.api.product.v1_0+json" -X POST -d '{"name":"Product 3"}' localhost:8080/products

Then query for the data, and verify that the data is properly isolated between tenants:

curl -H "X-TENANT-ID: tenant1" localhost:8080/products
curl -H "X-TENANT-ID: tenant2" localhost:8080/products
curl -H "X-TENANT-ID: tenant3" localhost:8080/products

Having a look at the Product table in the master database shows our newly created test data, with discriminating tenant column:

Content

That concludes this blog series. Thanks for reading!

References

The following link has been very useful inspiration when preparing this material:

spring.io/blog/2022/07/31/how-to-integrate-hibernates-multitenant-feature-with-spring-data-jpa-in-a-spring-boot-application

Tack för att du läser Callistas blogg.
Hjälp oss att nå ut med information genom att dela nyheter och artiklar i ditt nätverk.

Kommentarer