Spring JPA: Как обновить две разные таблицы в двух разных DataSource в одном запросе? - PullRequest
0 голосов
/ 13 декабря 2018

В нашем приложении у нас есть общая база данных под названием central, и у каждого клиента будет своя собственная база данных с точно таким же набором таблиц.База данных каждого клиента может быть размещена на нашем собственном сервере или на сервере клиента в зависимости от требований организации клиента.

Чтобы справиться с этим требованием мультитенанта, мы расширяем AbstractRoutingDataSource из Spring JPA и переопределяемметод determineTargetDataSource() для создания нового DataSource и установки нового соединения на лету на основе входящего customerCode.Мы также используем простой класс DatabaseContextHolder для хранения текущего контекста источника данных в переменной ThreadLocal.Наше решение похоже на то, что описано в этой статье .

Допустим, в одном запросе нам потребуется обновить некоторые данные как в базе данных central, так и в базе данных клиента следующим образом.

public void createNewEmployeeAccount(EmployeeData employee) {
    DatabaseContextHolder.setDatabaseContext("central");
    // Code to save a user account for logging in to the system in the central database

    DatabaseContextHolder.setDatabaseContext(employee.getCustomerCode());
    // Code to save user details like Name, Designation, etc. in the customer's database
}

Этот код будет работать только в том случае, еслиdetermineTargetDataSource() вызывается каждый раз непосредственно перед выполнением любых запросов SQL, чтобы мы могли динамически переключать DataSource на половину нашего метода.

Однако из этого Stackoverflowвопрос , похоже, что determineTargetDataSource() вызывается только один раз для каждого HttpRequest, когда DataSource извлекается в первый раз в этом запросе.

Буду очень признателен, еслиВы можете дать мне некоторое представление о том, когда на самом деле позвонят AbstractRoutingDataSource.determineTargetDataSource().Кроме того, если вы уже сталкивались с подобным мультитенантным сценарием ранее, я хотел бы услышать ваше мнение о том, как мне следует обновлять несколько DataSource в одном запросе.

1 Ответ

0 голосов
/ 13 декабря 2018

Мы нашли рабочее решение, которое представляет собой сочетание статических настроек источника данных для нашей central базы данных и динамических настроек источника данных для базы данных наших клиентов.

По сути, мы точно знаем, из какой таблицы происходиткакая база данных.Следовательно, мы смогли разделить наши @Entity классы на 2 разных пакета следующим образом.

com.ft.model
   -- central
      -- UserAccount.java
      -- UserAccountRepo.java
   -- customer
      -- UserProfile.java
      -- UserProfileRepo.java

Впоследствии мы создали два @Configuration класса для настройки источника данных для каждого пакета.Для нашей базы данных central мы используем статические настройки следующим образом.

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef = "entityManagerFactory",
        transactionManagerRef = "transactionManager",
        basePackages = { "com.ft.model.central" }
)
public class CentralDatabaseConfiguration {
    @Primary
    @Bean(name = "dataSource")
    public DataSource dataSource() {
        return DataSourceBuilder.create(this.getClass().getClassLoader())
                .driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
                .url("jdbc:sqlserver://localhost;databaseName=central")
                .username("sa")
                .password("mhsatuck")
                .build();
    }

    @Primary
    @Bean(name = "entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("dataSource") DataSource dataSource) {
        return builder
                .dataSource(dataSource)
                .packages("com.ft.model.central")
                .persistenceUnit("central")
                .build();
    }

    @Primary
    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager (@Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
}

Для @Entity в пакете customer мы устанавливаем динамический преобразователь источника данных, используя следующий @Configuration.

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef = "customerEntityManagerFactory",
        transactionManagerRef = "customerTransactionManager",
        basePackages = { "com.ft.model.customer" }
)
public class CustomerDatabaseConfiguration {
    @Bean(name = "customerDataSource")
    public DataSource dataSource() {
        return new MultitenantDataSourceResolver();
    }

    @Bean(name = "customerEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("customerDataSource") DataSource dataSource) {
        return builder
                .dataSource(dataSource)
                .packages("com.ft.model.customer")
                .persistenceUnit("customer")
                .build();
    }

    @Bean(name = "customerTransactionManager")
    public PlatformTransactionManager transactionManager(@Qualifier("customerEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
}

В классе MultitenantDataSourceResolver мы планируем сохранить Map от созданного DataSource, используя customerCode в качестве ключа.Из каждого входящего запроса мы получим customerCode и введем его в наш MultitenantDataSourceResolver, чтобы получить правильный DataSource в методе determineTargetDataSource().

public class MultitenantDataSourceResolver extends AbstractRoutingDataSource {
    @Autowired
    private Provider<CustomerWrapper> customerWrapper;

    private static final Map<String, DataSource> dsCache = new HashMap<String, DataSource>();

    @Override
    protected Object determineCurrentLookupKey() {
        try {
            return customerWrapper.get().getCustomerCode();

        } catch (Exception ex) {
            return null;

        }
    }

    @Override
    protected DataSource determineTargetDataSource() {
        String customerCode = (String) this.determineCurrentLookupKey();

        if (customerCode == null)
            return MultitenantDataSourceResolver.getDefaultDataSource();
        else {
            DataSource dataSource = dsCache.get(customerCode);
            if (dataSource == null)
                dataSource = this.buildDataSourceForCustomer();

            return dataSource;
        }
    }

    private synchronized DataSource buildDataSourceForCustomer() {
        CustomerWrapper wrapper = customerWrapper.get();

        if (dsCache.containsKey(wrapper.getCustomerCode()))
            return dsCache.get(wrapper.getCustomerCode() );
        else {
            DataSource dataSource = DataSourceBuilder.create(MultitenantDataSourceResolver.class.getClassLoader())
                    .driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
                    .url(wrapper.getJdbcUrl())
                    .username(wrapper.getDbUsername())
                    .password(wrapper.getDbPassword())
                    .build();

            dsCache.put(wrapper.getCustomerCode(), dataSource);

            return dataSource;
        }
    }

    private static DataSource getDefaultDataSource() {
        return DataSourceBuilder.create(CustomerDatabaseConfiguration.class.getClassLoader())
                .driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
                .url("jdbc:sqlserver://localhost;databaseName=central")
                .username("sa")
                .password("mhsatuck")
                .build();
    }
}

CustomerWrapper - это @RequestScope объект, значения которого будут заполняться при каждом запросе @Controller.Мы используем java.inject.Provider, чтобы ввести его в наш MultitenantDataSourceResolver.

Наконец, даже если логически, мы никогда не будем сохранять что-либо, используя значение по умолчанию DataSource, потому что все запросы всегда будут содержать customerCode, во время запуска нет customerCode доступных.Следовательно, нам все еще нужно предоставить действительное значение по умолчанию DataSource.В противном случае приложение не сможет запуститься.

Если у вас есть какие-либо комментарии или лучшее решение, пожалуйста, сообщите мне.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...