【多数据源动态切换】Spring Boot中实现多数据源动态切换效果(1):通过继承AbstractRoutingDataSource类实现

news2024/11/18 19:44:22

在Spring Boot中,可以通过多种方式实现多数据源的动态切换效果,本篇介绍第一种实现方案。

一 AbstractRoutingDataSource

Spring Boot提供了org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 这个类,其中#determineCurrentLookupKey方法可以让用户根据自己定义的规则在某一个SQL执行之前动态地选择想要的数据源。

1

2

3

4

5

6

7

8

9

/**

* Determine the current lookup key. This will typically be

* implemented to check a thread-bound transaction context.

* <p>Allows for arbitrary keys. The returned key needs

* to match the stored lookup key type, as resolved by the

* {@link #resolveSpecifiedLookupKey} method.

*/

@Nullable

protected abstract Object determineCurrentLookupKey();

因此,我们的整体实现逻辑就很清晰了,具体分为以下几个步骤:

  1. 定义DynamicRoutingDataSource类,继承AbstractRoutingDataSource类并实现#determineCurrentLookupKey方法(具体逻辑是从当前线程的ThreadLocal中获取我们在某一个SQL执行之前通过AOP切面动态指定的数据源名称);

  2. application.yml中配置多个数据源;

  3. 解析在application.yml中配置的多个数据源,然后生成DynamicRoutingDataSource实例,并设置默认数据源(defaultTargetDataSource)和其他数据源(targetDataSources);

  4. 调用AbstractRoutingDataSource#getConnection的方法的时候,会先调用#determineTargetDataSource方法获取具体的数据源,而在这个方法中会进一步调用我们在DynamicRoutingDataSource类中自定义的#determineCurrentLookupKey方法,最后在返回DataSource后再进行#getConnection的调用。显然,剩下就是具体的SQL逻辑执行了。

二 具体实现

(1)测试使用的数据库

这里我们创建3个数据库,分别是:db01db02db03,然后这3个数据库都有一张名为user_info的表,表结构一样,只是数据不同。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

-- 建表语句

DROP TABLE IF EXISTS `user_info`;

CREATE TABLE `user_info` (

  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',

  `name` varchar(255) DEFAULT NULL COMMENT '姓名',

  `age` int(11) DEFAULT NULL COMMENT '年龄',

  `addr_city` varchar(255) DEFAULT NULL COMMENT '所在城市',

  `addr_district` varchar(255) DEFAULT NULL COMMENT '所在区',

  PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- db01中表「user_info」的数据

INSERT INTO `user_info` VALUES ('1', '张三', '20', '北京', '朝阳区');

INSERT INTO `user_info` VALUES ('2', '李四', '18', '北京', '东城区');

-- db02中表「user_info」的数据

INSERT INTO `user_info` VALUES ('1', '王五', '22', '上海', '普陀区');

INSERT INTO `user_info` VALUES ('2', '赵六', '24', '上海', '浦东新区');

-- db03中表「user_info」的数据

INSERT INTO `user_info` VALUES ('1', '孙七', '28', '成都', '武侯区');

INSERT INTO `user_info` VALUES ('2', '周八', '26', '成都', '天府新区');

(2)动态切换数据源的上下文

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

package cn.zifangsky.example.webexercise.dataSource;

import lombok.extern.slf4j.Slf4j;

import java.util.List;

import java.util.concurrent.CopyOnWriteArrayList;

/**

* 动态切换数据源的上下文

*

*/

@Slf4j

public class DynamicDataSourceContext {

    /**

     * 所有配置过的数据源的KEY

     */

    public static List<String> dataSourceKeys = new CopyOnWriteArrayList<>();

    /**

     * 默认数据源的KEY

     */

    public static String defaultDataSourceKey;

    /**

     * 当前SQL执行之前,在{@link ThreadLocal}中设置的数据源的KEY

     */

    private static final ThreadLocal<String> RESOURCE = new ThreadLocal<>();

    /**

     * 获取「当前在{@link ThreadLocal}中设置的数据源的KEY」

     */

    public static String getRoutingDataSourceKey(){

        return RESOURCE.get();

    }

    /**

     * 获取「当前在{@link ThreadLocal}中设置的数据源的KEY」

     */

    public static void setRoutingDataSourceKey(String routingDataSourceKey){

        log.debug("切换至「{}」数据源", routingDataSourceKey);

        RESOURCE.set(routingDataSourceKey);

    }

    /**

     * 动态路由完成之后,清空设置的数据源的KEY

     */

    public static void clearRoutingDataSourceKey(){

        RESOURCE.remove();

    }

    /**

     * 添加配置过的数据源的KEY

     */

    public static void addDataSourceKey(String dataSourceKey, boolean ifDefaultDataSourceKey){

        dataSourceKeys.add(dataSourceKey);

        if(ifDefaultDataSourceKey){

            defaultDataSourceKey = dataSourceKey;

        }

    }

    /**

     * 判断是否已经配置某个数据源

     */

    public static boolean containsDataSource(String dataSourceKey){

        return dataSourceKeys.contains(dataSourceKey);

    }

}

(3)定义DynamicRoutingDataSource

主要是继承AbstractRoutingDataSource类并实现#determineCurrentLookupKey方法,其具体逻辑是从当前线程的ThreadLocal中获取我们在某一个SQL执行之前通过AOP切面动态指定的数据源名称。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

package cn.zifangsky.example.webexercise.dataSource;

import lombok.extern.slf4j.Slf4j;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**

* 动态数据源路由配置

*

*/

@Slf4j

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    @Override

    protected Object determineCurrentLookupKey() {

        //如果 currentDataSourceKey 为null,则调用方「#determineTargetDataSource」方法会在当前方法返回null之后自动使用默认数据源

        String currentDataSourceKey = DynamicDataSourceContext.getRoutingDataSourceKey();

        log.debug("当前使用的数据源是:「{}」(这里为null表示使用的是默认数据源)", currentDataSourceKey);

        return currentDataSourceKey;

    }

}

