DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • An Overview of Health Check Patterns
  • Best Performance Practices for Hibernate 5 and Spring Boot 2 (Part 4)
  • Best Performance Practices for Hibernate 5 and Spring Boot 2 (Part 1)
  • Optimizing Database Connectivity: A Comparative Analysis of Tomcat JDBC vs. HikariCP

Trending

  • How to Create a Successful API Ecosystem
  • A Complete Guide to Modern AI Developer Tools
  • Intro to RAG: Foundations of Retrieval Augmented Generation, Part 2
  • FIPS 140-3: The Security Standard That Protects Our Federal Data
  1. DZone
  2. Coding
  3. Frameworks
  4. Multi-Tenancy Implementation for Spring Boot + Hibernate Projects

Multi-Tenancy Implementation for Spring Boot + Hibernate Projects

Separating tenants' data onto different schema is a good way to implement multi-tenancy. Learn how to do that with Spring Boot and Hibernate.

By 
Alon Segal user avatar
Alon Segal
·
Updated Mar. 28, 17 · Tutorial
Likes (60)
Comment
Save
Tweet
Share
101.1K Views

Join the DZone community and get the full member experience.

Join For Free

In this article, we'll see how to achieve multi-tenancy in a Spring Boot project using a schema-per-tenant approach.

This article will also offer a way to handle logins in a multi-tenant environment. This is tricky because, at that stage, it is not always clear to which tenant the yet-to-be-logged user belongs.

This article assumes that the reader is already familiar with multi-tenancy practices, but just in case, here's a short recap:

Multi-Tenancy Has Three Different Approaches

  1. DB per tenant: Each tenant has its own DB for its data. This is the highest level of isolation.

  2. Schema per tenant: Each tenant's data is saved on the same DB but on a different schema. This approach can be implemented in two different ways:

    • Connection pool per schema

    • Single connection pool for all schemas — for each request, a connection is retrieved from the pool and set schema is called with the relevant tenant before assigning it to the context.

  3. Discriminator field: All tenants' data is saved on the same tables, providing that a discriminator field is available on these tables to distinguish each tenant.

I'm not going to dive into the pros and cons of each approach, but if you want to learn more about it, you can read this article, and this MSDN page.

Implementation

In this article, I chose to implement multitenancy using schema-per-tenant paradigm with one connection pool for all tenants.

To handle login, we'll use a general schema (tenant) that has only a single table that maps each user in the system to its relevant tenant. The purpose of this table is to get the user's tenant identifier upon login where the tenant is still unknown. Then, the tenant identifier will be saved in a JWT, but it can be saved in different places, such as an HTTP header.

Multi-Tenancy Setup

First of all, we'll need a shared context of the current tenant. The tenant will be set before each request is handled and be released after. Also, notice that the context is ThreadLocal rather than static because the server can handle more than one tenant at a time.

public class TenantContext {

    private static Logger logger = LoggerFactory.getLogger(TenantContext.class.getName());

    private static ThreadLocal<String> currentTenant = new ThreadLocal<>();

    public static void setCurrentTenant(String tenant) {
        logger.debug("Setting tenant to " + tenant);
        currentTenant.set(tenant);
    }

    public static String getCurrentTenant() {
        return currentTenant.get();
    }

    public static void clear() {
        currentTenant.set(null);
    }
}


The next is TenantInterceptor, an interceptor that reads the tenant identifier from the JWT (or request header in different implementations) and sets the tenant context:

@Component
public class TenantInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.header}")
    private String tokenHeader;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        String authToken = request.getHeader(this.tokenHeader);
        String tenantId = jwtTokenUtil.getTenantIdFromToken(authToken);
        TenantContext.setCurrentTenant(tenantId);

        return true;
    }

    @Override
    public void postHandle(
            HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {
        TenantContext.clear();
    }
}


Create a  CurrentTenantIdentifierResolver — this is a module that Hibernate needs to resolve the current tenant:

@Component
public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver {

    @Override
    public String resolveCurrentTenantIdentifier() {
        String tenantId = TenantContext.getCurrentTenant();
        if (tenantId != null) {
            return tenantId;
        }
        return DEFAULT_TENANT_ID;
    }

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


The next is MultiTenantConnectionProvider — also required by Hibernate to provide the connection to the context. In our case, we're asking a connection from the data source and setting its schema to the relevant tenant:

@Component
public class MultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {

    @Autowired
    private DataSource dataSource;

    @Override
    public Connection getAnyConnection() throws SQLException {
        return dataSource.getConnection();
    }

    @Override
    public void releaseAnyConnection(Connection connection) throws SQLException {
        connection.close();
    }

    @Override
    public Connection getConnection(String tenantIdentifie) throws SQLException {
        String tenantIdentifier = TenantContext.getCurrentTenant();
        final Connection connection = getAnyConnection();
        try {
            if (tenantIdentifier != null) {
                connection.createStatement().execute("USE " + tenantIdentifier);
            } else {
                connection.createStatement().execute("USE " + DEFAULT_TENANT_ID);
            }
        }
        catch ( SQLException e ) {
            throw new HibernateException(
                    "Problem setting schema to " + tenantIdentifier,
                    e
            );
        }
        return connection;
    }

    @Override
    public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
        try {
            connection.createStatement().execute( "USE " + DEFAULT_TENANT_ID );
        }
        catch ( SQLException e ) {
            throw new HibernateException(
                    "Problem setting schema to " + tenantIdentifier,
                    e
            );
        }
        connection.close();
    }

    @SuppressWarnings("rawtypes")
    @Override
    public boolean isUnwrappableAs(Class unwrapType) {
        return false;
    }

    @Override
    public <T> T unwrap(Class<T> unwrapType) {
        return null;
    }

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


Now to wire it up:

@Configuration
public class HibernateConfig {

    @Autowired
    private JpaProperties jpaProperties;

    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        return new HibernateJpaVendorAdapter();
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
                                                                       MultiTenantConnectionProvider multiTenantConnectionProviderImpl,
                                                                       CurrentTenantIdentifierResolver currentTenantIdentifierResolverImpl) {
        Map<String, Object> properties = new HashMap<>();
        properties.putAll(jpaProperties.getHibernateProperties(dataSource));
        properties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
        properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProviderImpl);
        properties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolverImpl);

        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan("com.autorni");
        em.setJpaVendorAdapter(jpaVendorAdapter());
        em.setJpaPropertyMap(properties);
        return em;
    }
}


Login Handling

Upon login, we'll need to query the general schema and to retrieve the user's tenantId. Only then do we proceed with the login on the relevant tenant schema.

Notice that once the connection is established in the context (typically when the first query is executed), it is cached per thread and it cannot be changed. Therefore, the tenant cannot be altered in the middle of the controller. This is a limitation of Hibernate and there's also a ticket for it here, so the workaround is to use a different thread for the default DB query and force Hibernate to recreate the connection with the desired tenant. This workaround (which is taken from here) is necessary only for the login part. There aren't many reasons to switch tenants in the middle of the flow.

In this example, I created a Callable that's called TenantResolver, which contains the logic of querying the default schema in order to get the user's tenantId. 

@RequestMapping(value = "login", method = RequestMethod.POST)
    public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtAuthenticationRequest authenticationRequest) throws AuthenticationException {
//Resolve the user's tenantId
        try {
            tenantResolver.setUsername(authenticationRequest.getUsername());
            ExecutorService es = Executors.newSingleThreadExecutor();
            Future<UserTenantRelation> utrFuture = es.submit(tenantResolver);
            UserTenantRelation utr = utrFuture.get();
            //TODO: handle utr == null, user is not found
            //Got the tenant, now switch to the context
            TenantContext.setCurrentTenant(utr.getTenant());
        } catch (Exception e) {
            e.printStackTrace();
        }

        // Perform the authentication
        final Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        authenticationRequest.getUsername(),
                        authenticationRequest.getPassword()
                )
        );
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // Reload password post-security so we can generate token
        final User user = (User)userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
        final String token = jwtTokenUtil.generateToken(user);

        // Return the token
        return ResponseEntity.ok(new JwtAuthenticationResponse(token, user));
    }


I didn't attach all the code and didn't create a dedicated GitHub repo for it. If it's requested, I will!

Spring Framework Spring Boot Hibernate Implementation Database Schema Connection pool Connection (dance)

Opinions expressed by DZone contributors are their own.

Related

  • An Overview of Health Check Patterns
  • Best Performance Practices for Hibernate 5 and Spring Boot 2 (Part 4)
  • Best Performance Practices for Hibernate 5 and Spring Boot 2 (Part 1)
  • Optimizing Database Connectivity: A Comparative Analysis of Tomcat JDBC vs. HikariCP

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!