一.整合Spring JDBC
1.1 引入maven依赖包,包括spring JDBC和MySQL驱动
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
1.2修改application.yml,增加数据库连接、用户名、密码相关的配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: test
password: 4rfv$RFV
driver-class-name: com.mysql.cj.jdbc.Driver
1.3新建测试表
CREATE TABLE `article` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`author` VARCHAR(32) NOT NULL,
`title` VARCHAR(32) NOT NULL,
`content` VARCHAR(512) NOT NULL,
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
)
COMMENT='文章'
ENGINE=InnoDB;
CREATE TABLE `comment` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`article_id` INT(11) NOT NULL
`name` VARCHAR(32) NOT NULL,
`content` TEXT NOT NULL,
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
)
COMMENT='评论'
ENGINE=InnoDB;
1.4在DAO中直接注入JdbcTemplate使用
@Repository
public class ArticleDAO {
@Resource
private JdbcTemplate jdbcTemplate;
//保存文章
public Long saveArticle(Article article) {
//jdbcTemplate.update适合于insert 、update和delete操作;
KeyHolder keyHolder = new GeneratedKeyHolder();
String sql = "INSERT INTO article(author, title,content,create_time) values(?, ?, ?, ?)";
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
ps.setString(1, article.getAuthor());
ps.setString(2, article.getTitle());
ps.setString(3, article.getContent());
ps.setDate(4, new Date(article.getCreateTime().getTime()));
return ps;
}, keyHolder);
return Objects.requireNonNull(keyHolder.getKey()).longValue();
}
//删除文章
public void deleteById(Long id) {
//jdbcTemplate.update适合于insert 、update和delete操作;
jdbcTemplate.update("DELETE FROM article WHERE id = ?",id);
}
//更新文章
public void updateById(Article article) {
//jdbcTemplate.update适合于insert 、update和delete操作;
jdbcTemplate.update("UPDATE article SET author = ?, title = ? ,content = ?,create_time = ? WHERE id = ?",
article.getAuthor(),
article.getTitle(),
article.getContent(),
article.getCreateTime(),
article.getId());
}
//根据id查找文章
public Article findById(Long id) {
//queryForObject用于查询单条记录返回结果
return jdbcTemplate.queryForObject("SELECT * FROM article WHERE id=?",
new Object[]{id},new BeanPropertyRowMapper<>(Article.class));
}
//查询所有
public List<Article> findAll(){
//query用于查询结果列表
return jdbcTemplate.query("SELECT * FROM article ", new BeanPropertyRowMapper<>(Article.class));
}
}
二.Spring JDBC多数据源的实现
2.1 配置多个数据源
application.yml配置2个数据源,第一个叫做primary,第二个叫做secondary。注意两个数据源连接的是不同的库,testdb和testdb2.
spring:
datasource:
primary:
jdbc-url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
secondary:
jdbc-url: jdbc:mysql://localhost:3306/testdb2?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
2.2 通过Java Config将数据源注入到Spring上下文
@Configuration
public class MultiDataSourceConfig {
@Primary
@Bean(name = "primaryDataSource")
@ConfigurationProperties(prefix="spring.datasource.primary") //testdb
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "secondaryDataSource")
@ConfigurationProperties(prefix="spring.datasource.secondary") //testdb2
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name="primaryJdbcTemplate")
public JdbcTemplate primaryJdbcTemplate (@Qualifier("primaryDataSource") DataSource dataSource ) {
return new JdbcTemplate(dataSource);
}
@Bean(name="secondaryJdbcTemplate")
public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
- primaryJdbcTemplate使用primaryDataSource数据源操作数据库testdb。
- secondaryJdbcTemplate使用secondaryDataSource数据源操作数据库testdb2。
- primaryDataSource和secondaryDataSource都是DataSource接口的实例化对象(Bean)
- @Primary注解的作用是当一个接口有多个实现类的时候,我们在主实现类对象的上面加上这个注解。表示当Spring如果只能选一个实现进行依赖注入的时候,就选@Primary标识的这个Bean。(如果这个项目只使用一个数据源,那就是primaryDataSource)
- @Qualifier明确通过编码的形式说明,当一个接口有多个实现类对象Bean的时候,我要使用哪一个Bean。
2.3 ArticleDAO改造,将文章保存到testdb数据库的article表,使用primaryJdbcTemplate对象
@Repository
public class ArticleDAO {
// @Resource
// private JdbcTemplate jdbcTemplate;
@Resource
private JdbcTemplate primaryJdbcTemplate;
//保存文章
public Long saveArticle(Article article) {
//jdbcTemplate.update适合于insert 、update和delete操作;
KeyHolder keyHolder = new GeneratedKeyHolder();
String sql = "INSERT INTO article(author, title,content,create_time) values(?, ?, ?, ?)";
primaryJdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
ps.setString(1, article.getAuthor());
ps.setString(2, article.getTitle());
ps.setString(3, article.getContent());
ps.setDate(4, new Date(article.getCreateTime().getTime()));
return ps;
}, keyHolder);
return Objects.requireNonNull(keyHolder.getKey()).longValue();
}
}
2.4 新增CommentDAO,将评论保存到testdb2数据库的comment表,使用secondaryJdbcTemplate对象
@Repository
public class CommentDAO {
@Resource
private JdbcTemplate secondaryJdbcTemplate;
//保存评论
public void saveComment(Comment comment) {
//jdbcTemplate.update适合于insert 、update和delete操作;
String sql = "INSERT INTO comment(article_id, `name`, content, create_time) values(?, ?, ?, ?)";
secondaryJdbcTemplate.update(sql, comment.getArticleId(), comment.getName(),
comment.getContent(), comment.getCreateTime());
}
}
2.5 新增一个Service类用于同时保存文章和评论
@Service
public class ArticleService {
@Resource
private ArticleDAO articleDAO;
@Resource
private CommentDAO commentDAO;
@Override
@Transactional
public Long saveArticleAndComment(Article article, Comment comment) {
Long id = articleDAO.saveArticle(article);
comment.setArticleId(id);
commentDAO.saveComment(comment);
// int a = 2 / 0;
return id;
}
}
2.6 测试同时向两个数据库保存数据
@SpringBootTest
public class JDBCTest {
@Resource
private ArticleService articleService;
@Resource
private CommentService commentService;
@Test
public void testSaveArticleAndComment() {
Article article = new Article();
article.setTitle("SpringBoot实战");
article.setContent("详细介绍SpringBoot的各种姿势");
article.setAuthor("William");
article.setCreateTime(new Date());
Comment comment = new Comment();
comment.setName("Tom");
comment.setContent("内容详实,偏实战");
comment.setCreateTime(new Date());
articleService.saveArticleAndComment(article, comment);
}
}
在src/test/java目录下,加入上面单元测试类并进行测试。正常情况下,在testdb和testdb2数据库的article表,将分别插入一条数据,表示多数据源测试成功。但是存在事务不生效问题,即使将int a = 2 / 0;
取消注释,人为制造一个被除数为0的异常,数据同样会保存成功,这是因为:数据库事务不能跨连接, 当然也就不能跨数据源,更不能跨库。一旦出现跨连接的情况,也就成了分布式事务,分布式事务不能单纯依赖于数据库去处理。
三.SpringJDBC整合JTA实现跨库分布式事务
3.1 JTA简介
- XA是X/Open DTP组织(X/Open DTP group)定义的两阶段提交协议,XA被许多数据库(如Oracle、DB2、SQL Server、MySQL)和中间件等工具(如CICS 和 Tuxedo)支持 。
- JTA规范:JTA(Java Transaction API),是J2EE的编程接口规范,它是XA协议的JAVA实现。 某种程度上,可以认为JTA规范是XA规范的Java版。
- Atomikos是一个分布式事务管理器,是JTA / XA的具体实现,提供的功能比JTA / XA所要求的更多。
3.2 引入maven依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
3.3 修改application配置文件,配置testdb和testdb2双数据源
primarydb:
uniqueResourceName: primary
xaDataSourceClassName: com.mysql.cj.jdbc.MysqlXADataSource
xaProperties:
url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=utf-8&useSSL=false
user: root
password: root
exclusiveConnectionMode: true
minPoolSize: 3
maxPoolSize: 10
testQuery: SELECT 1 from dual #由于采用HikiriCP,用于检测数据库连接是否存活。
secondarydb:
uniqueResourceName: secondary
xaDataSourceClassName: com.mysql.cj.jdbc.MysqlXADataSource
xaProperties:
url: jdbc:mysql://localhost:3306/testdb2?useUnicode=true&characterEncoding=utf-8&useSSL=false
user: root
password: root
exclusiveConnectionMode: true
minPoolSize: 3
maxPoolSize: 10
testQuery: SELECT 1 from dual #由于采用HikiriCP,用于检测数据库连接是否存活。
3.4 数据源配置
@Configuration
public class DataSourceConfig {
//jta数据源primarydb
@Bean(initMethod="init", destroyMethod="close", name="primaryDataSource")
@Primary
@ConfigurationProperties(prefix = "primarydb")
public DataSource primaryDataSource() {
//这里是关键,返回的是AtomikosDataSourceBean,所有的配置属性也都是注入到这个类里面
return new AtomikosDataSourceBean();
}
//jta数据源secondarydb
@Bean(initMethod="init", destroyMethod="close", name="secondaryDataSource")
@ConfigurationProperties(prefix = "secondarydb")
public DataSource secondaryDataSource() {
return new AtomikosDataSourceBean();
}
//primaryJdbcTemplate使用primaryDataSource数据源
@Bean
public JdbcTemplate primaryJdbcTemplate(
@Qualifier("primaryDataSource") DataSource primaryDataSource) {
return new JdbcTemplate(primaryDataSource);
}
//secondaryJdbcTemplate使用secondaryDataSource数据源
@Bean
public JdbcTemplate secondaryJdbcTemplate(
@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
return new JdbcTemplate(secondaryDataSource);
}
}
- primaryDataSource数据源对象通过ConfigurationProperties使用primarydb数据源配置(application.yml)。同理secondaryDataSource数据源对象通过ConfigurationProperties使用secondarydb数据源配置
- JdbcTemplate是操作数据源(数据库)的模板对象,需要为它传入一个数据源。@Qualifier注解将数据源对象以参数的形式,传到primaryJdbcTemplate和secondaryJdbcTemplate的数据源模板对象里面。
3.4 事务管理器配置
@Configuration
public class TransactionManagerConfig {
@Bean
public UserTransaction userTransaction() throws SystemException {
UserTransactionImp userTransactionImp = new UserTransactionImp();
userTransactionImp.setTransactionTimeout(10000);
return userTransactionImp;
}
@Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close")
public TransactionManager atomikosTransactionManager() throws Throwable {
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(false);
return userTransactionManager;
}
@Bean(name = "transactionManager")
@DependsOn({ "userTransaction", "atomikosTransactionManager" })
public PlatformTransactionManager transactionManager() throws Throwable {
UserTransaction userTransaction = userTransaction();
JtaTransactionManager manager = new JtaTransactionManager(userTransaction, atomikosTransactionManager());
return manager;
}
}
正确完成配置后,再测试一下之前的测试用例:人为制造一个被除数为0的异常,异常抛出,两个数据库实例中article、comment表将都无法插入数据。符合事务的要求:正常情况数据操作都成功,异常情况数据操作都失败回滚。