(4)新增application-datasource.yml配置文件

新建这个用于测试的配置文件,主要配置了接下来需要用到的多个数据源,其关键配置如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

spring:

  datasource:

    master:

      type: com.zaxxer.hikari.HikariDataSource

      driver-class-name: com.mysql.jdbc.Driver

      url: jdbc:mysql://localhost:3306/db01?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&failOverReadOnly=false&useSSL=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai

      username: root

      password: root

      # HikariCP 连接池配置

      hikari:

        pool-name: db01_HikariCP

        minimum-idle: 5  #最小空闲连接数量

        idle-timeout: 30000  #空闲连接存活最大时间,默认600000(10分钟)

        maximum-pool-size: 20  #连接池最大连接数,默认是10

        auto-commit: true  #此属性控制从池返回的连接的默认自动提交行为,默认值:true

        max-lifetime: 1800000  #此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟

        connection-timeout: 30000  #数据库连接超时时间,默认30秒,即30000

    cluster:

    - key: db02

      type: com.zaxxer.hikari.HikariDataSource

      driver-class-name: com.mysql.jdbc.Driver

      url: jdbc:mysql://localhost:3306/db02?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&failOverReadOnly=false&useSSL=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai

      username: root

      password: root

      # HikariCP 连接池配置

      hikari:

        pool-name: db02_HikariCP

        minimum-idle: 5  #最小空闲连接数量

        idle-timeout: 30000  #空闲连接存活最大时间,默认600000(10分钟)

        maximum-pool-size: 20  #连接池最大连接数,默认是10

        auto-commit: true  #此属性控制从池返回的连接的默认自动提交行为,默认值:true

        max-lifetime: 1800000  #此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟

        connection-timeout: 30000  #数据库连接超时时间,默认30秒,即30000      

    - key: db03

      type: com.zaxxer.hikari.HikariDataSource

      driver-class-name: com.mysql.jdbc.Driver

      url: jdbc:mysql://localhost:3306/db03?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&failOverReadOnly=false&useSSL=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai

      username: root

      password: root

      # HikariCP 连接池配置

      hikari:

        pool-name: db03_HikariCP

        minimum-idle: 5  #最小空闲连接数量

        idle-timeout: 30000  #空闲连接存活最大时间,默认600000(10分钟)

        maximum-pool-size: 20  #连接池最大连接数,默认是10

        auto-commit: true  #此属性控制从池返回的连接的默认自动提交行为,默认值:true

        max-lifetime: 1800000  #此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟

        connection-timeout: 30000  #数据库连接超时时间,默认30秒,即30000

(5)解析在application-datasource.yml中配置的多个数据源

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

package cn.zifangsky.example.webexercise.dataSource;

