要求:

在Spring Boot中可能会遇到多个数据库的时候,这应该如何配置呢?我的核心是要求无论如何配置都不允许影响我第一个数据库的默认相关配置及代码的写法,并且保证事务的正常执行。

代码:

1、我们先正常配置第一个数据库。

建立项目

选则相关依赖,这里我们用jdbcTemplate来测试,第一数据库用mysql,第二个数据库用oracle。

2、修改数据库配置文件

        我比较喜欢用yml配置文件,所以就将application.properties更名为application.yml。这里一口气将2个数据库都配置完全。

spring:
    datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/test
        username: root
        password: mysql

second:
    datasource:
        driver-class-name: oracle.jdbc.driver.OracleDriver
        url: jdbc:oracle:thin:localhost:1521:orcl
        username: system
        password: manager

这里的 spring: 就是第一个数据库配置,默认大家都这么配置,没什么好说的了。而 second: 就是自己定义的第二个数据库配置了。在网上查资料时有很多配置多数据源都得把 url: 改为 jdbc-url: ,但我不想这么改,我必须保证第一个数据库不受第二个数据库的影响。

3、封装 jdbcTemplate 。

        基本上大家用 jdbcTemplate 时都会对其进行封装,很少有拿来直接用的。我这里也进行一下封装,主要是增加 分页查询 功能。

package com.example.databasetest.tools;

import java.util.Map;
import java.util.HashMap;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;

/**
 * 功能:JdbcTemplate封装类<br>
 * 作者:我是小木鱼(Lag)<br>
 */
public class JdbcTemplatePackage extends JdbcTemplate
{
    /** 数据库类型(ORACLE,MYSQL,DB2)*/
    private String databaseType = "MYSQL";

    public JdbcTemplatePackage(){}

    /**
     * 功能:初始化需要注入数据源<br>
     * 备注:非常重要<br>
     */
    public JdbcTemplatePackage(DataSource dataSource){super(dataSource);}

    /**
     * 功能:执行SQL查询语句并将结果以Map对象返回。
     * @param sql 查询语句
     * @return 仅返回符合条件的第一条数据,无则返回空值。
     */
    @Override
    public Map<String,Object> queryForMap(String sql)
    {
        return this.queryForMap(sql,null);
    }

    /**
     * 功能:执行SQL查询语句并将结果以Map对象返回。
     * @param sql 查询语句
     * @param args 查询参数
     * @return 仅返回符合条件的第一条数据,无则返回空值。
     */
    @Override
    public Map<String,Object> queryForMap(String sql, Object[] args)
    {
        List<Map<String,Object>> list = this.queryForList(sql,args);
        if(list.size() > 0)
        {
            return list.get(0);
        }
        else
        {
            return null;
        }
    }

    /**
     * 功能:执行SQL查询语句并将数据总数以int对象返回。
     * @param sql 查询语句
     * @return -1-语句出错,0-未查询到符合条件的数据,>0-已查询到符合条件的数据。
     */
    public int queryForInt(String sql)
    {
        return this.queryForInt(sql,null);
    }

    /**
     * 功能:执行SQL查询语句并将数据总数以int对象返回。
     * @param sql 查询语句
     * @param args 查询参数
     * @return -1-语句出错,0-未查询到符合条件的数据,>0-已查询到符合条件的数据。
     */
    public int queryForInt(String sql, Object[] args)
    {
        int rowCount = -1;

        Map<String,Object> map = this.queryForMap(sql,args);
        if(map != null)
        {
            //遍历取值
            for (Map.Entry<String, Object> stringObjectEntry : map.entrySet())
            {
                rowCount = Integer.parseInt(stringObjectEntry.getValue().toString());
            }
        }

        return rowCount;
    }

    /**
     * 功能:分页执行SQL查询语句
     * @param sql 查询语句
     * @param pageNo 查询页码
     * @param pageSize 每页行数
     * @return Map对象{"rowCount" :总行数 ,"pageCount" :总页数 ,"queryData" :查询数据列表}
     */
    public Map<String,Object> queryForPage(String sql, int pageNo, int pageSize)
    {
        return this.queryForPage(sql,null,pageNo,pageSize);
    }

