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!