import com.zaxxer.hikari.HikariDataSource;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.MutablePropertyValues;

import org.springframework.beans.factory.support.BeanDefinitionRegistry;

import org.springframework.beans.factory.support.GenericBeanDefinition;

import org.springframework.boot.context.properties.bind.Bindable;

import org.springframework.boot.context.properties.bind.Binder;

import org.springframework.boot.context.properties.source.ConfigurationPropertyName;

import org.springframework.boot.context.properties.source.ConfigurationPropertyNameAliases;

import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;

import org.springframework.context.EnvironmentAware;

import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;

import org.springframework.core.env.Environment;

import org.springframework.core.type.AnnotationMetadata;

import org.springframework.util.StringUtils;

import javax.sql.DataSource;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

/**

* 动态数据源注册

* <p>实现{@link [email protected]�</p>

* <p>实现{@link EnvironmentAware}目的是读取 application-datasource.yml 配置</p>

*

*/

@Slf4j

public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {

    /**

     * 别名

     */

    private final static ConfigurationPropertyNameAliases ALIASES = new ConfigurationPropertyNameAliases();

    //由于部分数据源配置不同,所以在此处添加别名,避免切换数据源出现某些参数无法注入的情况

    static {

        ALIASES.addAliases("url", "jdbc-url");

        ALIASES.addAliases("username", "user");

    }

    

    /**

     * 参数绑定工具

     */

    private Binder binder;

    @Override

    public void setEnvironment(Environment environment) {

        this.binder = Binder.get(environment);

    }

    @Override

    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

        log.info("开始注册多个数据源");

        //1. 注册默认数据源

        //1.1 获取默认数据源参数

        Map defaultDataSourceProperties = this.binder.bind("spring.datasource.master", Map.class).get();

        //1.2 获取默认数据源类型

        String defaultDataSourceType = defaultDataSourceProperties.get("type").toString();

        Class<? extends DataSource> defaultDataSourceClazz = this.getDataSourceType(defaultDataSourceType);

        //1.3 绑定默认数据源参数

        DataSource defaultDataSource = this.bind(defaultDataSourceClazz, defaultDataSourceProperties);

        //1.4 将其添加到「动态切换数据源的上下文」中

        DynamicDataSourceContext.addDataSourceKey("master", true);

        log.info("注册默认数据源「master」成功");

        //2. 注册其他数据源

        Map<String, DataSource> targetDataSources = new HashMap<>();

        List<Map> slaveDataSourcePropertiesList = this.binder.bind("spring.datasource.cluster", Bindable.listOf(Map.class)).get();

        if(slaveDataSourcePropertiesList != null && slaveDataSourcePropertiesList.size() > 0){

            //2.1 获取某一数据源参数

            for(Map dataSourceProperties : slaveDataSourcePropertiesList){

                //2.2 获取数据源类型

                String dataSourceType = dataSourceProperties.get("type").toString();

                Class<? extends DataSource> dataSourceClazz = this.getDataSourceType(dataSourceType);

                //2.3 绑定数据源参数

                DataSource dataSource = this.bind(dataSourceClazz, dataSourceProperties);

                //2.4 获取数据源的KEY,并将其添加到「动态切换数据源的上下文」和「targetDataSources」中

                String dataSourceKey = dataSourceProperties.get("key").toString();

                DynamicDataSourceContext.addDataSourceKey(dataSourceKey, false);

                targetDataSources.put(dataSourceKey, dataSource);

                log.info("注册数据源「{}」成功", dataSourceKey);

            }

        }

        //3. 实例化 DynamicRoutingDataSource Bean

        //3.1 bean定义

        GenericBeanDefinition definition = new GenericBeanDefinition();

        definition.setBeanClass(DynamicRoutingDataSource.class);

        //3.2 注入需要的参数

        MutablePropertyValues propertyValues = definition.getPropertyValues();

        propertyValues.add("defaultTargetDataSource", defaultDataSource);

        propertyValues.add("targetDataSources", targetDataSources);

        //3.3 将该bean注册为datasource,不使用springboot自动生成的datasource

        registry.registerBeanDefinition("datasource", definition);

        log.info("注册数据源成功,一共注册「{}」个数据源", targetDataSources.keySet().size() + 1);

    }

    /**

     * 通过数据源字符串获取数据源Class对象

     */

    private Class<? extends DataSource> getDataSourceType(String dataSourceType){

        Class<? extends DataSource> type;

        try {

            if(!StringUtils.isEmpty(dataSourceType)){

                //如果字符串不为空,则通过反射获取class对象

                type = (Class<? extends DataSource>) Class.forName(dataSourceType);

            }else {

                //否则设置为默认的 HikariCP 连接池

                type = HikariDataSource.class;

            }

            return type;

        } catch (Exception e) {

            throw new IllegalArgumentException("can not resolve class with type: " + dataSourceType);

        }

    }

    /**

     * 通过类型绑定参数并获得实例对象

     */

    private <T extends DataSource> T bind(Class<T> clazz, Map properties){

        MapConfigurationPropertySource propertySource = new MapConfigurationPropertySource(properties);

        Binder binder = new Binder(propertySource.withAliases(ALIASES));

        //通过类型绑定参数并获得实例对象

        return binder.bind(ConfigurationPropertyName.EMPTY, Bindable.of(clazz)).get();

    }

}