    /**
     * 功能:分页执行SQL查询语句
     * @param sql 查询语句
     * @param args 查询参数
     * @param pageNo 查询页码
     * @param pageSize 每页行数
     * @return Map对象{"rowCount" :总行数 ,"pageCount" :总页数 ,"queryData" :查询数据列表}
     */
    public Map<String,Object> queryForPage(String sql, Object[] args, int pageNo, int pageSize)
    {
        int rowCount;					//总行数
        int pageCount;					//总页数
        int rowBegin;					//起始行
        int rowEnd;						//截止行
        Map<String,Object> rtnMap = new HashMap<>();

        //数据库类型不允许为空
        if("".equals(this.databaseType)){throw new RuntimeException("数据库类型为空,请用setDatabaseType(...)函数添加数据库类型!");}

        //得到总行数
        rowCount = this.queryForInt("select count(*) from ("+sql+") tempTableForCount",args);

        if(rowCount < 1)   //没有数据直接返回
        {
            rtnMap.put("rowCount",rowCount);
            rtnMap.put("pageCount",0);
            rtnMap.put("queryData",null);
            return rtnMap;
        }

        //得到总页数
        if(rowCount % pageSize == 0)
        {
            pageCount = rowCount / pageSize;
        }
        else
        {
            pageCount = rowCount / pageSize + 1;
        }

        //检验当前页码
        if(pageNo < 1)
        {
            pageNo = 1;
        }
        else if(pageNo > pageCount)
        {
            pageNo = pageCount;
        }

        //计算起始行与截止行
        rowBegin = (pageNo - 1) * pageSize + 1;
        rowEnd = pageNo * pageSize;
        if(rowEnd > rowCount){rowEnd = rowCount;}

        //重新生成SQL语句
        if("ORACLE".equals(databaseType))
        {
            sql = "select * from (select rownum myrownum,a.* from ("+sql+") a where rownum <= "+ rowEnd +") tempTableForPage where myrownum >= "+ rowBegin;		//虚拟顺序号rownum变成真实顺序号rownum
        }
        else if("MYSQL".equals(databaseType) || "MARIADB".equals(databaseType))
        {
            sql = "select * from ("+sql+") tempTableForPage limit "+ (rowBegin - 1) +","+ pageSize +"";
        }
        else if("DB2".equals(databaseType))
        {
            sql = "select * from (select rownumber() over() as myrownum,a.* from ("+sql+") a) tempTableForPage where myrownum between "+ rowBegin +" and "+ rowEnd;
        }
        else
        {
            throw new RuntimeException("这取得的数据库类型["+this.databaseType+"]我也找不到啊!");
        }

        rtnMap.put("rowCount",rowCount);
        rtnMap.put("pageCount",pageCount);
        rtnMap.put("queryData",this.queryForList(sql,args));

        return rtnMap;
    }

    /**
     * 功能:设置数据库类型<br>
     * @param databaseType(ORACLE,MYSQL,DB2)
     */
    public void setDatabaseType(String databaseType)
    {
        this.databaseType = databaseType;
    }

}

解析JdbcTemplatePackage类:

1)无参的构造函数与带DataSource参数的构造函数必须要有啊,并且 super(dataSource); 别忘了写,这没什么好多说的了,大家都知道!

2)调用 queryForPage(...) 查询分页函数时由于各个数据库语法不一样,所以我需要知道是哪个数据库调用的。因此我增加了变量

/** 数据库类型(ORACLE,MYSQL,DB2)*/
private String databaseType = "MYSQL";

的引用,默认是mysql数据库,装配第一个与第二个数据库Bean时要提供该参数值。

3)JdbcTemplatePackage类就是一个普通类,没有用@Component注解装配成Bean

4、生成第一个数据库的Bean。

package com.example.databasetest.tools;

import org.springframework.stereotype.Component;
import javax.sql.DataSource;

@Component
public class MyJdbcTemplate extends JdbcTemplatePackage
{
    public MyJdbcTemplate(DataSource dataSource)
    {
        super(dataSource);
        super.setDatabaseType("MYSQL");
    }

}

解析 MyJdbcTemplate 类:

1)该类继承自 JdbcTemplatePackage,构造函数添加了默认数据源,并设置成 mysql 数据库。

2)该类使用了@Component注解,生成了Bean。用户可以正常是调用该类的Bean来操作数据库的。

3)若有多个数据库的话在这个类中还要加事务管理器TransactionManager,这个等后面再加。

5、测试第一个数据库的Bean。

(1)我们先看一下Spring Boot是否给我们自动装配了MyJdbcTemplate这个Bean。

修改 启动类DatabaseTestApplication。

