/**
* v1.0
* 工具类包含一个连接池对象,并且对外提供获取连接和回收连接的方法
*
* 小建议:
* 工具类的方法,推荐携程静态,外部调用会更加方便
*
* 实现:
* 属性 连接池对象 [实例化一次]
* 单例
* static {
* 全局调用一次
* }
* 方法
* 对外提供连接的方法
* 回收外部传入连接方法
*/
public class JDBCToolsVersion1 {
private static DataSource dataSource;
static {
// 初始化连接池对象
Properties properties = new Properties();
InputStream ips = JdbcUtils.class.getClassLoader().getResourceAsStream("druid.properties");
try {
properties.load(ips);
} catch (Exception e) {
throw new RuntimeException(e);
}
try {
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
public static void free(Connection connection) throws SQLException {
connection.setAutoCommit(true);
connection.close();
}
}
此时在做事务操作需要调用dao层时,需要手动传入 connection 保证两次操作的连接时同一个连接
@Test
public void testCommitV1() throws SQLException {
Connection connection = JDBCToolsVersion1.getConnection();
int flag = 0;
//利用try代码块,调用dao
try {
//开启事务(关闭事务自动提交)
connection.setAutoCommit(false);
BankDao bankDao = new BankDao();
//调用加钱 和 减钱
bankDao.addMoney1("ergouzi", 500, connection);
System.out.println("--------------");
// 手动模拟报错
int i = 1 / 0;
bankDao.subMoney1("lvdandan", 500, connection);
flag = 1;
//不报错,提交事务
connection.commit();
} catch (Exception e) {
//报错回滚事务
connection.rollback();
throw e;
} finally {
JDBCToolsVersion1.free(connection);
}
if (flag == 1) {
System.out.println("转账成功!");
} else {
System.out.println("转账失败!");
}
}
注意一个小细节,在手动 close connection 的时候,配置连接自动提交为 true,避免后面拿到的连接的默认开始了事务
public class JDBCToolsVersion2 {
private static DataSource ds;
private static ThreadLocal<Connection> tl = new ThreadLocal<>();
static{//静态代码块,JDBCToolsVersion1类初始化执行
try {
Properties pro = new Properties();
pro.load(ClassLoader.getSystemResourceAsStream("druid.properties"));
ds = DruidDataSourceFactory.createDataSource(pro);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
Connection connection = tl.get();
if(connection == null){//当前线程还没有拿过连接,就给它从数据库连接池拿一个
connection = ds.getConnection();
tl.set(connection);
}
return connection;
}
public static void free() throws SQLException {
Connection connection = tl.get();
if(connection != null){
tl.remove();
connection.setAutoCommit(true);//避免还给数据库连接池的连接不是自动提交模式(建议)
connection.close();
}
}
}
此时调用时,无需dao层传入 connection,使用了 threadlocal 已保证这个线程中拿到的connection为同一个
@Test
public void testCommitV2() throws SQLException {
Connection connection = JDBCToolsVersion2.getConnection();
int flag = 0;
//利用try代码块,调用dao
try {
//开启事务(关闭事务自动提交)
connection.setAutoCommit(false);
BankDao bankDao = new BankDao();
//调用加钱 和 减钱
bankDao.addMoney("ergouzi", 500);
System.out.println("--------------");
// 手动模拟报错
int i = 1 / 0;
bankDao.subMoney("lvdandan", 500);
flag = 1;
//不报错,提交事务
connection.commit();
} catch (Exception e) {
//报错回滚事务
connection.rollback();
throw e;
} finally {
JDBCToolsVersion2.free();
}
if (flag == 1) {
System.out.println("转账成功!");
} else {
System.out.println("转账失败!");
}
}
/**
* 加钱方法
* @param account
* @param money
* @return 影响行数
*/
public int addMoney(String account, int money) throws SQLException {
Connection connection = JDBCToolsVersion2.getConnection();
String sql = "update t_bank set money = money + ? where account = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//占位符赋值
preparedStatement.setInt(1, money);
preparedStatement.setString(2, account);
//发送SQL语句
int rows = preparedStatement.executeUpdate();
//输出结果
System.out.println("加钱执行完毕!");
//关闭资源close
preparedStatement.close();
return rows;
}
/**
* 减钱方法
* @param account
* @param money
* @return 影响行数
*/
public int subMoney(String account, int money) throws SQLException {
Connection connection = JDBCToolsVersion2.getConnection();
String sql = "update t_bank set money = money - ? where account = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//占位符赋值
preparedStatement.setObject(1, money);
preparedStatement.setString(2, account);
//发送SQL语句
int rows = preparedStatement.executeUpdate();
//输出结果
System.out.println("减钱执行完毕!");
//关闭资源close
preparedStatement.close();
return rows;
}
public abstract class BaseDao {
/*
通用的增、删、改的方法
String sql:sql
Object... args:给sql中的?设置的值列表,可以是0~n
*/
protected int update(String sql, Object... args) throws SQLException {
// 创建PreparedStatement对象,对sql预编译
Connection connection = JDBCToolsVersion2.getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
//设置?的值
// 可变参数当作数据使用
// 占位符赋值
if (args != null && args.length > 0) {
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);//?的编号从1开始,不是从0开始,数组的下标是从0开始
}
}
//执行sql
int len = ps.executeUpdate();
ps.close();
// 这里检查下是否开启事务,开启不关闭连接,业务方法关闭!
// 没有开启事务的话,直接回收关闭即可!
if (connection.getAutoCommit()) {
// 回收
JDBCToolsVersion2.free();
}
return len;
}
}
public class BankDao extends BaseDao {
public int addMoney2(String account, Integer money) throws SQLException {
return update("update t_bank set money = money + ? where account = ?", money, account);
}
public int subMoney2(String account, Integer money) throws SQLException {
return update("update t_bank set money = money - ? where account = ?", money, account);
}
}
@Test
public void testCommitV3() throws SQLException {
Connection connection = JDBCToolsVersion2.getConnection();
int flag = 0;
//利用try代码块,调用dao
try {
//开启事务(关闭事务自动提交)
connection.setAutoCommit(false);
BankDao bankDao = new BankDao();
//调用加钱 和 减钱
bankDao.addMoney2("ergouzi", 500);
System.out.println("--------------");
// 手动模拟报错
int i = 1 / 0;
bankDao.subMoney2("lvdandan", 500);
flag = 1;
//不报错,提交事务
connection.commit();
} catch (Exception e) {
//报错回滚事务
connection.rollback();
throw e;
} finally {
JDBCToolsVersion2.free();
}
if (flag == 1) {
System.out.println("转账成功!");
} else {
System.out.println("转账失败!");
}
}
基于注解的事务管理器配置,数据库使用 JdbcTemplate 操作
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>6.0.2version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aopartifactId>
<version>6.0.2version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aspectsartifactId>
<version>6.0.2version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>6.0.2version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<version>6.0.2version>
dependency>
<dependency>
<groupId>org.junit.jupitergroupId>
<artifactId>junit-jupiter-apiartifactId>
<version>5.9.2version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.26version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.16version>
dependency>
jdk17
@Configuration
@ComponentScan("com.example.jdbc")
@Import(
{BankService.class,
BankDao.class}
)
@EnableTransactionManagement
public class SpringConfig {
@Bean
public DataSource getDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("oracle.jdbc.OracleDriver");
dataSource.setUrl("jdbc:oracle:thin:@localhost:1521:orcl");
dataSource.setUsername("wateruser");
dataSource.setPassword("sys");
return dataSource;
}
@Bean
public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
@Bean
public DataSourceTransactionManager getTransactionManager(DataSource dataSource) {
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
}
注意这里的 @EnableTransactionManagement 和 注入的 DataSourceTransactionManager
测试代码
@SpringJUnitConfig(classes = {SpringConfig.class})
public class JDBCTemplateTest {
@Autowired
private BankService bankService;
@Test
//测试增删改功能
public void testUpdate() {
bankService.transferAnno();
}
}
@Service
public class BankService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
public void transferAnno() {
String sql1 = "update t_bank set money = money + ? where account = ?";
jdbcTemplate.update(sql1, 1000, "ergouzi");
int i = 1/0;
String sql2 = "update t_bank set money = money -? where account =?";
jdbcTemplate.update(sql2, 1000, "lvdandan");
}
}
TODO: 剩余事务的嵌套问题的理解
mybatis 版本的和这个基本一致,因为 我们在做整合的时候也是在 mybatis 中使用 spring 的事务管理器
中与 事务相关的部分
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.7.9version>
<relativePath/>
parent>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.3.1version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>dynamic-datasource-spring-boot-starterartifactId>
<version>3.6.1version>
dependency>
<dependency>
<groupId>cn.easyprojectgroupId>
<artifactId>orai18nartifactId>
<version>12.1.0.2.0version>
dependency>
<dependency>
<groupId>com.oracle.database.jdbcgroupId>
<artifactId>ojdbc8artifactId>
<version>21.9.0.0version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
创建表空间,用户,密码
-- 创建表空间
create tablespace waterboss
datafile 'd:\waterboss.dbf'
size 100m
autoextend on -- 用于设置自动增长,如果存储量超过初始大小,则开始自动扩容
next 10m;
-- 创建用户
create user wateruser
identified by sys
default tablespace waterboss1;
-- 用户授权
grant dba to wateruser;
-- 创建表空间
create tablespace waterboss1
datafile 'd:\waterboss1.dbf'
size 100m
autoextend on -- 用于设置自动增长,如果存储量超过初始大小,则开始自动扩容
next 10m;
-- 创建用户
create user wateruser1
identified by sys
default tablespace waterboss1;
-- 用户授权
grant dba to wateruser1;
建表语句
-- ----------------------------
-- Table structure for T_BANK
-- ----------------------------
DROP TABLE "T_BANK";
CREATE TABLE "WATERUSER1"."T_BANK" (
"ID" NUMBER(20,0) VISIBLE NOT NULL,
"ACCOUNT" VARCHAR2(20 BYTE) VISIBLE NOT NULL,
"MONEY" NUMBER(20,0) VISIBLE NOT NULL
)
LOGGING
NOCOMPRESS
PCTFREE 10
INITRANS 1
STORAGE (
INITIAL 65536
NEXT 1048576
MINEXTENTS 1
MAXEXTENTS 2147483645
BUFFER_POOL DEFAULT
)
PARALLEL 1
NOCACHE
DISABLE ROW MOVEMENT
;
-- ----------------------------
-- Records of T_BANK
-- ----------------------------
INSERT INTO "T_BANK" VALUES ('1', 'ergouzi', '2000');
INSERT INTO "T_BANK" VALUES ('2', 'lvdandan', '1000');
-- ----------------------------
-- Primary Key structure for table T_BANK
-- ----------------------------
ALTER TABLE "T_BANK" ADD CONSTRAINT "SYS_C007513" PRIMARY KEY ("ID");
-- ----------------------------
-- Checks structure for table T_BANK
-- ----------------------------
ALTER TABLE "T_BANK" ADD CONSTRAINT "SYS_C007511" CHECK ("ACCOUNT" IS NOT NULL) NOT DEFERRABLE INITIALLY IMMEDIATE NORELY VALIDATE;
ALTER TABLE "T_BANK" ADD CONSTRAINT "SYS_C007512" CHECK ("MONEY" IS NOT NULL) NOT DEFERRABLE INITIALLY IMMEDIATE NORELY VALIDATE;
配置文件
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
dynamic:
# 设置默认的数据源或者数据源组,默认值即为 master
primary: master
# 严格模式 匹配不到数据源则报错
strict: true
datasource:
# 主库数据源
master:
driverClassName: oracle.jdbc.OracleDriver
url: jdbc:oracle:thin:@127.0.0.1:1521:orcl
username: wateruser
password: sys
slave:
lazy: true
driverClassName: oracle.jdbc.OracleDriver
url: jdbc:oracle:thin:@127.0.0.1:1521:orcl
username: wateruser1
password: sys
hikari:
# 最大连接池数量
maxPoolSize: 20
# 最小空闲线程数量
minIdle: 10
# 配置获取连接等待超时的时间
connectionTimeout: 10000
# 校验超时时间
validationTimeout: 5000
# 空闲连接存活最大时间,默认10分钟
idleTimeout: 60000
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认30分钟
maxLifetime: 900000
# 连接测试query(配置检测连接是否有效)
connectionTestQuery: SELECT 1 from dual
#mybatis
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.example.datasource.entity
global-config:
#数据库相关配置
db-config:
#主键类型
id-type: ASSIGN_ID
banner: false
#原生配置
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
call-setters-on-nulls: true
jdbc-type-for-null: 'null'
configuration-properties:
prefix:
blobType: BLOB
boolValue: TRUE
@TableName("t_bank")
@Data
@AllArgsConstructor
@Accessors(chain = true)
public class Bank {
@TableId(type = IdType.AUTO)
private Integer id;
private String account;
private Integer money;
}
@Mapper
public interface BankDao extends BaseMapper<Bank> {
public default void addMoney(Integer id, Integer money){
Bank bank = this.selectById(id).setMoney(this.selectById(id).getMoney() + money);
this.updateById(bank);
}
@DS("slave")
public default void subMoney(Integer id, Integer money){
Bank bank = this.selectById(id).setMoney(this.selectById(id).getMoney() - money);
this.updateById(bank);
}
}
@Service
public class BankService {
@Resource
BankDao bankDao;
@Transactional
public void transfer() {
bankDao.addMoney(1, 500);
int i = 1 / 0;
bankDao.subMoney(1, 500);
}
}
测试类
@SpringBootTest
public class TestTransaction {
@Resource
BankService bankService;
@Test
public void testTransaction() {
bankService.transfer();
}
}
可以验证到,多数据源情况下同样会回滚