(6)通过AOP+注解实现数据源的动态切换

i)在pom.xml文件中添加切面需要的依赖:

1

2

3

4

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-aop</artifactId>

</dependency>

ii)新增一个用于切换数据源的注解:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

package cn.zifangsky.example.webexercise.dataSource;

import java.lang.annotation.*;

/**

* 用于动态切换数据源的注解(优先级:方法级别 > 类级别)

*/

@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface DataSource {

    String value() default "master";

}

iii)定义一个AOP的通知类:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

package cn.zifangsky.example.webexercise.dataSource;

import lombok.extern.slf4j.Slf4j;

import org.aopalliance.intercept.MethodInterceptor;

import org.aopalliance.intercept.MethodInvocation;

import org.springframework.core.annotation.AnnotationUtils;

import java.lang.reflect.Method;

import java.util.Map;

import java.util.concurrent.ConcurrentHashMap;

/**

* 基于注解拦截后的通知

*

*/

@Slf4j

public class AnnotationInterceptor implements MethodInterceptor {

    /**

     * 缓存方法对应的注解值

     */

    private Map<Method, DataSource> methodCacheMap = new ConcurrentHashMap<>();

    @Override

    public Object invoke(MethodInvocation methodInvocation) throws Throwable {

        try {

            String dataSource = this.determineDataSource(methodInvocation);

            if(dataSource == null || !DynamicDataSourceContext.containsDataSource(dataSource)){

                String defaultDataSource = DynamicDataSourceContext.defaultDataSourceKey;

                log.error("数据源「{}」不存在,即将使用默认数据源「{}」。", dataSource, defaultDataSource);

                dataSource = defaultDataSource;

            }

            DynamicDataSourceContext.setRoutingDataSourceKey(dataSource);

            return methodInvocation.proceed();

        }finally {

            DynamicDataSourceContext.clearRoutingDataSourceKey();

        }

    }

    /**

     * 获取用于动态切换数据源的注解

     */

    private String determineDataSource(MethodInvocation invocation){

        Method method = invocation.getMethod();

        if(this.methodCacheMap.containsKey(method)){

            return this.methodCacheMap.get(method).value();

        }else{

            DataSource dataSource = method.isAnnotationPresent(DataSource.class) ?

                    //从当前方法获取 DataSource 注解

                    method.getAnnotation(DataSource.class) :

                    //如果获取不到,则尝试从当前方法所在类或者接口级别获取

                    AnnotationUtils.findAnnotation(method.getDeclaringClass(), DataSource.class);

            this.methodCacheMap.put(method, dataSource);

            return (dataSource != null) ? dataSource.value() : null;

        }

    }

}

iv)定义一个Advisor,将通知注入到指定的切点:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

package cn.zifangsky.example.webexercise.dataSource;

import org.aopalliance.aop.Advice;

import org.springframework.aop.Pointcut;

import org.springframework.aop.support.AbstractPointcutAdvisor;

import org.springframework.aop.support.ComposablePointcut;

import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;

/**

* 将通知注入到指定的切点

*

*/

public class AnnotationAdvisor extends AbstractPointcutAdvisor {

    private Advice advice;

    private Pointcut pointcut;

    public AnnotationAdvisor(AnnotationInterceptor annotationInterceptor) {

        this.advice = annotationInterceptor;

        this.pointcut = this.buildPointcut();

    }

    @Override

