Spring Boot briefly talks about multi-database configuration

Require:

When multiple databases may be encountered in Spring Boot, how should this be configured? My core requirement is that no matter how the configuration is configured, it is not allowed to affect the default configuration of my first database and the way the code is written, and the normal execution of the transaction is ensured.

Code:

1. Let’s configure the first database normally.

create project

Select the relevant dependencies. Here we use jdbcTemplate to test. The first database uses mysql, and the second database uses oracle.

2. Modify the database configuration file

        I prefer to use yml configuration files, so I renamed application.properties to application.yml. Here, both databases are fully configured in one go.

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

The spring here: is the first database configuration. By default, everyone configures it this way. There is nothing more to say. And second: is the second database configuration defined by yourself. When searching for information on the Internet, many configurations of multiple data sources require changing url: to jdbc-url:, but I don’t want to change it like this. I must ensure that the first database is not affected by the second database.

3. Encapsulate jdbcTemplate.

        Basically, when everyone uses jdbcTemplate, they encapsulate it, and few use it directly. I will also encapsulate it here, mainly to add the paging query function.

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;
    }

}

Parse the JdbcTemplatePackage class:

1) There must be a parameterless constructor and a constructor with DataSource parameters, and super(dataSource); Don’t forget to write it. There’s not much to say about this, everyone knows it!

2) When calling queryForPage(...) to query the paging function, since the syntax of each database is different, I need to know which database is called. So I added the variable

/** Database type (ORACLE, MYSQL, DB2) */
private String databaseType = "MYSQL";

The reference is the mysql database by default. This parameter value must be provided when assembling the first and second database beans.

3) The JdbcTemplatePackage class is an ordinary class, is not assembled into a Bean with the @Component annotation.

4. Generate the Bean of the first database.

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");
    }

}

Parsing the MyJdbcTemplate class:

1) This class inherits from JdbcTemplatePackage. The constructor adds a default data source and sets it to mysql database.

2) This class uses the @Component annotation to generate a Bean. Users can normally call beans of this type to operate the database.

3) If there are multiple databases, add the transaction manager TransactionManager to this class, which will be added later.

5. Test the Bean of the first database.

(1) Let’s first see if Spring Boot has automatically assembled the MyJdbcTemplate Bean for us.

Modify the startup class 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("====================我是快乐的分割线===================");


    }

}

Run this class to see the results

Look, it’s automatically assembled for me, haha!

(2) Data of table t_001 in the first database

(3) Add test web page category.

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();
    }


}

Parse the Test class:

1) @RestController This annotation does not explain and is boring.

2) @Autowired MyJdbcTemplate myJT; We are going to use the MyJdbcTemplate Bean. @Autowired finds the Bean based on its type. It is different from @Resource.

(4) Look at the returned results.

 It was very successful, 2 of the 5 pieces of data were returned, and the page was indeed paginated.

(5) Add transactions

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();
    }


}

Here we add the transaction annotation @Transactional, which will roll back the RuntimeException. I threw an exception on purpose to see the results.

The data has not changed. This completes the first database bean test!​ 

6. Create the Bean of the second database. The point is here.

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");
    }

}

Parsing the MyJdbcTemplate2 class:

1) This class continues from JdbcTemplatePackage.

2) Use the @Value annotation to obtain the parameters of the second database in the configuration file application.yml.

3) We need to use these parameters to generate a new data source DataSource and assign it to the parent class. But there is a very important question! @Value takes effect after the Bean is generated by default, and cannot be obtained in the constructor (it is not completely unavailable, just use it as a parameter, but it is too troublesome and not universal), so we use @PostConstruct Annotation, which is executed when the spring container is started. Therefore, before the second database bean is generated, the function annotated with @PostConstruct is executed, the second database source is generated and assigned to the parent class to replace the default data source.

4) Use the @Component annotation to register this class as a Bean.

Let's start Spring Boot and see the beans loading.

As you can see, @PostConstruct is loaded first, and then the beans of the two databases MyJdbcTemplate and MyJdbcTemplate2 are generated.

7. Test the beans of the second database

(1) Let’s look at the second data table

(2) Modify the test webpage

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();
    }
}

