簡體   English   中英

多租戶 Spring JPA:動態數據源的動態方言解析

[英]Multi-tenant Spring JPA: Dynamic dialects resolution for dynamic datasources

我有一個具有基礎數據庫 (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)

我有一個實現CurrentTenantIdentifierResolverTenantIdentifierResolver

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

有很多使用屬性文件設置方言的例子,但它們有固定的數據庫類型和數量。 在我的例子中,它可以是任何數據庫類型。 我還嘗試為 hibernate 解析器添加一個自定義類,但它仍然無法正常工作。 我可能會遺漏一些東西。 因此,我應該怎么做才能通過休眠本身根據數據庫啟用方言。 任何幫助都會得到幫助。 謝謝

嘗試將多租戶策略作為DATABASE而不是SCHEMADISCRIMINATOR ,因為您正在處理不同類型的數據庫(例如:Oracle、MySQL 等)。

根據Hibernate Docs :The approaches that you can take for separating data in the multi-tenant systems:

  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

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

根據我們當前對 Spring/JPA 多租戶實施的分析,僅當您在啟動時具有要連接的初始數據源時,才可以連接到多種數據庫類型(MSSQL、PostgGreSQL)。 Spring/JPA/Hibernate 框架支持確實需要在應用程序啟動期間設置方言,如果您不設置,將會拋出錯誤。 我們的要求是通過另一個需要租戶上下文的服務以惰性方式獲得這些連接。 我們實施了一項解決方案,以使用我們在啟動時連接的輕量級空/虛擬內存中 sqlite 數據庫,以傳遞所需的初始方言和連接。 這是對當前框架代碼進行最少定制的途徑,希望這將作為多租戶實施過程中的一項功能添加。

需要初始連接並有助於稍后根據需要連接到多種類型的數據庫的關鍵方法是在類中擴展 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();
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM