#java #spring #spring-boot #spring-data-jpa #spring-data
Вопрос:
Я изучаю маршрутизацию транзакций весной, но у моего приложения есть проблема во время выполнения.
У меня есть две базы данных MySQL, одна для чтения и одна для чтения/записи, но моя конфигурация маршрутизации не работает, когда я применяю конфигурацию только для чтения, я не добиваюсь успеха.
Это мои конфигурации:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.1</version>
</parent>
<groupId>br.com.multidatasources</groupId>
<artifactId>multidatasources</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>multidatasources</name>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
применение.свойства
# Database master
master.datasource.url=jdbc:mysql://localhost:3306/billionaires?createDatabaseIfNotExist=trueamp;useTimezone=trueamp;serverTimezone=UTC
master.datasource.username=root
master.datasource.password=root
# Database slave
slave.datasource.url=jdbc:mysql://localhost:3307/billionaires?createDatabaseIfNotExist=trueamp;useTimezone=trueamp;serverTimezone=UTC
slave.datasource.username=root
slave.datasource.password=root
# Database driver
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA property settings
spring.jpa.database=mysql
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
DataSourceType.java
public enum DataSourceType {
READ_ONLY,
READ_WRITE
}
TransactionRoutingDataSource.java
public class TransactionRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? DataSourceType.READ_ONLY : DataSourceType.READ_WRITE;
}
}
RoutingConfiguration.java
@Configuration
@EnableTransactionManagement
public class RoutingConfiguration {
private final Environment environment;
public RoutingConfiguration(Environment environment) {
this.environment = environment;
}
@Bean
public JpaTransactionManager transactionManager(@Qualifier("entityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory.getObject());
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Qualifier("routingDataSource") DataSource routingDataSource) {
LocalContainerEntityManagerFactoryBean bean = new LocalContainerEntityManagerFactoryBean();
bean.setDataSource(routingDataSource);
bean.setPackagesToScan(Billionaires.class.getPackageName());
bean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
bean.setJpaProperties(additionalProperties());
return bean;
}
@Bean
public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}
@Bean
public TransactionRoutingDataSource routingDataSource(
@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource
) {
TransactionRoutingDataSource routingDataSource = new TransactionRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(DataSourceType.READ_WRITE, masterDataSource);
dataSourceMap.put(DataSourceType.READ_ONLY, slaveDataSource);
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(masterDataSource());
return routingDataSource;
}
@Bean
public DataSource masterDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setUrl(environment.getProperty("master.datasource.url"));
dataSource.setUsername(environment.getProperty("master.datasource.username"));
dataSource.setPassword(environment.getProperty("master.datasource.password"));
return dataSource;
}
@Bean
public DataSource slaveDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setUrl(environment.getProperty("slave.datasource.url"));
dataSource.setUsername(environment.getProperty("slave.datasource.username"));
dataSource.setPassword(environment.getProperty("slave.datasource.password"));
return dataSource;
}
private Properties additionalProperties() {
Properties properties = new Properties();
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
return properties;
}
}
Billionaires.java
@Entity
@Table(name = "billionaires")
public class Billionaires {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
private String career;
public Billionaires() { }
public Billionaires(Long id, String firstName, String lastName, String career) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.career = career;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getCareer() {
return career;
}
public void setCareer(String career) {
this.career = career;
}
}
BillionairesRepository.java
@Repository
public interface BillionairesRepository extends JpaRepository<Billionaires, Long> {
}
BillionairesService.java
@Service
public class BillionairesService {
private final BillionairesRepository billionairesRepository;
public BillionairesService(BillionairesRepository billionairesRepository) {
this.billionairesRepository = billionairesRepository;
}
@Transactional(readOnly = true) // Should be used the READ_ONLY (This point not working)
public List<Billionaires> findAll() {
return billionairesRepository.findAll();
}
@Transactional // Should be used the READ_WRITE
public Billionaires save(Billionaires billionaires) {
return billionairesRepository.save(billionaires);
}
}
В классе BillionairesService я применяю метод @Transactional(readOnly = true)
on findAll
для использования READ_ONLY
источника данных, но этого не происходит.
findAll
Метод должен использоваться READ_ONLY
в качестве источника данных, а save
метод должен использоваться READ_WRITE
в качестве источника данных.
Кто-нибудь может помочь мне решить эту проблему?
Комментарии:
1. При получении соединения из источника данных транзакция не требуется. Как я подозреваю, вы используете открытый сеанс в поле зрения (по умолчанию), это происходит еще до того, как источник данных сможет определить состояние tx.
2. Дополнительный совет, вы используете Spring Boot, поэтому вам нужна только конфигурация источника данных, материал JPA все еще может быть автоматически настроен с помощью Spring Boot, а также
@EnableTransactionManagement
. Также проверьте, правильно ли вы используетеTransactionSynchronizationManager
(есть 2 реактивных и один классический, вы должны использовать последний!).3. @М. Deinum я использую
@EnableTransactionalManagement
для создания фасолиentityManagerFactory
, потому что без этого параметра приводит к инициализации исключения: ` *************************** приложение не удалось запустить *************************** описание: параметр 0 конструктора в БР.ком.multidatasources.multidatasources.услуги. Для службы BillionairesService требовался компонент с именем «EntityManagerFactory», который не удалось найти. Действие: Рассмотрите возможность определения компонента с именем «EntityManagerFactory» в вашей конфигурации. `4.
TransactionSynchronizationManager
Я использую классику, ничего реактивного.5.
@EnableTransactionManagement
не создает этот компонент, он только регистрирует аспект для управления транзакциями. Вы должны пометить свойLazyDataSourceConnection
компонент как@Primary
, чтобы он использовался автоматически настроенным менеджером сущностей.
Ответ №1:
Я бы настоятельно рекомендовал использовать автоконфигурацию настолько, насколько это возможно, это немного упростит ситуацию. Главный ключ-установить задержку получения соединения и подготовки его к текущей транзакции.
Этого можно достичь 2 различными способами.
- Установите
prepareConnection
свойствоJpaDialect
дляfalse
. Если вы этого не сделаете, то сJpaTransactionManager
нетерпением получитеConnection
и подготовите его к сделке. Это еще до того, как он успел установить текущее состояние транзакции наTransactionSynchronizationManager
. Который заставит вызовTransactionSynchronizationManager.isCurrentTransactionReadOnly
всегда возвращатьсяfalse
(так как он установлен в концеdoBegin
метода вJpaTransactionManager
. - Установите
hibernate.connection.handling_mode
DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION
значение «кому». Это приведет к задержке получения соединения и закрытию соединения после транзакции. Без Spring это также используется по умолчанию для Hibernate 5.2 (см. Руководство пользователя Hibernate), но по причинам устаревания Spring переключает это наDELAYED_ACQUISITION_AND_HOLD
.
Любое из этих решений будет работать, так как подготовка соединения задерживается, и JpaTransactionManager
, таким образом, у пользователя есть время для синхронизации состояния в TransactionSynchronizationManager
.
@Bean
public BeanPostProcessor dialectProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof HibernateJpaVendorAdapter) {
((HibernateJpaVendorAdapter) bean).getJpaDialect().setPrepareConnection(false);
}
return bean;
}
};
}
Однако добавление этого свойства к вашему application.properties
также будет работать:
spring.jpa.properties.hibernate.connection.handling_mode=DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION
С помощью любого из этих решений вы теперь можете отказаться от конфигурации транзакций, jpa и т. Д. Существует также более простой способ настройки нескольких источников данных. Это описано в справочном руководстве по загрузке Spring, в котором будет использоваться как можно больше автоматической конфигурации Spring.
Сначала убедитесь, что в вашем application.properties
# DATABASE MASTER PROPERTIES
master.datasource.url=jdbc:h2:mem:masterdb;DB_CLOSE_DELAY=-1
master.datasource.username=sa
master.datasource.password=sa
master.datasource.configuration.pool-name=Master-DB
# DATABASE SLAVE PROPERTIES
slave.datasource.url=jdbc:h2:mem:slavedb;DB_CLOSE_DELAY=-1
slave.datasource.username=sa
slave.datasource.password=sa
slave.datasource.configuration.pool-name=Slave-DB
# JPA PROPERTIES SETTINGS
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.properties.hibernate.connection.provider_disables_autocommit=true
spring.jpa.open-in-view=false
# ENABLE ERRORS IN DESERIALIZATION OF MISSING OR IGNORED PROPERTIES
spring.jackson.deserialization.fail-on-unknown-properties=true
spring.jackson.deserialization.fail-on-ignored-properties=true
# ENABLE ERRORS ON REQUESTS FOR NON-EXISTENT RESOURCES
spring.mvc.throw-exception-if-no-handler-found=true
# DISABLE MAPPINGS OF STATIC RESOURCES (IS NOT USABLE IN DEVELOPMENT OF APIs)
spring.web.resources.add-mappings=false
ПРИМЕЧАНИЕ: Удален драйвер для JDBC (не требуется), только spring.jpa.database-platform
если вы установили один database
из них или database-platform
не оба.
Теперь с этим и следующим @Configuration
классом у вас будет 2 источника данных, маршрутизирующий и BeanPostProcessor
упомянутый выше (если вы решите использовать свойство, которое вы можете удалить BeanPostProcessor
).
@Configuration
public class DatasourceConfiguration {
@Bean
@ConfigurationProperties("master.datasource")
public DataSourceProperties masterDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties("master.datasource.configuration")
public HikariDataSource masterDataSource(DataSourceProperties masterDataSourceProperties) {
return masterDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("slave.datasource")
public DataSourceProperties slaveDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties("slave.datasource.configuration")
public HikariDataSource slaveDataSource(DataSourceProperties slaveDataSourceProperties) {
return slaveDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Bean
@Primary
public TransactionRoutingDataSource routingDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
TransactionRoutingDataSource routingDataSource = new TransactionRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(DataSourceType.READ_WRITE, masterDataSource);
dataSourceMap.put(DataSourceType.READ_ONLY, slaveDataSource);
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
@Bean
public BeanPostProcessor dialectProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof HibernateJpaVendorAdapter) {
((HibernateJpaVendorAdapter) bean).getJpaDialect().setPrepareConnection(false);
}
return bean;
}
};
}
}
Это позволит настроить все, что вам нужно для работы, и при этом вы сможете использовать как можно больше функций автоматической настройки и обнаружения. При этом единственная конфигурация, которую вам нужно выполнить, — это эта DataSource
настройка. Никакого JPA, управления транзакциями и т.д., Так как это будет сделано автоматически.
Наконец, вот тест для проверки этого (вы можете протестировать оба сценария). Только для чтения произойдет сбой, потому что там нет схемы, сохранение будет успешным, так как на стороне READ_WRITE есть схема.
@Test
void testDatabaseSwitch() {
Assertions.assertThatThrownBy(() -> billionaireService.findAll())
.isInstanceOf(DataAccessException.class);
Billionaire newBIllionaire = new Billionaire(null, "Marten", "Deinum", "Spring Nerd.");
billionaireService.save(newBIllionaire);
}
Комментарии:
1. Это очень хорошее предложение! Моя вина заключалась в том , что я не определил
@Bean
as@Primary
, по этой причинеentityManager
произошла ошибка инициализации. Я чрезвычайно благодарен вам за то, что вы помогли мне.
Ответ №2:
Я решил эту проблему, изменив свою реализацию RoutingConfiguration.java
класса.
Я настроил источник данных для использования setAutoCommit(false)
конфигурации и добавил свойство hibernate.connection.provider_disables_autocommit
со значением true
.
@Configuration
@EnableTransactionManagement
public class RoutingConfiguration {
private final Environment environment;
public RoutingConfiguration(Environment environment) {
this.environment = environment;
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Qualifier("routingDataSource") DataSource routingDataSource) {
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName());
entityManagerFactoryBean.setPersistenceProvider(new HibernatePersistenceProvider());
entityManagerFactoryBean.setDataSource(routingDataSource);
entityManagerFactoryBean.setPackagesToScan(Billionaires.class.getPackageName());
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
HibernateJpaDialect jpaDialect = vendorAdapter.getJpaDialect();
jpaDialect.setPrepareConnection(false);
entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter);
entityManagerFactoryBean.setJpaProperties(additionalProperties());
return entityManagerFactoryBean;
}
@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
@Bean
public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) {
return new TransactionTemplate(transactionManager(entityManagerFactory));
}
@Bean
public TransactionRoutingDataSource routingDataSource(
@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource
) {
TransactionRoutingDataSource routingDataSource = new TransactionRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(DataSourceType.READ_WRITE, masterDataSource);
dataSourceMap.put(DataSourceType.READ_ONLY, slaveDataSource);
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(masterDataSource());
return routingDataSource;
}
@Bean
public DataSource masterDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setUrl(environment.getProperty("master.datasource.url"));
dataSource.setUsername(environment.getProperty("master.datasource.username"));
dataSource.setPassword(environment.getProperty("master.datasource.password"));
return connectionPoolDataSource(dataSource, determinePoolName(DataSourceType.READ_WRITE));
}
@Bean
public DataSource slaveDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setUrl(environment.getProperty("slave.datasource.url"));
dataSource.setUsername(environment.getProperty("slave.datasource.username"));
dataSource.setPassword(environment.getProperty("slave.datasource.password"));
return connectionPoolDataSource(dataSource, determinePoolName(DataSourceType.READ_ONLY));
}
private HikariDataSource connectionPoolDataSource(DataSource dataSource, String poolName) {
return new HikariDataSource(hikariConfig(dataSource, poolName));
}
private HikariConfig hikariConfig(DataSource dataSource, String poolName) {
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setPoolName(poolName);
hikariConfig.setMaximumPoolSize(Runtime.getRuntime().availableProcessors() * 4);
hikariConfig.setDataSource(dataSource);
hikariConfig.setAutoCommit(false);
return hikariConfig;
}
private Properties additionalProperties() {
Properties properties = new Properties();
properties.setProperty("hibernate.dialect", environment.getProperty("spring.jpa.database-platform"));
properties.setProperty("hibernate.connection.provider_disables_autocommit", "true");
return properties;
}
private String determinePoolName(DataSourceType dataSourceType) {
return dataSourceType.getPoolName().concat("-").concat(dataSourceType.name());
}
}
hibernate.connection.provider_disables_autocommit
Разрешение на подключение устанавливается до вызова determineCurrentLookupKey
метода.
Комментарии:
1. Вы можете просто установить это свойство
application.properties
, для этого вам не нужно заменять автоматическую конфигурацию JPA.2. Я пытался использовать этот подход, но он не дает желаемого эффекта, поскольку необходимо, чтобы я сам создавал компоненты, соответствующие настройкам JPA.
3. Это хранилище учебного кейса с окончательным решением: github.com/jonathanmdr/RoutingDataSource
4. Вы не устанавливаете свойства через JPA (как я уже упоминал, вам следует), если вы этого не сделаете, это, конечно, все равно не будет работать.
5. Я сделал все в соответствии с вашими указаниями, но это просто не сработало. По этой причине я сохранил реализацию в соответствии с ответом.