J'ai dû rechercher et mettre à jour plusieurs bases de données en une seule demande, alors j'ai cherché comment le faire. 2018/05/20 ** J'ai créé un article en utilisant narayanaJTA ici. ** **
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
Créer DataSourceProperties.java
et DataSourcePropertyDetail.java
utilisés pour obtenir les informations de connexion et lire les informations de ʻapplication.yml`
package com.example.demo.common;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties("demo.datasource")
public class DataSourceProperties {
/**
* Conservez la valeur initiale des informations de connexion
*/
private DataSourcePropertyDetail base;
/**
* Gardez une liste des informations de connexion. Des informations insuffisantes sont définies à partir de la valeur initiale
*/
private List<DataSourcePropertyDetail> list;
/ ** setter getter omis * /
}
Pour la valeur réelle du paramètre, récupérez celle définie dans DataSourcePropertyDetail.java
.
Cette fois, définissez les quatre minimum de driverClassName, url, nom d'utilisateur, mot de passe et defaultAutoCommit.
Préparez également un constructeur pour une copie complète en tenant compte du cas de l'ajout d'informations supplémentaires générées dans la logique.
package com.example.demo.common;
import java.util.Objects;
public class DataSourcePropertyDetail {
private String name;
private String driverClassName;
private String url;
private String username;
private String password;
private Boolean defaultAutoCommit;
public DataSourcePropertyDetail(){
}
public DataSourcePropertyDetail(DataSourcePropertyDetail prop){
if (Objects.isNull(prop)) {
return;
}
this.driverClassName = prop.driverClassName;
this.url = prop.url;
this.username = prop.username;
this.password = prop.password;
this.defaultAutoCommit = prop.defaultAutoCommit;
}
/ ** setter getter omis * /
}
Ajoutez les informations pour lire les données DataSourceProperties
etDataSourcePropertyDetail
créées ci-dessus à ʻapplication.yml`.
demo:
datasource:
base:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/postgres
username: postgres
password: postgres
default-auto-commit: false
list:
- name: postgres
- name: user
url: jdbc:postgresql://localhost:5432/user
username: user
password: user
- name: database
url: jdbc:postgresql://localhost:5432/database
username: database
password: database
Deux parties sont nécessaires pour changer de source de données. Préparez une partie qui crée une source de données à partir des informations de connexion et une partie qui détermine la source de données à utiliser au moment de la connexion.
Fondamentalement, le traitement nécessaire peut être décrit dans DataSourceConfig
, mais comme il y a certains traitements que vous souhaitez utiliser à plusieurs reprises cette fois, DataSourceUtil
est prêt à résumer le traitement à utiliser dans le futur.
package com.example.demo.common;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
@Component
@Configuration
public class DataSourceConfig {
@Autowired
DataSourceProperties properties;
@Bean
@Primary
public DynamicRoutingDataSourceResolver getRoutingdataSource() {
DynamicRoutingDataSourceResolver resolver = new DynamicRoutingDataSourceResolver();
DataSource defDataSource = DataSourceUtil.getDataSource(properties.getBase(), properties.getBase());
Map<Object, Object> datasources = new HashMap<Object,Object>();
datasources.put(properties.getBase().getName(), defDataSource);
properties.getList().forEach(prop -> datasources.put(prop.getName(), getDataSource(prop)));
resolver.setTargetDataSources(datasources);
resolver.setDefaultTargetDataSource(defDataSource);
return resolver;
}
private DataSource getDataSource(DataSourcePropertyDetail prop) {
return DataSourceUtil.getDataSource(properties.getBase(), prop);
}
}
package com.example.demo.common;
import java.util.Objects;
import java.util.function.Consumer;
import org.apache.tomcat.jdbc.pool.DataSource;
public class DataSourceUtil {
public static DataSource getDataSource(DataSourcePropertyDetail base, DataSourcePropertyDetail prop) {
DataSource datasource = new org.apache.tomcat.jdbc.pool.DataSource();
skipIfNull(defaultIfEmpty(prop.getDriverClassName(), base.getDriverClassName()), datasource::setDriverClassName);
skipIfNull(defaultIfEmpty(prop.getUrl(), base.getUrl()), datasource::setUrl);
skipIfNull(defaultIfEmpty(prop.getUsername(), base.getUsername()), datasource::setUsername);
skipIfNull(defaultIfEmpty(prop.getPassword(), base.getPassword()), datasource::setPassword);
skipIfNull(defaultIfEmpty(prop.getDefaultAutoCommit(), base.getDefaultAutoCommit()), datasource::setDefaultAutoCommit);
return datasource;
}
public static<T> T defaultIfEmpty(T obj, T def) {
if (Objects.isNull(obj)) {
return def;
}
return obj;
}
public static <T> void skipIfNull(T obj, Consumer<T> consumer) {
if (Objects.isNull(obj)) {
return;
}
consumer.accept(obj);
}
}
Trois pour se préparer
ʻDynamicRoutingDataSourceResolver qui hérite de AbstractRoutingDataSource
DataBaseSelectHolder
qui contient les informations de connexion sélectionnées sur le thread local
DataBaseSelectInfo
, qui est une structure pour les informations de connexion
package com.example.demo.common;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicRoutingDataSourceResolver extends AbstractRoutingDataSource {
@Autowired
DataSourceProperties properties;
@Override
protected Object determineCurrentLookupKey() {
DataBaseSelectInfo key = DataBaseSelectHolder.getDataBaseInstanceInfo();
if (Objects.isNull(key)) {
return null;
}
return key.getDataSourceName();
}
private DataBaseSelectInfo determineCurrentLookupKeyRaw() {
return DataBaseSelectHolder.getDataBaseInstanceInfo();
}
@Override
protected DataSource determineTargetDataSource() {
DataBaseSelectInfo info = determineCurrentLookupKeyRaw();
// S'il y a des informations supplémentaires, créez une source de données basée sur les informations supplémentaires
// Appelle la méthode de la classe héritière s'il n'y a pas d'informations supplémentaires
if (info != null && info.isAdditional()) {
return createNewDataSouce(info);
}
return super.determineTargetDataSource();
}
// Créer une source de données avec des informations supplémentaires basées sur les propriétés spécifiées
private DataSource createNewDataSouce(DataBaseSelectInfo info) {
Optional<DataSourcePropertyDetail> op = properties.getList()
.stream()
.filter(prop -> Objects.equals(prop.getName(), info.getDataSourceName()))
.findFirst();
if (op.isPresent()) {
DataSourcePropertyDetail prop = new DataSourcePropertyDetail(op.get());
DataSourceUtil.skipIfNull(info.getAdditionalUrl(),
(url) -> prop.setUrl(prop.getUrl() + url));
DataSourceUtil.skipIfNull(info.getAdditionalUserName(),
(name) -> prop.setUsername(prop.getUsername() + name));
DataSourceUtil.skipIfNull(info.getAdditionalPassword(),
(password) -> prop.setPassword(prop.getPassword() + password));
return DataSourceUtil.getDataSource(properties.getBase(), prop);
}
return super.determineTargetDataSource();
}
}
package com.example.demo.common;
import org.springframework.util.Assert;
public class DataBaseSelectHolder {
private static ThreadLocal<DataBaseSelectInfo> contextHolder = new ThreadLocal<DataBaseSelectInfo>();
public static void setDataBaseInstanceInfo(DataBaseSelectInfo datasource) {
Assert.notNull(datasource, "datasource cannot be null.");
contextHolder.set(datasource);
}
public static void setDataBaseInstanceInfo(String dataSourceName) {
Assert.notNull(dataSourceName, "dataSourceName cannot be null.");
contextHolder.set(new DataBaseSelectInfo(dataSourceName));
}
public static void setDataBaseInstanceInfo(String dataSourceName, String additionalUrl,
String additionalUserName, String additionalPassword) {
Assert.notNull(dataSourceName, "dataSourceName cannot be null.");
contextHolder.set(new DataBaseSelectInfo(dataSourceName, additionalUrl,
additionalUserName, additionalPassword));
}
public static DataBaseSelectInfo getDataBaseInstanceInfo() {
return contextHolder.get();
}
public static void clear() {
contextHolder.remove();
}
}
package com.example.demo.common;
import java.util.Objects;
public class DataBaseSelectInfo {
private String dataSourceName;
private String additionalUrl;
private String additionalUserName;
private String additionalPassword;
public DataBaseSelectInfo(String dataSourceName, String additionalUrl
, String additionalUserName, String additionalPassword) {
this.dataSourceName = dataSourceName;
this.additionalUrl = additionalUrl;
this.additionalUserName = additionalUserName;
this.additionalPassword = additionalPassword;
}
/ ** setter getter omis * /
/**
* Méthode de détermination de la présence ou de l'absence d'informations complémentaires
*/
public boolean isAdditional() {
return !(Objects.isNull(this.additionalUrl)
&& Objects.isNull(this.additionalUserName)
&& Objects.isNull(this.additionalPassword));
}
}
Créez un contrôleur, un service et un DAO et faites-les réellement fonctionner. Utilisez Propagation.REQUIRES_NEW pour suspendre la transaction en cours et créer une autre transaction pour la propriété Transactional.
Confirmez qu'il est annulé au moment de l'exception avec DemoController # exception
Vérifiez que plusieurs bases de données ont été mises à jour avec DemoController # demo
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.demo.common.DataBaseSelectHolder;
import com.example.demo.service.DemoService;
@Controller
@Transactional(propagation=Propagation.REQUIRES_NEW)
public class DemoController {
@Autowired
private DemoService service;
@RequestMapping("exception")
public String exception() {
DataBaseSelectHolder.setDataBaseInstanceInfo("postgres");
service.insertException();
return "empty";
}
@RequestMapping("demo")
public String demo() {
DataBaseSelectHolder.setDataBaseInstanceInfo("postgres");
service.insert();
return "empty";
}
}
Je ne sais pas s'il s'agit de la spécification de Spring Transactional ou de la spécification d'AOP elle-même car je n'ai pas étudié, mais comme la transaction n'est pas effectuée au moment de l'appel de méthode dans le même cas, la commutation de base de données dans la transaction est la suivante Vous utilisez la méthode. --Transactional est ajouté à la méthode de classe DAO elle-même
package com.example.demo.service;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.common.DataBaseSelectHolder;
import com.example.demo.dao.DemoDAO;
@Service
@Transactional(propagation=Propagation.REQUIRES_NEW)
public class DemoService {
@Autowired
private DemoDAO demoDao;
@Autowired
private DemoService self;
public void insertException() {
// La base de données a été restaurée avec une erreur de duplication de clé
DataBaseSelectHolder.setDataBaseInstanceInfo("database");
List<Map<String, Object>> list = demoDao.selectTransactional();
list.stream().forEach(demoDao::insert);
list.stream().forEach(demoDao::insert);
}
public void insert() {
DataBaseSelectHolder.setDataBaseInstanceInfo("user", "001", null, null);
List<Map<String, Object>> list1 = demoDao.selectTransactional();
list1.stream().forEach(demoDao::insert);
DataBaseSelectHolder.setDataBaseInstanceInfo("user", "002", null, null);
List<Map<String, Object>> list2 = self.getList();
list2.stream().forEach(demoDao::insert);
DataBaseSelectHolder.setDataBaseInstanceInfo("database");
List<Map<String, Object>> list3 = demoDao.select();
self.insertOther(list3);
}
public List<Map<String, Object>> getList() {
return demoDao.select();
}
public void insertOther(List<Map<String, Object>> list) {
list.stream().forEach(demoDao::insert);
}
}
package com.example.demo.dao;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Mapper
public interface DemoDAO {
@Select("SELECT key, value FROM demo")
List<Map<String, Object>> select();
@Select("SELECT key, value FROM demo")
@Transactional(propagation=Propagation.REQUIRES_NEW)
List<Map<String, Object>> selectTransactional();
@Insert("INSERT INTO demo (key, value) VALUES ( #{map.key}, #{map.value})")
int insert(@Param("map")Map<String, Object> map);
@Update("UPDATE demo SET value = #{map.value} WHERE key = #{map.key}")
int update(@Param("map")Map<String, Object> map);
}
3.1 DemoController#exception http://localhost:8080/exception Le résultat SELECT de ~~ user01 est INSERT dans postgres, mais une exception se produit dans le deuxième INSERT et il est annulé. ~~ ** Les données de test sont incorrectes et aucune restauration n'est réellement effectuée. ** **
key | value |
---|---|
key | value |
---|---|
user001 | Utilisateur test 001 |
key | value |
---|---|
key | value |
---|---|
user001 | Utilisateur test 001 |
3.2 DemoController#demo http://localhost:8080/demo INSÉRER les résultats SELECT de user01 et user02 dans postgres. Puis SELECT postgres et INSERT dans la base de données au sein de la même transaction.
key | value |
---|---|
key | value |
---|---|
database | database |
key | value |
---|---|
user001 | Utilisateur test 001 |
key | value |
---|---|
user002 | Utilisateur test 002 |
key | value |
---|---|
user001 | Utilisateur test 001 |
user002 | Utilisateur test 002 |
key | value |
---|---|
database | database |
user001 | Utilisateur test 001 |
user002 | Utilisateur test 002 |
key | value |
---|---|
user001 | Utilisateur test 001 |
key | value |
---|---|
user002 | Utilisateur test 002 |
[Spring Boot] Changement de source de données dynamique
Essayez l'attribut de propagation (propagation) de la transaction (@Transactional) avec Spring Boot
Recommended Posts