    public Advice getAdvice() {

        return this.advice;

    }

    @Override

    public Pointcut getPointcut() {

        return this.pointcut;

    }

    /**

     * 定义AOP的切点范围

     */

    private Pointcut buildPointcut(){

        //类注解,包括超类和接口

        Pointcut classPointcut = new AnnotationMatchingPointcut(DataSource.class, true);

        //方法注解

        Pointcut methodPointcut = AnnotationMatchingPointcut.forMethodAnnotation(DataSource.class);

        return new ComposablePointcut(classPointcut).union(methodPointcut);

    }

}

v)导入上面的数据源配置,以及启动切面:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

package cn.zifangsky.example.webexercise.config;

import cn.zifangsky.example.webexercise.dataSource.AnnotationAdvisor;

import cn.zifangsky.example.webexercise.dataSource.AnnotationInterceptor;

import cn.zifangsky.example.webexercise.dataSource.DynamicDataSourceRegister;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Import;

/**

* 动态切换数据源配置

*

*/

@Import(DynamicDataSourceRegister.class)

@Configuration

public class DynamicDataSourceConfig {

    @Bean

    public AnnotationAdvisor annotationAdvisor(){

        return new AnnotationAdvisor(new AnnotationInterceptor());

    }

}

三 效果测试

(1)新建两个测试使用的Mapper

UserInfoMapper为插件自动生成,没有添加我们自定义的@DataSource注解,用于测试不添加注解的情况下默认使用的数据源。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

package cn.zifangsky.example.webexercise.mapper;

import cn.zifangsky.example.webexercise.model.UserInfo;

public interface UserInfoMapper {

    int deleteByPrimaryKey(Integer id);

    int insert(UserInfo record);

    int insertSelective(UserInfo record);

    UserInfo selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(UserInfo record);

    int updateByPrimaryKey(UserInfo record);

}

UserInfoDynamicMapper为手动新建的几个方法,用于测试数据源的动态切换效果。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

package cn.zifangsky.example.webexercise.mapper;

import cn.zifangsky.example.webexercise.dataSource.DataSource;

import cn.zifangsky.example.webexercise.model.UserInfo;

import org.apache.ibatis.annotations.Param;

@DataSource("db02")

public interface UserInfoDynamicMapper {

    /**

     * 通过默认数据源查询,方法级别的注解优先级更高

     */

    @DataSource

    UserInfo selectByDefaultDataSource(Integer id);

    /**

     * 方法级别没有添加注解,则使用接口级别的注解,通过 db02 数据源查询

     */

    UserInfo selectByDB02DataSource(Integer id);

    /**

     * 通过 db03 数据源查询

     */

    @DataSource("db03")

    UserInfo selectByDB03DataSource(Integer id);

    /**

     * 测试事务是否回滚(数据插入 db02 数据源)

     */

    @DataSource("db02")

    int addToDB02(UserInfo record);

    /**

     * 测试事务是否回滚(数据插入 db03 数据源)

     */

    @DataSource("db03")

    int addToDB03(UserInfo record);

    /**

     * 从 db03 数据源删除数据

     */

    @DataSource("db03")

    int deleteFromDB03ByName(@Param("name") String name);

}