package com.example.databasetest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class DatabaseTestApplication
{

    public static void main(String[] args)
    {
//        SpringApplication.run(DatabaseTestApplication.class, args);

        //看看spring boot给我们都自动装配了哪些bean
        ConfigurableApplicationContext run = SpringApplication.run(DatabaseTestApplication.class, args);
        String[] names = run.getBeanDefinitionNames();
        for(String name: names)
        {
            System.out.println("===Bean:"+name);
        }

        System.out.println("====================我是快乐的分割线===================");


    }

}

运行该类看看结果

看看,给我自动装配了吧,哈哈!

(2)第一个数据库中表 t_001 数据

(3)增加测试网页类。

package com.example.databasetest.test;

import java.util.Map;
import java.util.List;
import com.example.databasetest.tools.MyJdbcTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Test
{
    @Autowired
    MyJdbcTemplate myJT;

    @RequestMapping("/db1")
    public String say()
    {
        //分页查询t_001表数据,第2页,每页显示2条数据,该函数返回Map对象,其中的key包括总行数,总页数与具体的查询数据。
        Map mapQry = this.myJT.queryForPage("select * from t_001", 2, 2);
        List list = (List)mapQry.get("queryData");  //得到Map中的具体查询数据

        return "我是第一数据库的测试方法:" + list.toString();
    }


}

解析 Test 类:

1)@RestController 该注解不解释,没意思。

2)@Autowired MyJdbcTemplate myJT; 我们要使用MyJdbcTemplate这个Bean了。@Autowired是根据类型来查找该Bean的。与@Resource不同哦。

(4)看看返回结果。

 很成功,5条数据返回2条,确实分页了。

(5)增加事务

package com.example.databasetest.test;

import java.util.Map;
import java.util.List;
import com.example.databasetest.tools.MyJdbcTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Test
{
    @Autowired MyJdbcTemplate myJT;

    @RequestMapping("/db1")
    @Transactional
    public String say()
    {
        //分页查询t_001表数据,第2页,每页显示2条数据,该函数返回Map对象,其中的key包括总行数,总页数与具体的查询数据。
        Map mapQry = this.myJT.queryForPage("select * from t_001", 2, 2);
        List list = (List)mapQry.get("queryData");  //得到Map中的具体查询数据
        System.out.println("我是第一数据库的测试方法:"+list.toString());

        //事务
        this.myJT.update("insert into t_001 (c01,c02,c03) values ('999','国王',100)");
        //故意抛出异常
        int i = 1/0;

        return "我是第一数据库的测试方法:" + list.toString();
    }


}

这里我们增加事务注解 @Transactional,它会回滚RuntimeException异常。我故意抛出异常,查看结果。

数据未变化。到此第一个数据库Bean测试完毕! 

6、建立第二个数据库的Bean,重点到了啊。

package com.example.databasetest.tools;

import javax.sql.DataSource;
import javax.annotation.PostConstruct;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class MyJdbcTemplate2 extends JdbcTemplatePackage
{
    @Value("${second.datasource.driver-class-name}")
    private String driverClassName ;

    @Value("${second.datasource.url}")
    private String url ;

    @Value("${second.datasource.username}")
    private String userName ;

    @Value("${second.datasource.password}")
    private String password ;

    @PostConstruct
    public void init()
    {
        System.out.println("=====@PostConstruct===========================");
        HikariConfig config = new HikariConfig();
        config.setDriverClassName(driverClassName);
        config.setJdbcUrl(url);
        config.setUsername(userName);
        config.setPassword(password);
        DataSource ds = new HikariDataSource(config);
        super.setDataSource(ds);
        super.setDatabaseType("ORACLE");
    }

}

解析 MyJdbcTemplate2 类:

1)该类继续自 JdbcTemplatePackage。

2)利用 @Value 注解取得配置文件application.yml中第二个数据库的参数。

3)我们要利用这些参数生成新的数据源DataSource,并赋值给父类。但有个很重要的问题啊!@Value默认是在生成Bean之后生效,在构造函数中是取不到的(也不是完全取不到,将其作为参数即可,但太麻烦了,又不通用),因此我们使用了@PostConstruct注解,该注解在spring容器启动的时候就执行了。因此在第二个数据库Bean生成之前就执行带@PostConstruct注解的函数,生成了第二个数据库源并赋值给父类替换默认数据源。

4)使用@Component注解将该类注册Bean。

让我们启动Spring Boot看看Bean的加载。

看到了吧,先加载@PostConstruct,然后再生成MyJdbcTemplate与MyJdbcTemplate2这两个数据库的Bean。

7、测试第二个数据库的Bean

(1)让我们看看第二个数据表

(2)修改测试网页

package com.example.databasetest.test;