Parse the Test class:

1) Add @Autowired MyJdbcTemplate2 myJT2; Bean of the second database.

2) Add new function say2()

@RequestMapping("/db2")
public String say2()
{
    //Query the t_test table data in paging. On page 2, each page displays 2 pieces of data. This function returns a Map object, the key of which includes the total number of rows, the total number of pages and the specific query data. 
    Map mapQry = this.myJT2.queryForPage("select * from t_test", 2, 2);
    List list = (List)mapQry.get("queryData"); //Get the specific query data in the Map
    System.out.println("I am the test method of the second database:"+list.toString());

    return "I am the test method of the second database:" + list.toString();
}

3) I commented out the exception in the first database Bean test function say(), and tested the two databases together in a while.

        //Transaction
        this.myJT.update("insert into t_001 (c01,c02,c03) values ​​('999','King',100)");< a i=2>
        //Throw an exception on purpose
// int i = 1/0;

start testing

Test the first database first

http://localhost:8080/db1

The data insertion was successful because I commented the exception.

Test the second database bean again

http://localhost:8080/db2

 

 Success!

8. There are two last items, the transaction of generating database beans.

Note: I want to generate a transaction manager for the second database bean, because in this case the system will have two transaction managers. The @Transactional of the first database bean does not know which one to use, so we need to use @ Primary tells it.

Modify the MyJdbcTemplate class and add a default transaction manager to it

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());
    }

}

A new transaction manager is added to this class and set as the default.

Then add a transaction manager to the class 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());
    }
}

Let's see if the beans of these two transaction managers are generated.

Run Spring Boot,

 You see, the beans have been successfully generated.

9. The last item, start testing multi-database transactions

(1) Test the first database Bean transaction first

Modify the say() function in the test webpage as follows:

@RequestMapping("/db1")
@Transactional
public String say()
{
    //Query the t_001 table data in paging. On page 2, 2 pieces of data are displayed on each page. This function returns a Map object, the key of which includes the total number of rows, the total number of pages and the specific query data. 
    Map mapQry = this.myJT.queryForPage("select * from t_001", 2, 2);
    List list = (List)mapQry.get("queryData"); //Get the specific query data in the Map
    System.out.println("I am the first database test method:"+list.toString());

    //Transaction
    //this.myJT.update("insert into t_001 (c01,c02,c03) values ​​('999','King',100)");
    this.myJT.update("insert into t_001 (c01,c02,c03) values ​​('000','beggar',0)");
    int i = 1/0;
    return "I am the first database test method:" + list.toString();
}

Run result:http://localhost:8080/db1

 

The data was rolled back, but the data was not successfully inserted.

The first database bean transaction test was successful!

(2) Test the second database bean transaction again and notice that the transaction annotation has changed.

Modify the say2() function in the test webpage as follows:

@RequestMapping("/db2")
@Transactional(transactionManager = "twoManager")
public String say2()
{
    //Query the t_test table data in paging. On page 2, each page displays 2 pieces of data. This function returns a Map object, the key of which includes the total number of rows, the total number of pages and the specific query data. 
    Map mapQry = this.myJT2.queryForPage("select * from t_test", 2, 2);
    List list = (List)mapQry.get("queryData"); //Get the specific query data in the Map
    System.out.println("I am the test method of the second database:"+list.toString());

    //Transaction
    this.myJT2.update("insert into t_test (id,name,age) values ​​('Z','ZZZ',100)");
    //Intentionally throw exception
    int i = 1/0;

    return "I am the test method of the second database:" + list.toString();
}

Transaction annotation @Transactional(transactionManager = "twoManager")

Transaction manager specifically designated

Start testing:http://localhost:8080/db2

 

As you can see, the data was not inserted and was rolled back.

This shows that the transaction of the second database bean was also tested successfully.

The complete code of the test webpage is as follows:

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();
    }
}

Okay, now mask the manually thrown exceptions of say() and say2() and run again.

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

 Data insertion successful!

OK, writing this article is so tiring and annoying! Just stop here, rest, take a rest!

Guess you like

Origin blog.csdn.net/lag_csdn/article/details/124730482