文章目录
SpringBoot多数据源与事务
连接单个数据库(即单数据源)是很普遍的做法,但需要连接多个数据库的应用场景也很多,如主从数据库。本篇博客就来配置多数据源,并配置事务。
1 基本配置
1.1 建表
用户表很简单,只有username、pwd两个字段。
/*
Navicat MySQL Data Transfer
Source Server : test
Source Server Version : 50718
Source Host : localhost:3306
Source Database : db01
Target Server Type : MYSQL
Target Server Version : 50718
File Encoding : 65001
Date: 2019-01-13 21:46:17
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for all_user
-- ----------------------------
DROP TABLE IF EXISTS `all_user`;
CREATE TABLE `all_user` (
`username` varchar(20) NOT NULL,
`pwd` varchar(20) DEFAULT NULL,
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of all_user
-- ----------------------------
INSERT INTO `all_user` VALUES ('jack', 'jack123');
INSERT INTO `all_user` VALUES ('lily', 'lily123');
INSERT INTO `all_user` VALUES ('lucy', 'lucy123');
INSERT INTO `all_user` VALUES ('rhine', 'abc123');
数据库结构如下:
├── db01 # 主数据库
├── all_user # 主表1
├── testdb # 从数据库
├── my_user # 从表1
即有主、从两个数据库,主表1与从表1的表结构完全相同,由以上sql语句创建表。主表1中有完整用户数据,从表1为空表。下面要做的就是将主表1中的数据,复制到从表1中。
1.2 .pom
创建好SpringBoot项目后,就开始对项目进行基本配置。
最重要的依赖:mybatis、mysql-connector-java、druid。
本篇博客里就演示两个mysql数据库的连接与操作,如果通过maven来添加sqlserver或则oracle数据库的依赖会稍微困难一些,因为这两个数据库的驱动需要手动添加,这不是本篇博客的内容,所以不做介绍。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</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>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.18</version>
</dependency>
</dependencies>
1.3 .properties
接下来是配置文件:
可以看到,我们将要连接的分别是本机的db01与testdb两个数据库,主数据库是db01,从数据库是testdb
## master 数据源配置
master.datasource.url=jdbc:mysql://localhost:3306/db01?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
master.datasource.username=root
master.datasource.password=abc123456
master.datasource.driverClassName=com.mysql.cj.jdbc.Driver
## cluster 数据源配置
cluster.datasource.url=jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
cluster.datasource.username=root
cluster.datasource.password=abc123456
cluster.datasource.driverClassName=com.mysql.cj.jdbc.Driver
2 项目结构
这里列出包结构,下面的代码将不包含controller层,我们直接对service层进行单元测试:
├── main
├── java
├── com.rhine.mutildata
├── config # 数据源配置
├── controller
├── mapper # 主从分开
├── cluster
├── master
├── pojo
├── service
├── MutildataApplication.java # 程序入口
├── resources
├── mapper # 主从分开
├── cluster
├── master
├── test
├── java
├── com.rhine.mutildata
├── service # service测试包
为什么maper包下面要主从分开两个包呢?因为之后在config中数据源配置类需要做包扫描,即主数据源扫描的是com.rhine.mutildata.mapper.master,从数据源扫描的是com.rhine.mutildata.mapper.cluster,以此来作区分。
3 代码编写
3.1 mian
@SpringBootApplication
public class MutildataApplication {
public static void main(String[] args) {
SpringApplication.run(MutildataApplication.class, args);
}
}
3.2 config
Spring是通过设置DataSource与SqlSessionFactory来进行配置数据源,SspringBoot也不例外。只是SpringBoot中是通过注解配置类来实现配置,以下分别是主、从数据源的注解配置类,注意其中的不同之处。
主数据源配置类
package com.rhine.mutildata.config;
@Configuration
@MapperScan(basePackages = MasterDataSourceConfig.PACKAGE,
sqlSessionFactoryRef = "masterSqlSessionFactory")
public class MasterDataSourceConfig {
// 配置包扫描路径
static final String PACKAGE = "com.rhine.mutildata.mapper.master";
static final String MAPPER_LOCATION = "classpath:mapper/master/*.xml";
@Value("${master.datasource.url}")
private String url;
@Value("${master.datasource.username}")
private String user;
@Value("${master.datasource.password}")
private String password;
@Value("${master.datasource.driverClassName}")
private String driverClass;
@Bean(name = "masterDataSource")
@Primary
public DataSource masterDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverClass);
dataSource.setUrl(url);
dataSource.setUsername(user);
dataSource.setPassword(password);
return dataSource;
}
// 事务管理器
@Bean(name = "masterTransactionManager")
@Primary
public DataSourceTransactionManager masterTransactionManager() {
return new DataSourceTransactionManager(masterDataSource());
}
@Bean(name = "masterSqlSessionFactory")
@Primary
public SqlSessionFactory masterSqlSessionFactory(
@Qualifier("masterDataSource") DataSource masterDataSource)
throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(masterDataSource);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources(MasterDataSourceConfig.MAPPER_LOCATION));
return sessionFactory.getObject();
}
}
从数据源与主数据配置大同小异:
package com.rhine.mutildata.config;
@Configuration
@MapperScan(basePackages = ClusterDataSourceConfig.PACKAGE,
sqlSessionFactoryRef = "clusterSqlSessionFactory")
public class ClusterDataSourceConfig {
// 配置包扫描路径
static final String PACKAGE = "com.rhine.mutildata.mapper.cluster";
static final String MAPPER_LOCATION = "classpath:mapper/cluster/*.xml";
@Value("${cluster.datasource.url}")
private String url;
@Value("${cluster.datasource.username}")
private String user;
@Value("${cluster.datasource.password}")
private String password;
@Value("${cluster.datasource.driverClassName}")
private String driverClass;
@Bean(name = "clusterDataSource")
public DataSource clusterDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverClass);
dataSource.setUrl(url);
dataSource.setUsername(user);
dataSource.setPassword(password);
return dataSource;
}
// 事务管理器
@Bean(name = "clusterTransactionManager")
public DataSourceTransactionManager clusterTransactionManager() {
return new DataSourceTransactionManager(clusterDataSource());
}
@Bean(name = "clusterSqlSessionFactory")
public SqlSessionFactory clusterSqlSessionFactory(
@Qualifier("clusterDataSource") DataSource clusterDataSource)
throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(clusterDataSource);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources(ClusterDataSourceConfig.MAPPER_LOCATION));
return sessionFactory.getObject();
}
}
我们在主数据源中通过@Primary注解,将主数据源注解为默认数据源。各自扫描各自的包路径,请结合2小节中的项目结构进行查阅。另外为了我们后边能使用事务管理,所以需要配置自己的事务管理器,后面再进行介绍。
3.3 pojo
这里不需要多解释,根据自己项目的表结构来,推荐使用mybatis逆向工程来生成,尤其是在字段很多的情况下。
package com.rhine.mutildata.pojo;
public class UserBean {
private String username;
private String pwd;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
}
3.4 mapper
主数据库mapper,即只有一个读取主数据库中全部记录的方法,返回List<UserBean>:
package com.rhine.mutildata.mapper.master;
@Mapper
public interface MasterMapper {
List<UserBean> queryAll();
}
masterMapper.xml
<?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="com.rhine.mutildata.mapper.master.MasterMapper">
<select id="queryAll" resultType="com.rhine.mutildata.pojo.UserBean">
SELECT username, pwd FROM all_user
</select>
</mapper>
从数据库mapper,一个批量插入,以及一个删除所有记录的方法:
package com.rhine.mutildata.mapper.cluster;
@Mapper
public interface ClusterMapper {
// 返回受影响行数
Integer insert(List<UserBean> beans);
Boolean deleteAll();
}
clusterMapper.xml
使用了mybatis的foreach来批量插入数据,这种方式性能也最好。
<?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="com.rhine.mutildata.mapper.cluster.ClusterMapper">
<insert id="insert">
INSERT INTO my_user (username, pwd)
VALUES
<foreach collection ="list" item="item" separator =",">
(#{item.username}, #{item.pwd})
</foreach >
</insert>
<delete id="deleteAll">
DELETE FROM my_user
</delete>
</mapper>
3.5 service
service层接口:
package com.rhine.mutildata.service;
public interface UserService {
void transferData();
}
有时候别觉得麻烦,不写这个接口层,这是为了以后的可扩展性,以及更清晰的项目结构。
service层接口实现类:
package com.rhine.mutildata.service.impl;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private MasterMapper masterMapper;
@Autowired
private ClusterMapper clusterMapper;
@Override
@Transactional(transactionManager="clusterTransactionManager",
rollbackFor = Exception.class)
public void transferData() {
try {
// 主库读
List<UserBean> beans = masterMapper.queryAll();
// 用于测试事务配置是否生效
// beans = null;
// 从库写
clusterMapper.deleteAll();
clusterMapper.insert(beans);
} catch (Exception e) {
e.printStackTrace();
// 事务回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
}
service层最重要的一层,分别通过MasterMapper、ClusterMapper来对两个数据源进行操作,在transferData()上添加@Transactional注解,来声明使用从库的事务管理器(增、删、改操作才需要配置事务)。
主库中读取所有的用户信息,再清空从库里的所有记录,最后把新的记录插入从库中。如果删除或则插入操作出现错误,则catch异常,并回滚事务。
当我们主动的把beans设置为null时,那么insert操作必定抛出异常,导致事务回滚,此时我们就可以检查从库中的记录是否有被清空来判断事务是否已经生效。
5 测试
最后我们写一个service层的单元测试,来测试是否成功。
package com.rhine.mutildata.service;
@RunWith(SpringRunner.class)
@SpringBootTest(classes=MutildataApplication.class)
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testTransferData() {
userService.transferData();
}
}
启动单元测试类,即可测试。
6 oracle数据源
博客里只是写了mysql数据库,当使用oracle数据库的时候,你可以会和我一样遇到mapper.xml中的sql语句报出各种莫名其妙的错误,下面这两篇文章或许会对你很有帮助,如有其他疑问欢迎在博客下方评论留言,我会尽快回复。
MyBatis - jdbcTypeForNull Oracle
mybatis oracle:批量操作(增删改查)