import java.util.Map;
import java.util.List;
import com.example.databasetest.tools.MyJdbcTemplate;
import com.example.databasetest.tools.MyJdbcTemplate2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Test
{
    @Autowired
    MyJdbcTemplate myJT;

    @Autowired
    MyJdbcTemplate2 myJT2;

    @RequestMapping("/db1")
    @Transactional
    public String say()
    {
        //分页查询t_001表数据,第2页,每页显示2条数据,该函数返回Map对象,其中的key包括总行数,总页数与具体的查询数据。
        Map mapQry = this.myJT.queryForPage("select * from t_001", 2, 2);
        List list = (List)mapQry.get("queryData");  //得到Map中的具体查询数据
        System.out.println("我是第一数据库的测试方法:"+list.toString());

        //事务
        this.myJT.update("insert into t_001 (c01,c02,c03) values ('999','国王',100)");
        //故意抛出异常
        //int i = 1/0;

        return "我是第一数据库的测试方法:" + list.toString();
    }

    @RequestMapping("/db2")
    public String say2()
    {
        //分页查询t_test表数据,第2页,每页显示2条数据,该函数返回Map对象,其中的key包括总行数,总页数与具体的查询数据。
        Map mapQry = this.myJT2.queryForPage("select * from t_test", 2, 2);
        List list = (List)mapQry.get("queryData");  //得到Map中的具体查询数据
        System.out.println("我是第二数据库的测试方法:"+list.toString());

        return "我是第二数据库的测试方法:" + list.toString();
    }
}

解析 Test 类:

1)增加 @Autowired MyJdbcTemplate2 myJT2; 第二个数据库的Bean。

2)增加新函数say2()

@RequestMapping("/db2")
public String say2()
{
    //分页查询t_test表数据,第2页,每页显示2条数据,该函数返回Map对象,其中的key包括总行数,总页数与具体的查询数据。
    Map mapQry = this.myJT2.queryForPage("select * from t_test", 2, 2);
    List list = (List)mapQry.get("queryData");  //得到Map中的具体查询数据
    System.out.println("我是第二数据库的测试方法:"+list.toString());

    return "我是第二数据库的测试方法:" + list.toString();
}

3)我将第一个数据库Bean测试函数say()中的异常注释掉,一会两个数据库一起测试。

        //事务
        this.myJT.update("insert into t_001 (c01,c02,c03) values ('999','国王',100)");
        //故意抛出异常
//        int i = 1/0;

开始测试

先测试第一个数据库

http://localhost:8080/db1

数据插入成功,因为我把异常给注释了。

再测试第二个数据库Bean

http://localhost:8080/db2

 

 成功了啊!

8、还有最后二项了,生成数据库Bean的事务。

说明:我要为第二个数据库Bean生成事务管理器,因为这样的话系统就会有2个事务管理器了,第一个数据库Bean的@Transactional就不知道该用哪个了,因此我们需要用@Primary告诉一下它。

修改一下 MyJdbcTemplate 类,为其增加默认事务管理器

package com.example.databasetest.tools;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Component
public class MyJdbcTemplate extends JdbcTemplatePackage
{
    public MyJdbcTemplate(DataSource dataSource)
    {
        super(dataSource);
        super.setDatabaseType("MYSQL");
    }

    @Bean
    @Primary
    public PlatformTransactionManager oneManager(){
        System.out.println("=====这是第1个数据库事务===");
        return new DataSourceTransactionManager(this.getDataSource());
    }

}

为该类新增加了事务管理器,并设为默认。

再为类 MyJdbcTemplate2 增加事务管理器

package com.example.databasetest.tools;

import javax.sql.DataSource;
import javax.annotation.PostConstruct;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;

@Component
public class MyJdbcTemplate2 extends JdbcTemplatePackage
{
    @Value("${second.datasource.driver-class-name}")
    private String driverClassName ;

    @Value("${second.datasource.url}")
    private String url ;

    @Value("${second.datasource.username}")
    private String userName ;

    @Value("${second.datasource.password}")
    private String password ;

    @PostConstruct
    public void init()
    {
        System.out.println("=====@PostConstruct===========================");
        HikariConfig config = new HikariConfig();
        config.setDriverClassName(driverClassName);
        config.setJdbcUrl(url);
        config.setUsername(userName);
        config.setPassword(password);
        DataSource ds = new HikariDataSource(config);
        super.setDataSource(ds);
        super.setDatabaseType("ORACLE");
    }

