多租户 Spring JPA:动态数据源的动态方言解析

Faz*_*zia 5 java spring hibernate multi-tenant

我有一个具有基础数据库 (Oracle) 的应用程序。它从基本数据库中的表中获取另一个租户数据库连接字符串。这些租户可以是 Oracle、Postgres 或 MSSQL。

当应用程序启动时,方言被设置为org.hibernate.dialect.SQLServerDialect基本数据库的休眠状态。但是当我尝试在 MSSQL 数据库的租户中插入数据时,它在插入数据时抛出错误。com.microsoft.sqlserver.jdbc.SQLServerException: DEFAULT 或 NULL 不允许作为显式标识值

这是因为它正在为 Oracle 数据库设置 MSSQL 方言。

[WARN ] 2020-01-21 09:16:22.504 [https-jsse-nio-22500-exec-5] [o.h.e.j.s.SqlExceptionHelper] -- SQL Error: 339, SQLState: S0001
[ERROR] 2020-01-21 09:16:22.504 [https-jsse-nio-22500-exec-5] [o.h.e.j.s.SqlExceptionHelper] -- DEFAULT or NULL are not allowed as explicit identity values.
[ERROR] 2020-01-21 09:16:22.535 [https-jsse-nio-22500-exec-5] [o.a.c.c.C.[.[.[.[dispatcherServlet]] -- Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.InvalidDataAccessResourceUsageException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: could not execute statement] with root cause
com.microsoft.sqlserver.jdbc.SQLServerException: DEFAULT or NULL are not allowed as explicit identity values.
    at com.microsoft.sqlserver.jdbc.SQLServerException.makeFromDatabaseError(SQLServerException.java:217)
    at com.microsoft.sqlserver.jdbc.SQLServerStatement.getNextResult(SQLServerStatement.java:1655)
    at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement.doExecutePreparedStatement(SQLServerPreparedStatement.java:440)
    at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement$PrepStmtExecCmd.doExecute(SQLServerPreparedStatement.java:385)
    at com.microsoft.sqlserver.jdbc.TDSCommand.execute(IOBuffer.java:7505)
    at com.microsoft.sqlserver.jdbc.SQLServerConnection.executeCommand(SQLServerConnection.java:2445)
    at com.microsoft.sqlserver.jdbc.SQLServerStatement.executeCommand(SQLServerStatement.java:191)
    at com.microsoft.sqlserver.jdbc.SQLServerStatement.executeStatement(SQLServerStatement.java:166)
    at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement.executeUpdate(SQLServerPreparedStatement.java:328)
    at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61)
    at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java)
    at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:197)
    at org.hibernate.dialect.identity.GetGeneratedKeysDelegate.executeAndExtract(GetGeneratedKeysDelegate.java:57)
    at org.hibernate.id.insert.AbstractReturningDelegate.performInsert(AbstractReturningDelegate.java:43)
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3106)
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3699)
    at org.hibernate.action.internal.EntityIdentityInsertAction.execute(EntityIdentityInsertAction.java:84)
    at org.hibernate.engine.spi.ActionQueue.execute(ActionQueue.java:645)
    at org.hibernate.engine.spi.ActionQueue.addResolvedEntityInsertAction(ActionQueue.java:282)
    at org.hibernate.engine.spi.ActionQueue.addInsertAction(ActionQueue.java:263)
    at org.hibernate.engine.spi.ActionQueue.addAction(ActionQueue.java:317)
    at org.hibernate.event.internal.AbstractSaveEventListener.addInsertAction(AbstractSaveEventListener.java:335)
    at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:292)
    at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:198)
    at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:128)
    at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:192)
    at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:135)
    at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:62)
    at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:108)
    at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:702)
    at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:688)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
Run Code Online (Sandbox Code Playgroud)

我有一个TenantIdentifierResolver实现CurrentTenantIdentifierResolver

