Hibernate multitenancy separate database connections

Hello Hibernate community,

I am working to create a proof of concept for a Spring Boot JPA-Hibernate multitenancy implementation.

My major requirement is using the separate database multitenancy strategy.

I have worked for many organizations over the past 8 years and have not seen this successfully done. My current shop is manually managing tenants’ databases and their tables. I aim to show our team that we could leverage Hibernates’ latest updates to simplify and validate our tenant databases. Relying on Hibernate to validate the schemas and eventually Flyway for complex data migration.

That all said, I have read countless tutorials, involving brilliant colleagues, and from what I can tell there is no reason my application should not generate one table for my platform database and one table for my tenant database instance(s).

I have created a unit test that validates my code and generates each entity/table in the correct data source.

Attached is my github repository, I encourage anyone who has feedback to utilize gitpod.io since I configured it to build and not run tests.

Any help is greatly appreciated, I am getting very discouraged and have resorted to phoning for help here. :pray:

Yours in code,

Stephen

For added context, I have two configuration classes: one for the platform database to hold tenant information and the other for the tenants within a config namespace/directory.

The platform_db is created already using docker compose named platform_db.
I created a database seeder class to populate the platform with some tenants, that calls a tenant database manager class to create databases for each tenant.

“public class DatabaseSeeder implements CommandLineRunner”

    /**
     * Seeds companies into the platform database.
     */
    private void seedCompanies() {
        try {
            long companyCount = companyRepository.count();
            if (companyCount == 0) {
                log.info("Seeding companies into the platform database");
                List<CompanyEntity> companies = Arrays.asList(
                    new CompanyEntity("Company A", true, "America/New_York", "company_a_db"),
                    new CompanyEntity("Company B", true, "America/Chicago", "company_b_db"),
                    new CompanyEntity("Company C", true, "America/Los_Angeles", "company_c_db")
                );

                companyRepository.saveAll(companies);
                log.info("Seeded {} companies into the platform database", companies.size());
            } else {
                log.info("Found {} existing companies in the platform database, skipping seeding", companyCount);
            }
        } catch (DataAccessException e) {
            log.error("Error occurred while seeding companies: ", e);
            throw e;
        }
    }
    /**
     * Initializes the tenant databases by creating them for all existing companies.
     */
    private void initializeTenantDatabases() {
        log.info("Initializing tenant databases");
        try {
            List<CompanyEntity> companies = companyRepository.findAll();
            log.info("Found {} companies to initialize databases for", companies.size());
            
            for (CompanyEntity company : companies) {
                try {
                    tenantDatabaseManager.createTenantDatabaseIfNotExists(company);
                    log.info("Initialized database for company: {}", company.getName());
                } catch (Exception e) {
                    log.error("Error initializing database for company {}: ", company.getName(), e);
                }
            }
            
            log.info("Finished initializing tenant databases");
        } catch (DataAccessException e) {
            log.error("Error occurred while initializing tenant databases: ", e);
            throw e;
        }
    }

After the seeding is complete I expect the Hibernate configurations to kick in. I’ve used namespaces to separate the platform versus tenant database repositories and entities. Yet even after specifying in each database configuration the application generates my company and contact entities only in the platform_db instance instead of companies in the platform_db and contacts in the tenant instance.

I’ve hardcoded my tenant so I can prove a single tenant can have its tables created.

In the platform database configuration class I’ve tried making it not use namespace scanning and explicitly the CompanyRepository.class.
@Configuration
@EnableJpaRepositories(
basePackageClasses = CompanyRepository.class,
entityManagerFactoryRef = “platformEntityManager”,
transactionManagerRef = “platformTransactionManager”
)
public class PlatformDatabaseConfig

    @Primary
    @Bean(name = "platformEntityManager")
    public LocalContainerEntityManagerFactoryBean platformEntityManager(
            @Qualifier("platformDataSource") DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan("traiforce.group.llc.jpa_hibernate_multitenancy_database_separation.platform.entity");

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        em.setJpaProperties(hibernateProperties.getPlatformProperties());

        return em;
    }

The only entity/table defined is the CompanyEntity.java

package traiforce.group.llc.jpa_hibernate_multitenancy_database_separation.platform.entity;

import traiforce.group.llc.jpa_hibernate_multitenancy_database_separation.platform.interfaces.Company;

import org.hibernate.annotations.TimeZoneColumn;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;

// TODO make this a platform entity
// TODO make entity interface to contain annotations keeping our classes clean
@Entity(name = "Company")
@Table(name = "companies")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CompanyEntity implements Company {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "name")
    private String name;
    @Column(name = "active")
    private Boolean active;
    @TimeZoneColumn(name = "time_zone")
    private String timeZone;
    @Column(name = "database")
    private String database;

    // Constructor without id (for creating new entities)
    public CompanyEntity(String name, Boolean active, String timeZone, String database) {
        this.name = name;
        this.active = active;
        this.timeZone = timeZone;
        this.database = database;
    }
}

The same is true for the MultiTenantConfig

@Configuration
@EnableJpaRepositories(
    basePackageClasses = ContactRepository.class,
    entityManagerFactoryRef = "tenantEntityManager",
    transactionManagerRef = "tenantTransactionManager"
)
public class MultiTenantConfig {
...
    @Bean(name = "tenantEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean tenantEntityManagerFactory(
            @Qualifier("tenantDataSource") DataSource dataSource,
            MultiTenantConnectionProviderImpl multiTenantConnectionProvider,
            TenantIdentifierResolver currentTenantIdentifierResolver) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan("traiforce.group.llc.jpa_hibernate_multitenancy_database_separation.tenant.entity");

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);

        Properties properties = new Properties();
        properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
        properties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver);
        properties.putAll(hibernateProperties.getTenantProperties());

        em.setJpaProperties(properties);
        return em;
    }
}

Hope this is useful!

The documentation says it already

Currently, schema export will not really work with multitenancy.

Schema export will use org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider#getAnyConnection for exporting the schema.

I would recommend you to manage and deploy your schema changes with a dedicated tool though like e.g. Liquibase or Flyway.

1 Like

Well, that’s embarrassing, thank you for clarifying.