    @Bean
    public PlatformTransactionManager twoManager(){
        System.out.println("=====这是第2个数据库事务===");
        return new DataSourceTransactionManager(this.getDataSource());
    }
}

让我们看一下这两个事务管理器的Bean是否生成了。

运行Spring Boot,

 看到了吧,都成功生成Bean了。

9、最后一项,开始测试多数据库事务

(1)先测试第一个数据库Bean事务

修改测试网页中say()函数如下

@RequestMapping("/db1")
@Transactional
public String say()
{
    //分页查询t_001表数据,第2页,每页显示2条数据,该函数返回Map对象,其中的key包括总行数,总页数与具体的查询数据。
    Map mapQry = this.myJT.queryForPage("select * from t_001", 2, 2);
    List list = (List)mapQry.get("queryData");  //得到Map中的具体查询数据
    System.out.println("我是第一数据库的测试方法:"+list.toString());

    //事务
    //this.myJT.update("insert into t_001 (c01,c02,c03) values ('999','国王',100)");
    this.myJT.update("insert into t_001 (c01,c02,c03) values ('000','乞丐',0)");
    //故意抛出异常
    int i = 1/0;

    return "我是第一数据库的测试方法:" + list.toString();
}

运行结果:http://localhost:8080/db1

 

数据回滚了,数据并没有成功插入。

第一个数据库Bean的事物测试成功!

(2)再测试第二个数据库Bean事务,注意事务注解变了啊。

修改测试网页中say2()函数如下:

@RequestMapping("/db2")
@Transactional(transactionManager = "twoManager")
public String say2()
{
    //分页查询t_test表数据,第2页,每页显示2条数据,该函数返回Map对象,其中的key包括总行数,总页数与具体的查询数据。
    Map mapQry = this.myJT2.queryForPage("select * from t_test", 2, 2);
    List list = (List)mapQry.get("queryData");  //得到Map中的具体查询数据
    System.out.println("我是第二数据库的测试方法:"+list.toString());

    //事务
    this.myJT2.update("insert into t_test (id,name,age) values ('Z','ZZZ',100)");
    //故意抛出异常
    int i = 1/0;

    return "我是第二数据库的测试方法:" + list.toString();
}

事务注解 @Transactional(transactionManager = "twoManager")

特意指定了事务管理器

开始测试:http://localhost:8080/db2

 

看到了吧,数据并没有插入,回滚了。

说明第二个数据库Bean的事务也测试成功了。

测试网页完整代码如下:

package com.example.databasetest.test;

import java.util.Map;
import java.util.List;
import com.example.databasetest.tools.MyJdbcTemplate;
import com.example.databasetest.tools.MyJdbcTemplate2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Test
{
    @Autowired
    MyJdbcTemplate myJT;

    @Autowired
    MyJdbcTemplate2 myJT2;

    @RequestMapping("/db1")
    @Transactional
    public String say()
    {
        //分页查询t_001表数据,第2页,每页显示2条数据,该函数返回Map对象,其中的key包括总行数,总页数与具体的查询数据。
        Map mapQry = this.myJT.queryForPage("select * from t_001", 2, 2);
        List list = (List)mapQry.get("queryData");  //得到Map中的具体查询数据
        System.out.println("我是第一数据库的测试方法:"+list.toString());

        //事务
        //this.myJT.update("insert into t_001 (c01,c02,c03) values ('999','国王',100)");
        this.myJT.update("insert into t_001 (c01,c02,c03) values ('000','乞丐',0)");
        //故意抛出异常
        int i = 1/0;

        return "我是第一数据库的测试方法:" + list.toString();
    }

    @RequestMapping("/db2")
    @Transactional(transactionManager = "twoManager")
    public String say2()
    {
        //分页查询t_test表数据,第2页,每页显示2条数据,该函数返回Map对象,其中的key包括总行数,总页数与具体的查询数据。
        Map mapQry = this.myJT2.queryForPage("select * from t_test", 2, 2);
        List list = (List)mapQry.get("queryData");  //得到Map中的具体查询数据
        System.out.println("我是第二数据库的测试方法:"+list.toString());

        //事务
        this.myJT2.update("insert into t_test (id,name,age) values ('Z','ZZZ',100)");
        //故意抛出异常
        int i = 1/0;

        return "我是第二数据库的测试方法:" + list.toString();
    }
}

好,现在将say()与say2() 的手工抛出的异常都屏蔽再运行看看

运行: http://localhost:8080/db1  与 http://localhost:8080/db2

 数据插入成功!

OK,写这篇文章太累了,好烦啊!就到这里吧,休息,休息一会!

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