spring-projects / spring-data-jpa

Simplifies the development of creating a JPA-based data access layer.
https://spring.io/projects/spring-data-jpa/
Apache License 2.0
2.93k stars 1.39k forks source link

Multi-Tenancy with Spring boot(2.7.18) + Hibernate: "SessionFactory configured for multi-tenancy, but no tenant identifier specified" #3406

Open NaveenRamu opened 3 months ago

NaveenRamu commented 3 months ago

Hi,

I created a multi-tenancy application with Spring Boot and JPA. When TenantIdentifier is not returning any default tenant value then my application is failing with the below error.

image

` TenantContext.java

public class TenantContext {

private static final InheritableThreadLocal<String> CURRENT_TENANT = new InheritableThreadLocal();
private static final Logger LOGGER = LoggerFactory.getLogger(TenantContext.class);

public TenantContext() {
}

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

public static void setCurrentTenant(String tenant) {
    CURRENT_TENANT.set(tenant);
    LOGGER.debug("Setting current tenant in tenant context to:{}", tenant);
}
}

`

` TenantIdentifier.java

@Component
public class TenantIdentifier implements CurrentTenantIdentifierResolver {

private static final Logger logger = LoggerFactory.getLogger(TenantIdentifier.class);

@Override
public String resolveCurrentTenantIdentifier() {
    logger.info("resolveCurrentTenantIdentifier called");
    String tenant = TenantContext.getCurrentTenant();
    logger.info("resolveCurrentTenantIdentifier called tenant {}", tenant);
    return tenant;
}

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

`

`PersistenceJpaConfigWithMultitenancy.java

@Configuration
@EnableJpaRepositories(basePackages = {"*****.**.persistence.repository"}, repositoryFactoryBeanClass = 
ConfigurationRepositoryFactory.class)
@EnableTransactionManagement
public class PersistenceJpaConfigWithMultitenancy {

private static final Logger logger = LoggerFactory.getLogger(PersistenceJpaConfigWithMultitenancy.class);

@Autowired
HikariDataSourceBuilder hikariDataSourceBuilder;

@Autowired
private EnvironmentService environmentService;

@Bean
public MultiTenantConnectionProvider multiTenantConnectionProvider() {
    logger.info("Called DataSourceBasedMultiTenantConnectionProviderImpl");
    return new DataSourceBasedMultiTenantConnectionProviderImpl();
}

@Bean
public CurrentTenantIdentifierResolver currentTenantIdentifierResolver() {
    logger.info("called TenantIdentifier");
    return new TenantIdentifier();
}

private LocalContainerEntityManagerFactoryBean getLocalContainerEntityManagerFactoryBean() {
    return new LocalContainerEntityManagerFactoryBean();
}

@Bean(name = "entityManagerFactory")
@Primary
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() {
    logger.info("Setting the entityManagerFactory bean"); //TODO: to be removed later
    JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    Map<String, Object> hibernateProps = new LinkedHashMap<>();
    hibernateProps.put(Environment.MULTI_TENANT, MultiTenancyStrategy.DATABASE);
    hibernateProps.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, 
    DataSourceBasedMultiTenantConnectionProviderImpl.class);
    hibernateProps.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, TenantIdentifier.class);

    LocalContainerEntityManagerFactoryBean em = getLocalContainerEntityManagerFactoryBean();

    em.setPackagesToScan("com.swgrp.itomdi.administration.persistence.model");
    em.setJpaVendorAdapter(vendorAdapter);
    em.setJpaProperties(additionalProperties());
    em.setJpaPropertyMap(hibernateProps);

    return em;
}

@Bean
@ConditionalOnProperty(
        name = "config.store.type",
        havingValue = "db")
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
    JpaTransactionManager transactionManager
            = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(entityManagerFactory);
    return transactionManager;
}

@Bean
public EntityManagerFactory entityManagerFactory(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
    return entityManagerFactoryBean.getObject();
}

@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
    return new PersistenceExceptionTranslationPostProcessor();
}

Properties additionalProperties() {
    Properties properties = new Properties();
    properties.setProperty("spring.jpa.database", "Vertica");
    properties.setProperty("hibernate.show_sql", "true");
    properties.setProperty(
            "hibernate.dialect", "org.hibernate.dialect.VerticaDialect");
    properties.setProperty("spring.data.jpa.repositories.enabled", "true");
    properties.setProperty("hibernate.temp.use_jdbc_metadata_defaults", "false");
    return properties;
}
}

`

` DataSourceBasedMultiTenantConnectionProviderImpl.java

@Component
public class DataSourceBasedMultiTenantConnectionProviderImpl extends 
AbstractDataSourceBasedMultiTenantConnectionProviderImpl {

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

@Autowired
private HikariDataSourceBuilder hikariDataSourceBuilder;

@Autowired
private EnvironmentService environmentService;

@Override
protected DataSource selectAnyDataSource() {
    logger.info("Calling selectAnyDataSource");
    return AdministrationCacheProvider.getAdminCache().values().iterator().next().getTenantDataSource(); //TODO: check if this can be removed and send null instead
}

@Override
protected DataSource selectDataSource(String tenant) {
    logger.info("Returning datasource for tenant:{}", tenant);
    return AdministrationCacheProvider.getAdminCache().get(tenant).getTenantDataSource();
}
}

`

christophstrobl commented 3 months ago

If you'd like us to spend some time investigating, please take the time to provide a complete minimal sample (something that we can unzip or git clone, build, and deploy) that reproduces the problem. It would also help if the above works with plain hibernate without any spring data being involved.

NaveenRamu commented 3 months ago

Hi @christophstrobl,

Thank you for your quick response.

Please find the attached reproducible code for the problem schema-based-multi-tenancy.zip

API endpoint: http://localhost:8080/notes header: [ X-TenantID - tenant1]

Note: If I uncomment lines 14 to 16 in TenantIdentifierResolver.java class, the application will run without any errors, and the session will open with the default tenant.

chavanp97 commented 1 month ago

Hi @christophstrobl , @spring-projects-issues Just wanted to check if you were able to reproduce the issue at your end using the standalone project provided by us? Kindly let us know if you need any other info from our end. We still have this issue open with us and waiting for any kind of technical assistance.