@Component
public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver {

    @Autowired
    PropertyConfig propertyConfig;

    @Override
    public String resolveCurrentTenantIdentifier() {
        String tenantId = TenantContext.getCurrentTenant();
        if (tenantId != null) {
            return tenantId;
        }
        return propertyConfig.getDefaultTenant();
    }
    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }
}
Run Code Online (Sandbox Code Playgroud)

MultiTenantConnectionProviderImpl扩展的组件类AbstractDataSourceBasedMultiTenantConnectionProviderImpl


@Component
public class MultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {
    @Autowired
    private DataSource defaultDS;

    @Autowired
    PropertyConfig propertyConfig;

    @Autowired
    TenantDataSourceService tenantDBService;

    private Map<String, DataSource> map = new HashMap<>();

    boolean init = false;

    @PostConstruct
    public void load() {
        map.put(propertyConfig.getDefaultTenant(), defaultDS);
        ConcurrentMap<String,DataSource> tenantList = tenantDBService.getGlobalTenantDataSource(); //gets tenant datasources from service
        map.putAll(tenantList);
    }

    @Override
    protected DataSource selectAnyDataSource() {
        return map.get(propertyConfig.getDefaultTenant());
    }

    @Override
    protected DataSource selectDataSource(String tenantIdentifier) {
        return map.get(tenantIdentifier) != null ? map.get(tenantIdentifier) : map.get(propertyConfig.getDefaultTenant());
    }
}
Run Code Online (Sandbox Code Playgroud)

和一个配置类 HibernateConfig


@Configuration
public class HibernateConfig {
    @Autowired
    private JpaProperties jpaProperties;

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

    @Bean
    LocalContainerEntityManagerFactoryBean entityManagerFactory(
            DataSource dataSource,
            MultiTenantConnectionProviderImpl multiTenantConnectionProviderImpl,
            TenantIdentifierResolver currentTenantIdentifierResolverImpl
    ) {

        Map<String, Object> jpaPropertiesMap = new HashMap<>(jpaProperties.getProperties());
        jpaPropertiesMap.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
        jpaPropertiesMap.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProviderImpl);
        jpaPropertiesMap.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolverImpl);
        //jpaPropertiesMap.put(Environment.DIALECT_RESOLVERS, "com.esq.cms.CashOrderMgmtService.multitenant.CustomDialectResolver");
        jpaPropertiesMap.put("hibernate.jdbc.batch_size", 500);
        jpaPropertiesMap.put("hibernate.order_inserts", true);
        jpaPropertiesMap.put("hibernate.order_updates", true);

        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan("com.esq.cms.*");
        em.setJpaVendorAdapter(this.jpaVendorAdapter());
        em.setJpaPropertyMap(jpaPropertiesMap);
        return em;
    }
}
Run Code Online (Sandbox Code Playgroud)

有许多使用属性文件设置方言的示例,但它们具有固定类型和数量的数据库。就我而言,它可以是任何数据库类型。我也试过为休眠解析器添加一个自定义类,但它仍然无法正常工作。我可能会遗漏一些东西。因此,我应该怎么做才能通过休眠本身根据数据库启用方言。任何帮助都将得到认可。谢谢

Ani*_* B. 4

当您处理不同类型的数据库(DATABASE例如:Oracle、MySQL 等)时,请尝试采用多租户策略。SCHEMADISCRIMINATOR

根据Hibernate 文档:在多租户系统中分离数据可以采取的方法:

  1. 独立数据库( MultiTenancyStrategy.DATABASE) :

每个租户的数据都保存在物理上独立的数据库实例中。JDBC 连接将专门指向每个单独的数据库,以便连接池将针对每个单租户。连接池是根据链接到特定用户的“租户标识符”来选择的。

  1. 单独的架构( MultiTenancyStrategy.SCHEMA) :

每个租户的数据都保存在单个数据库实例上的不同数据库模式中。

  1. 分区数据( MultiTenancyStrategy.DISCRIMINATOR):

