【问题标题】:Multi-tenant Spring JPA: Dynamic dialects resolution for dynamic datasources多租户 Spring JPA:动态数据源的动态方言解析
【发布时间】:2020-05-07 02:55:59
【问题描述】:

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

当应用程序启动时,基础数据库的 hibernate 将方言设置为 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)

我有一个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;
    }
}

扩展AbstractDataSourceBasedMultiTenantConnectionProviderImpl的组件类MultiTenantConnectionProviderImpl


@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());
    }
}

还有一个配置类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;
    }
}

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

【问题讨论】:

  • 你有没有得到解决这个问题的办法?我们遇到了同样的问题。
  • 这里有同样的问题。如果你有一个决议会很高兴
  • 只是附和别人的观点,这显然仍然是我无法解决的真正问题。

标签: java spring hibernate multi-tenant


【解决方案1】:

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

根据Hibernate Docs:在多租户系统中分离数据的方法:

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

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

  1. 独立架构 (MultiTenancyStrategy.SCHEMA):

每个租户的数据都保存在一个单独的数据库架构中 数据库实例。

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

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

根据需求确定您要采用的策略。


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

Github 存储库:Working Multitenancy Code

注意:在对数据库进行任何操作之前创建表。

我已在属性文件 (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

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;
    }

}

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();
    }

}

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();
    }
}

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;
    }
}

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

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MultiTenantInterceptor());
    }
}

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);
    }
}

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;
    }

}

用于测试的示例实体类:

@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;
    }

}

用于测试的示例存储库:

public interface UserRepository extends JpaRepository<User, Long>{

}

样品控制器:

@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");
    }
}

【讨论】:

  • 我真的很喜欢这个答案。也许,您会为这种复杂的最小可运行示例提供一个 GitHub 存储库,其中包含运行所需的先决条件?我想尝试一下,我认为它会帮助很多人理解和学习多租户数据库。 :)
  • @NikolasCharalambidis 是的,我会分享。稍等片刻。
  • @NikolasCharalambidis 我想分享这个项目。
  • 我很期待。非常感谢!
  • @NikolasCharalambidis 更新了我的答案。添加了存储库。
【解决方案2】:

根据我们目前对 Spring/JPA 多租户实现的分析,仅当您在启动时具有要连接的初始 DataSource 时,才能连接到多种数据库类型(MSSQL、PostgGreSQL)。 Spring/JPA/Hibernate 框架支持确实需要在应用程序启动期间进行 Dialect 设置,如果不设置会抛出错误。我们的要求是通过另一个需要租户上下文的服务以惰性方式获得这些连接。我们实施了一项变通方案,以使用我们在启动时连接的轻量级空/虚拟内存中 SQLite DB,以通过初始方言和所需的连接。这是对当前框架代码进行最少定制的途径,希望这将作为多租户实施的一项功能添加。

需要初始连接并有助于稍后根据需要连接到多种类型的数据库的关键方法在类中 扩展 AbstractDataSourceBasedMultiTenantConnectionProviderImpl 并覆盖以下方法:

   @Override
protected DataSource selectAnyDataSource() {
    //TODO This method is called more than once. So check if the data source map
    // is empty. If it is then set default tenant for now.
    // This is test code and needs to figure out making it work for application scenarios
    if (tenantDataSources.isEmpty()) {
        tenantDataSources.put("default", dataSource.getDataSource(""));
        log.info("selectAnyDataSource() method call...Total tenants:" + tenantDataSources.size());
    }
    return this.tenantDataSources.values().iterator().next();
}

【讨论】:

  • 另外一个开发者的实现和代码dzone.com/articles/…
  • 在查看这篇文章之前,另一个开发人员的实现和代码也帮助了我。我添加了一个简单的创建来测试我对几个 DBS 的需求和数据库脚本。在与作者核实后,我正在考虑更新 github repo。这段代码在很大程度上有助于理解我们的问题并找到解决方法,因为我们花了很多时间来弄清楚为什么只有供应商数据库特定的操作(如生成序列)不起作用。 dzone.com/articles/…
猜你喜欢
  • 2016-09-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-07-22
  • 1970-01-01
  • 2019-12-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多