其对应的UserInfoDynamicMapper.xml文件是:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="cn.zifangsky.example.webexercise.mapper.UserInfoDynamicMapper">

  <resultMap id="BaseResultMap" type="cn.zifangsky.example.webexercise.model.UserInfo">

    <id column="id" jdbcType="INTEGER" property="id" />

    <result column="name" jdbcType="VARCHAR" property="name" />

    <result column="age" jdbcType="INTEGER" property="age" />

    <result column="addr_city" jdbcType="VARCHAR" property="addrCity" />

    <result column="addr_district" jdbcType="VARCHAR" property="addrDistrict" />

  </resultMap>

  <sql id="Base_Column_List">

    id, `name`, age, addr_city, addr_district

  </sql>

  <select id="selectByDefaultDataSource" parameterType="java.lang.Integer" resultMap="BaseResultMap">

    select

    <include refid="Base_Column_List" />

    from user_info

    where id = #{id,jdbcType=INTEGER}

  </select>

  <select id="selectByDB02DataSource" parameterType="java.lang.Integer" resultMap="BaseResultMap">

    select

    <include refid="Base_Column_List" />

    from user_info

    where id = #{id,jdbcType=INTEGER}

  </select>

  <select id="selectByDB03DataSource" parameterType="java.lang.Integer" resultMap="BaseResultMap">

    select

    <include refid="Base_Column_List" />

    from user_info

    where id = #{id,jdbcType=INTEGER}

  </select>

  <insert id="addToDB02" keyColumn="id" keyProperty="id" parameterType="cn.zifangsky.example.webexercise.model.UserInfo" useGeneratedKeys="true">

    insert into user_info (`name`, age, addr_city,

      addr_district)

    values (#{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER}, #{addrCity,jdbcType=VARCHAR},

      #{addrDistrict,jdbcType=VARCHAR})

  </insert>

  <insert id="addToDB03" keyColumn="id" keyProperty="id" parameterType="cn.zifangsky.example.webexercise.model.UserInfo" useGeneratedKeys="true">

    insert into user_info (`name`, age, addr_city,

      addr_district)

    values (#{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER}, #{addrCity,jdbcType=VARCHAR},

      #{addrDistrict,jdbcType=VARCHAR})

  </insert>

  <delete id="deleteFromDB03ByName" parameterType="java.lang.String">

    delete from user_info

    where name = #{name,jdbcType=VARCHAR}

  </delete>

</mapper>

(2)使用单元测试测试「动态切换数据源」的效果

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

package cn.zifangsky.example.webexercise.dataSource;

import cn.zifangsky.example.webexercise.mapper.UserInfoDynamicMapper;

import cn.zifangsky.example.webexercise.mapper.UserInfoMapper;

import cn.zifangsky.example.webexercise.model.UserInfo;

import org.junit.jupiter.api.*;

import org.junit.jupiter.api.extension.ExtendWith;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.context.junit.jupiter.SpringExtension;

import org.springframework.transaction.annotation.Transactional;

import java.sql.SQLException;

/**

* 测试动态切换数据源

*

*/

@DisplayName("测试动态切换数据源")

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)

@ExtendWith(SpringExtension.class)

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

public class DynamicDataSourceTest {

    @Autowired

    private UserInfoMapper userInfoMapper;

    @Autowired

    private UserInfoDynamicMapper userInfoDynamicMapper;

    @Test

    @Order(1)

    @DisplayName("普通方法——使用默认数据源")

    public void testCommonMethod(){

        UserInfo userInfo = userInfoMapper.selectByPrimaryKey(1);

        Assertions.assertNotNull(userInfo);

        Assertions.assertEquals("张三", userInfo.getName());

    }

    @Test

    @Order(2)

    @DisplayName("通过默认数据源查询,方法级别的注解优先级更高")

    public void testSelectByDefaultDataSource(){

        UserInfo userInfo = userInfoDynamicMapper.selectByDefaultDataSource(1);

        Assertions.assertNotNull(userInfo);

        Assertions.assertEquals("张三", userInfo.getName());

    }

    @Test

    @Order(3)

    @DisplayName("方法级别没有添加注解,则使用接口级别的注解,通过 db02 数据源查询")

    public void testSelectByDB02DataSource(){

        UserInfo userInfo = userInfoDynamicMapper.selectByDB02DataSource(1);

        Assertions.assertNotNull(userInfo);

        Assertions.assertEquals("王五", userInfo.getName());

    }

    @Test

    @Order(4)

    @DisplayName("方法级别添加注解,手动指定通过 db03 数据源查询")

    public void testSelectByDB03DataSource(){

        UserInfo userInfo = userInfoDynamicMapper.selectByDB03DataSource(1);

        Assertions.assertNotNull(userInfo);

        Assertions.assertEquals("孙七", userInfo.getName());

    }

    @Test

    @Order(5)

    @DisplayName("在一个方法执行过程中嵌套操作多个数据源的情况")

    public void testNestedMultiDataSource(){

        //1. 从 db02 查询一条数据

        UserInfo userInfo = userInfoDynamicMapper.selectByDB02DataSource(1);

        //2. 插入到 db03

        userInfo.setId(null);

        userInfoDynamicMapper.addToDB03(userInfo);

    }

    @Test

    @Order(6)

    @DisplayName("从 db03 数据源删除数据")

    public void testDeleteFromDB03ByName(){

        userInfoDynamicMapper.deleteFromDB03ByName("王五");

    }

    @Test

    @Order(7)

    @DisplayName("嵌套多个数据源的事务回滚情况")

    @Transactional(rollbackFor = Exception.class)

    public void testTransaction() throws SQLException {

        //1. 从 db01 查询一条数据

        UserInfo userInfo = userInfoDynamicMapper.selectByDefaultDataSource(1);

        //2. 分别插入到 db02 和 db03

        userInfo.setId(null);

        userInfoDynamicMapper.addToDB02(userInfo);

        userInfoDynamicMapper.addToDB03(userInfo);

        //3. 手动抛出一个异常,测试事务回滚效果

        throw new SQLException("SQL执行过程中发生某些未知异常");

    }

}

注:以上测试代码基于Junit5 测试框架编写,需要的依赖如下:

1

2

3

4

5

6

7

8

9

10

11

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-test</artifactId>

    <scope>test</scope>

    <exclusions>

        <exclusion>

            <groupId>org.junit.vintage</groupId>

            <artifactId>junit-vintage-engine</artifactId>

        </exclusion>

    </exclusions>

</dependency>

运行单元测试后,测试效果如下:

 

可以发现,除了最后手动抛出一个异常的方法,其他几个方法都测试通过了。然后,通过查询数据库中的数据还可以发现,事务做了我们预期效果的回滚,因此本篇文章介绍的「多数据源动态切换」方案是可行的。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/81153.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java校园花卉销售系统ef5ox(1)

做毕业设计一定要选好题目。毕设想简单&#xff0c;其实很简单。这里给几点建议&#xff1a; 1&#xff1a;首先&#xff0c;学会收集整理&#xff0c;年年专业都一样&#xff0c;岁岁毕业人不同。很多人在做毕业设计的时候&#xff0c;都犯了一个错误&#xff0c;那就是不借鉴…

redis之作为分布式锁使用

写在前面 本文一起看下redis作为分布式锁使用的相关内容。 1&#xff1a;怎么算是锁或没锁 锁和没锁本身其实就是用一个变量的值来表示&#xff0c;比如变量lock&#xff0c;当值为1时代表处于上锁状态&#xff0c;当值为0时表示没有锁&#xff0c;那么多线程想要获取锁的话…

【网络工程】8、实操-万达酒店综合项目(二)

接上篇《7、实操-万达酒店综合项目&#xff08;一&#xff09;》 之前我们讲解了酒店网络项目的整体需求文档&#xff0c;包括项目背景、总体架构设计以及网络功能域划分、配置标准、路由规划等。本篇我们就来按照项目要求进行模拟拓扑的构建实操。 一、总体架构效果 按照之前…

力扣 1827. 最少操作使数组递增

题目 给你一个整数数组 nums &#xff08;下标从 0 开始&#xff09;。每一次操作中&#xff0c;你可以选择数组中一个元素&#xff0c;并将它增加 1 。 比方说&#xff0c;如果 nums [1,2,3] &#xff0c;你可以选择增加 nums[1] 得到 nums [1,3,3] 。 请你返回使 nums 严…

2.10.0 以太网 传统STP生成树简介、STP工作方式简单介绍

2.10.0 以太网 传统STP生成树&#xff08;简介、工作方式&#xff09; 作用&#xff1a; STP&#xff08;Spanning Tree Prortoco&#xff09;生成树协议&#xff0c;它的出现解决了交换机网络环路的问题。 交换机网络中收到BUM帧的时候&#xff0c;将会进行泛洪的操作&…

HCIA 以太网的帧

一、前言 网络经常会遇到一些词汇&#xff0c;比如包、报文、帧&#xff0c;其概念如下&#xff1a; 1&#xff09;帧&#xff08;frame&#xff09;&#xff1a;数据链路层的协议数据单元&#xff0c;它包括三部分&#xff1a;帧头、数据部分、帧尾。其中&#xff0c;帧头和…

vector容器模拟实现(上)

1.1:模板设置 因为不能与库已经有的vector冲突&#xff0c;首先定义命名空间namespace my 使用类模板。 1.2:成员变量 vector有size&#xff0c;capacity&#xff0c;同时也要进行扩容操作和缩容&#xff08;shrink to fit&#xff09;操作&#xff0c;所以需要3个迭代器。 p…

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java杨柳农贸市场摊位管理系统qr3ri

大四计算机专业的同学们即将面临大学4年的最后一次考验--毕业设计。通过完成毕业设计来对过去4年的大学学习生活做一个总结&#xff0c;也是检验我们学习成果的一种方式&#xff0c;毕业设计作品也是我们将来面试找工作的一个敲门砖。 选题前先看看自己掌握哪些技术点、擅长哪…

mysql首次安装error 1045 28000解决

mysql首次安装error 1045 28000解决 首先找到了这个教程&#xff0c;按其操作一步一步来&#xff0c;发现到登录输入密码这一步&#xff0c;怎么样都报错 2021MySql-8.0.26安装详细教程&#xff08;保姆级&#xff09; 然后按照这篇操作&#xff0c;但还是不行 MySQL安装1045错…

[附源码]Python计算机毕业设计Djangoospringboot作业管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

通过窗口看世界之 界面制作如此简单-带你制作旅游观光系统界面-----效果图展示

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 秩沅 原创 收录于专栏 玩归玩闹归闹&#xff0c;别拿java开玩笑 ⭐旅游管理系统部分效果图展示⭐ 文章目录⭐旅游管理系统部分效果图…

持久化的配置都不知道,也敢说精通Redis?

前言 所谓持久化可以简单理解为将内存中的数据保存到硬盘上存储的过程。持久化之后的数据在系统重启或者宕机之后依然可以进行访问&#xff0c;保证了数据的安全性。一键获取最先java文档。 Redis有两种持久化方案&#xff0c;一种是快照方式&#xff08;SNAPSHOTTING&#xff…

前端 | 前端设计设计及代码收集

全部来源于Github,侵权删&#xff01; 1.Maxime Euzire 2.Shubham Singh | Resume 3.Natasha Theodora Petrus 4.Ana Mara Martnez Gmez 5.bagussona.github.io 6.leodevbro.github.io 7.yuanlinping.github.io 8.Dr.Q 9.sureshmurali.github.io 10.Chandrika Deb | Software…

常见的接口测试面试题

1、按你的理解&#xff0c;软件接口是什么&#xff1f; 答&#xff1a; 就是指程序中具体负责在不同模块之间传输或接受数据的并做处理的类或者函数。 2、HTTP和HTTPS协议区别&#xff1f; 答&#xff1a; https协议需要到CA&#xff08;Certificate Authority&#xff0c;证…

[附源码]JAVA毕业设计医疗器械销售电子商城(系统+LW)

[附源码]JAVA毕业设计医疗器械销售电子商城&#xff08;系统LW&#xff09; 项目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目…

Spring:Ioc

目录 一、Spring的两大核心机制 1、IOC/ DI:控制反转和依赖注入 2、AOP:面向切面编程 二、IOC/ DI:控制反转 / 依赖注入&#xff1a; 1、bean的基础配置&#xff1a; 2、bean的实例化 3、配置bean 4、依赖注入方式 三、注解开发定义bean 注解依赖注入 Spring设计理念 …

Spring推断构造方法源码深度解析

文章目录前言思考目标一、bean的实例化入口-createBeanInstance1、源码逻辑思路**核心点&#xff0c;主要做了几件事情**2、instantiateBean-默认无参实例化3、instantiate-实例化4、instantiateClass-实例化类二、获取构造函数候选数组-determineConstructorsFromBeanPostProc…

这十套练习,教你如何用Pandas做数据分析(04)

练习4-Apply函数 探索1960 - 2014 美国犯罪数据 步骤1 导入必要的库 运行以下代码 import numpy as np import pandas as pd 步骤2 从以下地址导入数据集 运行以下代码 path4 ‘…/input/pandas_exercise/pandas_exercise/exercise_data/US_Crime_Rates_1960_2014.csv’…

微信小程序|入门进阶

接下来点击新建项目就可以在主界面中预览到我们的豆瓣电影示例了 小程序开发初体验 Hello world 希望是一个从零到一的转换过程~ 创建项目 接下来创建一个新的项目,理解小程序项目的基本结构和一些基础语法。 官方的开发工具为此准备了一个QuickStart项目。在创建过程中,…

利用jrebel与arthas在centos7 springboot热部署

jrebel 热部署 jrebel在本地是可以class xml一起热部署&#xff0c;但是远程热部署xml不行&#xff0c;所以用arthas代替去热部署xml 1.jrebel 反向代理 因为jrebel是收费插件&#xff0c;所以要高一些小动作咱们才能‘正常’使用&#xff0c;当然你也可以拿别人代理好的操作…