所有数据仅保存在单个数据库模式中。每个租户的数据通过使用鉴别器进行分区。所有租户使用单个 JDBC 连接池。对于每个 SQL 语句,应用程序需要根据“租户标识符”鉴别器来管理数据库上查询的执行。

根据要求决定您想要采用哪种策略。


我提供了我自己的多租户示例代码(使用 spring-boot),我使用两个不同的数据库完成了该代码,一个使用MySQL,另一个使用Postgres。这是我提供的工作示例。

Github 存储库:工作多租户代码

注意:在数据库中执行任何操作之前先创建表。

我已经使用不同的数据库配置了属性文件(application.properties)中的所有租户。

server.servlet.context-path=/sample

spring.jpa.generate-ddl=false
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true


## Tenant 1 database ##
multitenant.datasources.tenant1.url=jdbc:postgresql://localhost:5432/tenant1
multitenant.datasources.tenant1.username=postgres
multitenant.datasources.tenant1.password=Anish@123
multitenant.datasources.tenant1.driverClassName=org.postgresql.Driver

## Tenant 2 database ##
multitenant.datasources.tenant2.url=jdbc:mysql://localhost:3306/tenant2
multitenant.datasources.tenant2.username=root
multitenant.datasources.tenant2.password=Anish@123
multitenant.datasources.tenant2.driverClassName=com.mysql.cj.jdbc.Driver
Run Code Online (Sandbox Code Playgroud)

MultiTenantProperties:此类绑定并验证为多个租户设置的属性,并将它们保存为租户与所需数据库信息的映射。

@Component
@ConfigurationProperties(value = "multitenant")
public class MultiTenantProperties {

    private Map<String, Map<String, String>> datasources = new LinkedHashMap<>();

    public Map<String, Map<String, String>> getDatasources() {
        return datasources;
    }

    public void setDatasources(Map<String, Map<String, String>> datasources) {
        this.datasources = datasources;
    }

}
Run Code Online (Sandbox Code Playgroud)

ThreadLocalTenantStorage:此类保留来自当前线程执行 CRUD 操作的传入请求的租户名称。

public class ThreadLocalTenantStorage {

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

    public static void setTenantName(String tenantName) {
        currentTenant.set(tenantName);
    }

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

    public static void clear() {
        currentTenant.remove();
    }

}
Run Code Online (Sandbox Code Playgroud)

MultiTenantInterceptor:此类拦截传入请求,并为要选择的数据库设置当前租户的 ThreadLocalTenantStorage。请求完成后,租户将从班级中删除ThreadLocalTenantStorage

public class MultiTenantInterceptor extends HandlerInterceptorAdapter {

    private static final String TENANT_HEADER_NAME = "TENANT-NAME";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        String tenantName = request.getHeader(TENANT_HEADER_NAME);
        ThreadLocalTenantStorage.setTenantName(tenantName);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
        ThreadLocalTenantStorage.clear();
    }
}
Run Code Online (Sandbox Code Playgroud)

TenantIdentifierResolver:该类负责返回来自ThreadLocalTenantStorage的当前租户以选择数据源。

public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver {

    private static String DEFAULT_TENANT_NAME = "tenant1";

    @Override
    public String resolveCurrentTenantIdentifier() {
        String currentTenantName = ThreadLocalTenantStorage.getTenantName();
        return (currentTenantName != null) ? currentTenantName : DEFAULT_TENANT_NAME;
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }
}
Run Code Online (Sandbox Code Playgroud)

WebConfiguration:这是注册MultiTenantInterceptor要用作拦截器的类的配置。

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MultiTenantInterceptor());
    }
}
Run Code Online (Sandbox Code Playgroud)

DataSourceMultiTenantConnectionProvider:该类根据租户名称选择数据源。

public class DataSourceMultiTenantConnectionProvider extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {

    private static final long serialVersionUID = 1L;

    @Autowired
    private Map<String, DataSource> multipleDataSources;

    @Override
    protected DataSource selectAnyDataSource() {
        return multipleDataSources.values().iterator().next();
    }

    @Override
    protected DataSource selectDataSource(String tenantName) {
        return multipleDataSources.get(tenantName);
    }
}
Run Code Online (Sandbox Code Playgroud)

MultiTenantJPAConfiguration:此类为数据库事务配置自定义bean,并为多租户注册租户数据源。

@Configuration
@EnableJpaRepositories(basePackages = { "com.example.multitenancy.dao" }, transactionManagerRef = "multiTenantTxManager")
@EnableConfigurationProperties({ MultiTenantProperties.class, JpaProperties.class })
@EnableTransactionManagement
public class MultiTenantJPAConfiguration {

    @Autowired
    private JpaProperties jpaProperties;

    @Autowired
    private MultiTenantProperties multiTenantProperties;

    @Bean
    public MultiTenantConnectionProvider multiTenantConnectionProvider() {
        return new DataSourceMultiTenantConnectionProvider();
    }

    @Bean
    public CurrentTenantIdentifierResolver currentTenantIdentifierResolver() {
        return new TenantIdentifierResolver();
    }

    @Bean(name = "multipleDataSources")
    public Map<String, DataSource> repositoryDataSources() {
        Map<String, DataSource> datasources = new HashMap<>();
        multiTenantProperties.getDatasources().forEach((key, value) -> datasources.put(key, createDataSource(value)));
        return datasources;
    }

    private DataSource createDataSource(Map<String, String> source) {
        return DataSourceBuilder.create().url(source.get("url")).driverClassName(source.get("driverClassName"))
                .username(source.get("username")).password(source.get("password")).build();
    }

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

    @Bean
    public PlatformTransactionManager multiTenantTxManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(
            MultiTenantConnectionProvider multiTenantConnectionProvider,
            CurrentTenantIdentifierResolver currentTenantIdentifierResolver) {

        Map<String, Object> hibernateProperties = new LinkedHashMap<>();
        hibernateProperties.putAll(this.jpaProperties.getProperties());
        hibernateProperties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.DATABASE);
        hibernateProperties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
        hibernateProperties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver);
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactoryBean.setPackagesToScan("com.example.multitenancy.entity");
        entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        entityManagerFactoryBean.setJpaPropertyMap(hibernateProperties);
        return entityManagerFactoryBean;
    }

}
Run Code Online (Sandbox Code Playgroud)

用于测试的示例实体类:

@Entity
@Table(name = "user_details", schema = "public")
public class User {

    @Id
    @Column(name = "id")
    private Long id;

    @Column(name = "full_name", length = 30)
    private String name;

    public User() {
        super();
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}
Run Code Online (Sandbox Code Playgroud)

用于测试的示例存储库:

public interface UserRepository extends JpaRepository<User, Long>{

}
Run Code Online (Sandbox Code Playgroud)

示例控制器:

@RestController
@Transactional
public class SampleController {

    @Autowired
    private UserRepository userRepository;

    @GetMapping(value = "/{id}")
    public ResponseEntity<User> getUser(@PathVariable("id") String id) {
        Optional<User> user = userRepository.findById(Long.valueOf(id));
        User userDemo = user.get();
        return ResponseEntity.ok(userDemo);
    }

    @PostMapping(value = "/create/user")
    public ResponseEntity<String> createUser(@RequestBody User user) {
        userRepository.save(user);
        return ResponseEntity.ok("User is saved");
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我真的很喜欢这个答案。也许,您会为这种复杂的最小可运行示例提供一个 GitHub 存储库,并提供运行所需的先决条件吗?我想尝试一下,我认为这会帮助很多人了解和学习多租户数据库。:) (2认同)
  • @NikolasCharalambidis 是的,我会分享。等待一段时间。 (2认同)
  • 我期待着它。多谢! (2认同)