Spring-JDBC与事务
一、Spring整合JDBC的环境
(一)添加依赖坐标
(二)添加jdbc.properties的配置
(三)创建Spring的配置文件
(四)配置数据源
(五)JDBC测试
二、持久层账户模块操作(例)
(一)账户接口方法定义
(二)、账户记录添加实现
(三)账户记录查询的实现
(四)账户记录的更新操作
(五)账户记录删除实现
三、事务的概念
(一)事务的四大特性(ACID)
(二)Spring事务核心接口
(三)Spring事务控制的配置
(四)Spring事务控制——模拟转账的实现
往期文章回顾
Spring JDBC与事务操作
一、Spring整合JDBC的环境
Spring框架除了提供了IOC和AOP的核心功能之外,同样提供了基于JDBC的数据访问功能,使得访问持久层数据更加方便。使用Spring JDBC环境,首先需要一套Spring整合JDBC的环境。
(一)添加依赖坐标
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!--配置相关的依赖坐标-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.14</version>
</dependency>
<!--spring的测试-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.14</version>
</dependency>
<!--aop-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
<!--spring-jdbc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.14</version>
</dependency>
<!--spring事务-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.14</version>
</dependency>
<!--mysql驱动包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<!--C3P0连接池-->
<!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.5</version>
</dependency>
<!--使用lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
(二)添加jdbc.properties的配置
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost/spring_jdbc?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
jdbc.username = root
jdbc.password = root
jdbc.maxActive= 50
initialPoolSize=20
maxPoolSize=100
minPoolSize=10
maxIdleTime=600
acquireIncrement=5
maxStatements=5
idleConnectionTestPeriod=60
(三)创建Spring的配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
">
<context:component-scan base-package="org.apache"/>
<context:property-placeholder location="jdbc.properties"/>
</beans>
需要添加相应的命名空间,并且开启自动化扫描组件,使用context的property-placeholder属性,并指定location属性值为指定的配置文件。
(四)配置数据源
由于建立数据库连接是一个非常耗时耗资源的行为,所以通过连接池预先同数据库建立一些连接,放在内存中,应用程序需要建立数据库连接时,直接到连接池中申请一个即可。用完再放回去。
C3P0是一个开源的JDBC连接池,它实现了数据源,支持JDBC3规范和JDBC2的标准扩展,目前使用它的开源项目有Hibernate,Spring等,C3P0具有自动回收空闲连接的功能。
C3P0数据源配置
<!--配置C3P0数据源-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--通过property配置属性值,value是
properties中的值-->
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
模板类配置
Spring把JDBC中的重复操作组合成了一个模板类org.springframework.jdbc.core.JdbcTemplate
<!--配置模板类jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--将数据源注入jdbcTemplate对象中-->
<property name="dataSource" ref="dataSource"/>
</bean>
(五)JDBC测试
创建数据库表
创建tb_account表,并插入相应的数据支持。
USE MyBatis;
CREATE TABLE tb_account(
account_id int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
account_name varchar(20) NOT NULL,
account_type varchar(20) NOT NULL,
money double NOT NULL ,
remark varchar(50),
create_time datetime NOT NULL,
update_time datetime NOT NULL ,
user_id int(11) NOT NULL
);
INSERT INTO tb_account (account_id, account_name, account_type, money, remark, create_time, update_time, user_id)
VALUES (1,'账户1','建设银行',1000,'零花钱','2020-03-19 02:42:42','2020-03-19 02:42:56',1);
INSERT INTO tb_account(account_id, account_name, account_type, money, remark, create_time, update_time, user_id) VALUES
(2,'账户2','招商银行',500,'兼职费','2020-03-20 02:43:56','2020-03-20 02:46:57',1);
使用Junit测试
@Test
public void testTemplate1(){
//获取Spring的上下文环境
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
//定义sql语句
String totalUsers="SELECT count(1) FROM tb_account";
Integer total = jdbcTemplate.queryForObject(totalUsers, Integer.class);
System.out.println("共有"+total+"条数据");
}
@Test
public void testJdbcTemplate2(){
//获取Spring的上下文环境
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
String sql="SELECT count(1) FROM tb_account WHERE user_id=?";
Integer integer = jdbcTemplate.queryForObject(sql, Integer.class,2);
System.out.println("记录总数:"+integer);
}
junit测试改进
首先创建基类测试类
@RunWith(SpringJUnit4ClassRunner.class)//将测试运行在Spring的测试环境中
@ContextConfiguration(locations = {"classpath:spring.xml"})//设置要加载的配置文件,使用注解进行配置。
public class BaseTest {
}
需要注意的是基类测试类不需要书写任何内容,只需要在类上添加两个注解
@RunWith(SpringJUnit4ClassRunner.class)
:此处表示将原先的junit测试添加到Spring的测试环境之中去。
@ContextConfiguration(locations={xxxx,xxxx})
:此处表示设置要加载的配置文件,locations表示配置文件的数组名,之后添加一个数组,数组的内容是需要使用到的xml配置文件。
创建基本的测试类
public class SpringJDBCTest extends BaseTest{
/**使用注解来注入*/
@Resource
private JdbcTemplate jdbcTemplate;
@Test
public void testTemplate1(){
//定义sql语句
String totalUsers="SELECT count(1) FROM tb_account";
Integer total = jdbcTemplate.queryForObject(totalUsers, Integer.class);
System.out.println("共有"+total+"条数据");
}
@Test
public void testJdbcTemplate2(){
String sql="SELECT count(1) FROM tb_account WHERE user_id=?";
Integer integer = jdbcTemplate.queryForObject(sql, Integer.class,2);
System.out.println("记录总数:"+integer);
}
}
此处需要注意的是将jdbcTemplate类对象作为属性注入到测试类中,并通过@Resource
注解完成属性的注入。并且具体的测试类只需要继承父类BaseTest即可完成对配置文件的加载及junit环境添加到Spring环境之中去的操作。
二、持久层账户模块操作(例)
此处使用Spring JDBC完成增删改查操作
(一)账户接口方法定义
1. 定义账户实体类
Account.java类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Integer accountId;
private String accountName;
private String accountType;
private Double money;
private String remark;
private Date createTime;
private Date updateTime;
private Integer userId;
public Account(String accountName, String accountType, Double money, String remark, Integer userId) {
this.accountName = accountName;
this.accountType = accountType;
this.money = money;
this.remark = remark;
this.userId = userId;
}
}
2.定义账户的接口类
IAccountDao.java
/** * @author lambda * 添加、修改、删除、查询账户 * 1. 添加账户 * 添加账户记录,返回受影响的行数 * 添加账户记录,返回主键 * 批量添加账户记录,返回受影响的行数 * 2. 修改账户 * 修改账户记录,返回受影响的行数 * 批量修改账户记录,返回受影响的行数 * * 3.删除账户 * 删除账户记录,返回受影响的行数 * 批量删除账户记录,返回受影响的行数 * * 4. 查询账户 * 查询指定用户账户的总记录数 * 查询指定账户的账户详情,返回账户对象 * 多条件查询指定用户的账户列表,返回一个账户集合 * */public interface IAccountDao { /** * 添加账户记录,返回受影响的行数 * @param account * @return */ int addAccount(Account account); /** * 添加账户记录,返回主键 * @param account * @return */ int addAccountPrimaryKey(Account account); /** * 批量添加账户记录,返回受影响的行数 * @param accounts * @return */ int addAccountBatch(List<Account> accounts); /** * 查询指定用户账户的总记录数 * @param userId * @return */ int queryAccountCount(int userId); /** * 查询指定账户的账户详情,返回账户对象 * @param accountId * @return */ Account queryAccountByAccountId(int accountId); /** * 多条件查询指定用户的账户列表,返回一个账户集合 * @param userId * @param accountName * @param accountType * @param createTime * @return */ List<Account> queryAccountByParams(int userId,String accountName,String accountType,String createTime); /** * 修改账户记录,返回受影响的行数,传入整个账户对象 * @param account * @return */ int updateAccount(Account account); /** * 批量修改账户记录,返回受影响的行数 * @param accounts * @return */ int updateAccountBatch(List<Account> accounts); /** * 删除账户记录,返回受影响的行数 * @param accountId * @return */ int deleteAccount(int accountId); /** * 批量删除账户记录,返回受影响的行数 * @param ids * @return */ int deleteAccountBatch(int[] ids);}
3.定义接口的实现类
@Repository
public class AccountDaoImpl implements IAccountDao {
/**由于操作数据库需要使用Spring-JDBC,因此需要传入jdbcTemplate模板对象, * 并完成注入到当前类中*/
@Resource
private JdbcTemplate jdbcTemplate;
@Override
public int addAccount(Account account) {
return 0;
}
@Override
public int addAccountPrimaryKey(Account account) {
return 0;
}
@Override
public int addAccountBatch(List<Account> accounts) {
return 0;
}
@Override
public int queryAccountCount(int userId) {
return 0;
}
@Override
public Account queryAccountByAccountId(int accountId) {
return null;
}
@Override
public List<Account> queryAccountByParams(int userId, String accountName, String accountType, String createTime) {
return null;
}
@Override
public int updateAccount(Account account) {
return 0;
}
@Override
public int updateAccountBatch(List<Account> accounts) {
return 0;
}
@Override
public int deleteAccount(int accountId) {
return 0;
}
@Override
public int deleteAccountBatch(int[] ids) {
return 0;
}
}
由于操作数据库需要使用Spring-JDBC,因此需要传入jdbcTemplate模板对象
(二)、账户记录添加实现
在企业项目开发时,对于记录的添加可能涉及到多种添加方式,比如添加单条记录,批量添加多条记录等情况,这里对于账户记录添加方式为三种,添加单条记录,返回受影响的行数,添加单条记录,返回主键、批量添加多条记录。
1. 添加账户记录
添加账户记录返回首影响的行数
@Override
public int addAccount(Account account) {
String sql="INSERT INTO tb_account(account_name,account_type,money,remark,create_time" + ",update_time,user_id) VALUES(?,?,?,?,now(),now(),?)";
int update = jdbcTemplate.update(sql, account.getAccountName(), account.getAccountType(), account.getMoney(), account.getRemark(), account.getUserId());
return update;
}
@Test
public void addAccount(){
int i = accountDao.addAccount(new Account("账户3", "工商银行", 200.0, "奖金", 1));
if (i>0){
System.out.println("插入成功!");
}
}
jdbcTemplate.update方法需要传入对应的sql语句,并传递相应的参数。参数以可变参数的形式添加到对应的表中。
2.添加账户记录返回主键
/**
* 添加记录返回主键
* @param account
* @return
*/
@Override
public int addAccountPrimaryKey(Account account) {
String sql="INSERT INTO tb_account(account_name,account_type,money,remark,create_time" +",update_time,user_id)VALUES(?,?,?,?,now(),now(),?)";
//定义该对象,用于获取我们记录的主键
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
//需要传入两个参数,一个是PreparedStatementCreator,一个是keyHolder
jdbcTemplate.update(con -> {
//此处需要利用lambda表达式去预编译sql语句,并设置返回主键(tatement.RETURN_GENERATED_KEYS)
PreparedStatement ps=con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
//设置相应的参数;
ps.setString(1,account.getAccountName());
ps.setString(2,account.getAccountType());
ps.setDouble(3,account.getMoney());
ps.setString(4,account.getRemark());
ps.setInt(5,account.getUserId());
//返回预编译对象
return ps;
}, keyHolder);
//执行完之后可以通过keyHolder获取主键
return keyHolder.getKey().intValue();
}
@Test
public void addAccountPrimaryKey(){
Account account = new Account("账户4", "中国银行", 300.0, "绩效奖", 4); int key = accountDao.addAccountPrimaryKey(account);
System.out.println("主键为:"+key);
}
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
此处表示获取键值对象,后续可以用来获取主键jdbcTemplate.update(PreparedStatementCreator pcs,KeyHolder keyHolder)
此处表示执行update方法,并且传入的参数有经过处理的预处理语句(使用lambda表达式来进行设置sql语句的相关参数并返回预处理语句对象ps)和键对象最后通过 keyHolder.getKey().intValue();
来获取新添加的对象的主键值。
3.批量添加账户,返回受影响的行数
/**
* 批量添加账户,返回受影响的行数
* @param accounts
* @return
*/
@Override
public int addAccountBatch(List<Account> accounts) {
String sql="INSERT INTO tb_account(account_name,account_type,money,remark,create_time" +
",update_time,user_id)VALUES(?,?,?,?,now(),now(),?)";
//调用batchUpdate批量添加,需要传入sql语句和BatchPreparedStatementSetter pss对象
int rows= jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
//此处由于传入的是集合,所以需要通过i来获取指定的Account对象(会自动循环)
Account account=accounts.get(i);
//设置参数
ps.setString(1,account.getAccountName());
ps.setString(2,account.getAccountType());
ps.setDouble(3,account.getMoney());
ps.setString(4,account.getRemark());
ps.setInt(5,account.getUserId());
}
@Override
public int getBatchSize() {
//直接返回集合的大小,即受影响的行数
return accounts.size();
}
}).length;
//jdbcTemplate.batchUpdate返回的是数组,通过length属性可以获取数组的大小,即受影响的行数
return rows;
}
@Test
public void addAccountBatch(){
List<Account> accounts=new ArrayList<>();
accounts.add(new Account("账户5", "广东发展银行", 400.0, "出勤奖", 2));
accounts.add(new Account("账户6", "浦发银行", 600.0, "年终奖", 2));
int i = accountDao.addAccountBatch(accounts);
System.out.println("添加了"+i+"条记录");
}
jdbcTemplate.batchUpdate
表示执行批量添加操作,主要参数有要执行的sql语句和BatchPreparedStatementSetter pss对象。由于需要传入 BatchPreparedStatementSetter
对象。所以new一个对象,并实现其中的两个方法setValues
和getBatchSize
setValues
是使用预处理语句来对相应的sql语句进行预处理,并且通过循环取出集合中每一个值进行相应的赋值操作。getBatchSize
最终返回的是经过预处理的集合的大小。最终返回数组的大小。
(三)账户记录查询的实现
账户记录查询提供了三种查询的方式,查询指定用户所有账户记录数,查询单条账户记录详情,多条件查询指定用户账户记录。
1. 查询用户的账户记录总数
@Override
public int queryAccountCount(int userId) {
//定义sql语句(其中的count中的括号可以填写*表示全部,可以添加1,表示第一个字段的总数,也就是总数)
String sql="SELECT COUNT(1) FROM tb_account WHERE user_id=?";
//调用查询方法(其中的参数依次表示为sql语句,返回的类型,以及参数值)
Integer integer = jdbcTemplate.queryForObject(sql, Integer.class, userId);
return integer;
}
@Test
public void queryAccountCount(){
int i = accountDao.queryAccountCount(1);
System.out.println("1号用户的账户一共有:"+i+"条");
}
jdbcTemplate.queryForObject
用来执行查询操作,参数为sql语句,返回值的类型,以及方法的参数值。最终将返回的结果返回即可获取指定用户的总账户数。
2.查询指定账户记录详情
/**
* 查询指定账户记录详情,返回账户对象
* @param accountId
* @return
*/
@Override
public Account queryAccountByAccountId(int accountId) {
String sql="SELECT * FROM tb_account WHERE account_id=?";
//调用查询对象的方法,此处方法的第二个参数是一个函数式接口对象,lambda表达式,返回account对象
Account account= jdbcTemplate.queryForObject(sql,(rs,rowNum)->{ //在内存中创建一个账户对象来设置相应的信息以便后续返回
Account acc=new Account();
acc.setAccountId(accountId);
acc.setAccountName(rs.getString("account_name"));
acc.setAccountType(rs.getString("account_type"));
acc.setMoney(rs.getDouble("money"));
acc.setRemark(rs.getString("remark"));
acc.setCreateTime(rs.getDate("create_time"));
acc.setUpdateTime(rs.getDate("update_time"));
acc.setUserId(rs.getInt("user_id"));
return acc;
},accountId);
return account;
}
@Test
public void queryAccountByAccountId(){
Account account = accountDao.queryAccountByAccountId(1);
System.out.println(account);
}
此处的 queryForObject
方法主要有3个参数,一个是sql,一个是RowMapper对象,一个是对应的可变参数 RowMaper参数是一个函数式接口,可以使用lambda表达式,该接口中需要传入行集和行数,需要接收返回的Account(Account的各个信息由行集中获取并返回。)最终返回为一个Account对象
3. 多条件查询用户账户记录
@Override
public List<Account> queryAccountByParams(int userId, String accountName, String accountType, String createTime) {
//首先定义sql语句
String sql="SELECT * FROM tb_account WHERE user_id=?";
//定义一个参数集合,因为是多条件查询
List<Object> params=new ArrayList<>();
params.add(userId);
//判断字符串是否为空,如果不为空,则拼接sql语句
if (StringUtils.isNotBlank(accountName)){
//如果账户名称不为空,则进行拼接
sql+="and account_name Like contact('%',?,'%')";
params.add(accountName);
}
if (StringUtils.isNotBlank(accountType)){
//如果不为空
sql+="AND account_type=?";
params.add(accountType);
}
if (StringUtils.isNotBlank(createTime)){
//如果创建时间不为空
sql+="AND create_time<?";
params.add(createTime);
}
//将集合转换为数组
Object[] objects = params.toArray();
List<Account> query = jdbcTemplate.query(sql, objects, (rs, i) -> {
//在内存中创建一个账户对象来设置相应的信息以便后续返回
Account acc = new Account();
acc.setAccountId(rs.getInt("account_id"));
acc.setAccountName(rs.getString("account_name"));
acc.setAccountType(rs.getString("account_type"));
acc.setMoney(rs.getDouble("money"));
acc.setRemark(rs.getString("remark"));
acc.setCreateTime(rs.getDate("create_time"));
acc.setUpdateTime(rs.getDate("update_time"));
acc.setUserId(rs.getInt("user_id"));
return acc;
});
return query;
}
@Test
public void queryAccountByParams(){
List<Account> accounts = accountDao.queryAccountByParams(1, "账户1", "招商银行", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
System.out.println(accounts.toString());
}
此处多条件查询,需要涉及到AND条件的拼接,注意点在于 AND
关键字之间需要空格。jdbcTemplate.query
方法需要传入三个参数,一个是sql、一个是参数数组,一个是RowMapper<T> rowMapper
对象。sql语句需要查询出所有的所需要的数据 参数数组:需要判断里面的参数是否为空,不为空,则添加到参数数组中,并且对sql语句进行相应的拼接操作。 RowMapper对象需要使用lambda表达式依次对创建的对象进行相应的赋值, 最终返回查询到的对象 在测试的时候需要使用 SimpleDateFormat
类来对此进行格式化时间。
(四)账户记录的更新操作
1.更新账户记录
/**
* 更新账户,返回受影响的行数
* @param account
* @return
*/
@Override
public int updateAccount(Account account) {
String sql="UPDATE tb_account SET account_name=?, account_type=?,money=?,remark=?," +
"update_time=now() ,user_id=? WHERE account_id=?";
int update = jdbcTemplate.update(sql, account.getAccountName(), account.getAccountType(),
account.getMoney(), account.getRemark(), account.getUpdateTime(), account.getUserId()
, account.getAccountId());
return update;
}
@Test
public void updateAccount(){
int i = accountDao.updateAccount(new Account());
System.out.println("更新了"+i+"条数据.");
}
2.批量更新账户记录
/**
*批量更新账户记录
* @param accounts
* @return
*/
@Override
public int updateAccountBatch(List<Account> accounts) {
String sql="UPDATE tb_account SET account_name=?, account_type=?,money=?,remark=?," +
"update_time=now() ,user_id=? WHERE account_id=?";
int length = jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
//此处由于传入的是集合,所以需要通过i来获取指定的Account对象(会自动循环)
Account account=accounts.get(i);
//设置参数
ps.setString(1,account.getAccountName());
ps.setString(2,account.getAccountType());
ps.setDouble(3,account.getMoney());
ps.setString(4,account.getRemark());
ps.setInt(5,account.getUserId());
ps.setInt(6,account.getAccountId());
}
@Override
public int getBatchSize() {
return accounts.size();
}
}).length;
@Test
public void testUpdateBatch(){
List<Account>accounts=new ArrayList<>();
accounts.add(new Account());
accounts.add(new Account());
int i = accountDao.updateAccountBatch(accounts);
System.out.println("更新了"+i+"条数据");
}
(五)账户记录删除实现
1.删除账户记录
/**
*删除账户记录,返回受影响的行数
* @param accountId
* @return
*/
@Override
public int deleteAccount(int accountId) {
String sql="DELETE FROM tb_account WHERE account_id=?";
int update = jdbcTemplate.update(sql, accountId);
return update;
}
@Test
public void deleteAccountByAccountId(){
int i = accountDao.deleteAccount(1);
System.out.println("受影响的行数"+i);
}
对于jdbcTemplate而言,删除使用的是update语句
2.批量删除账户记录
/**
*批量删除用户记录,返回受影响的行数
* @param ids
* @return
*/
@Override
public int deleteAccountBatch(int[] ids) {
String sql="DELETE FROM tb_account WHERE account_id=?";
int length = jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
ps.setInt(1, ids[i]);
//i表示行数
}
@Override
public int getBatchSize() {
return ids.length;
}
}).length;
return length;
}
}
@Test
public void deleteAccountBatch(){
int[] ids={1,2,3};
int i = accountDao.deleteAccountBatch(ids);
System.out.println("受影响的行数有:"+ids);
}
批量删除使用batchupdate语句,传入sql语句与 BatchPreparedStatementSetter
对象setValues
是预处理语句循环赋值,因为是批处理,所以需要通过循环不断地为对应的处理语句赋值,最后返回参数数组的长度,即参数有多少个,赋了多少值。
三、事务的概念
(一)事务的四大特性(ACID)
1. 原子性(Atomicity)
要么全部成功,要么全部失败,指 一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着"同生共死"的感觉
2. 一致性(Consistency)
事务在执行前后,数据库中的数据要保持一致性状态(如转账过程前后需要保持数据的一致性),即只有改变前的状态和改变后的状态,没有所谓的中间状态。
3.隔离性(Isolation)
事务与事务之间的执行应当是相互隔离互不影响的。(多个角色对于统一记录进行操作必须保证没有任何干扰)。没有任何影响显然是不可能的,为了让影响级别降到最低,通过隔离级别加以限制
READ_UNCOMMITED(读未提交),隔离级别最低的一种事务级别,在这种隔离级别下,会引发脏读、不可重复读和幻读 READ_COMMITED(读已提交),读到的是别人提交后的值,在这种隔离级别下,会引发不可重复读和幻读,但是避免了脏读。 REPEATABLE_READ(可重复读),在这种隔离级别下,会引发幻读,但是避免了脏读和不可重复读 SERIALIZABLE(串行化),最严格的隔离级别,在serializable隔离级别下,所有事务按照次序依次进行,脏读不可重复读和幻读都不会出现。
4. 持久性(Durability)
事务提交完毕后,数据库中的数据的改变是永久的。
(二)Spring事务核心接口
Spring事务管理的实现有很多细节,如果对整个接口框架有个大体的了解会非常有利于我们理解事务。
Spring并不是直接管理事务,而是提供了多种事务管理器,他们将事务的管理职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现的。
Spring事务管理器的接口是org.springframework.transaction.PlatformTranscationManager
,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但是具体的实现可以依据各个平台自己的实际来定。接口内容如下:
public interface PlatformTransactionManager extends TransactionManager {
//得到事务对象的方法
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
//提交事务
void commit(TransactionStatus status) throws TransactionException;
//回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
从这里可知具体的事务管理机制对Spring来说是透明的,它并不关心具体的事务管理机制,哪些是对应各个平台需要关心的。所以Spring事务管理的一个优点就是为不同的事务API提供一个统一的编程模型,如JTA、JDBC、HIbernate、JPA等。
1. JDBC事务
如果应用程序中直接使用JDBC来进行持久化,此时使用DataSourceTransactionManager
来处理事务边界,为了使用DataSourceTransactionManager
,需要使用如下的xml信息将其装配到程序的上下文定义中去。(MyBatis也是使用DataSourceTransactionManager
)
<!--配置JDBC事务管理-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
事实上,DataSourceTransactionManager
是通过调用java.sql.connection
来管理事务,后者是通过DataSource获取的,通过调用连接的commit方法来提交事务,同样,如果事务失败,则通过调用rollback方法进行回滚。
2. Hibernate事务
使用HIbernate事务需要首先导入Hibernate的依赖坐标
<!-- https://mvnrepository.com/artifact/org.springframework/spring-hibernate3 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-hibernate3</artifactId>
<version>2.0.8</version>
</dependency>
如果应用程序的持久化是通过HIbernate来实现的话,需要使用HibernateTransactionManager
,对于HIbernate3,需要在spring的上下文中定义如下声明:
<!--Hibernate实现事务管理-->
<bean id="hibernateTransactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
sessionFactory
属性需要装配一个HIbernate的session工厂,HibernateTransactionManager
的实现细节是它将事务管理的职责委托给org.hibernate.Transaction
对象,而后者是从Hibernate session
中获取到的。当事务成功完成时,HibernateTransactionManager
将会调用Transaction
对象的commit方法,反之,则会调用rollback方法。
3. Java持久化API事务(JPA)
使用JPA需要导入坐标依赖
<!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-jpa -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>2.6.1</version>
</dependency>
Java持久化API作为真正的Java持久化标准进入我们的视野,使用JPA的话,需要使用Spring的JpaTransactionManager
来处理事务,可以在xml文件中进入如下配置
<bean id="jpaTransactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManager"/>
</bean>
JpaTransactionManager
只需要装配一个JPA实体管理工厂(javax.persistence.EntityManagerFactory
接口的任意实现),JpaTransactionManager
将由与工厂所产生的JPA EntityManager合作构建事务。
4. Java原生API事务
如果应用程序跨越了多个事务管理器(比如两个或者多个不同的数据源)。此时需要使用JtaTransactionManager
:
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManagerName" value="java:/TransactionManager/"/>
</bean>
JtaTransactionManager
将事务管理责任委托给javax.transaction.UserTransaction
和javax.transaction.TransactionManager
对象。其中事务成功完成通过UserTransaction.commit()
方法提交,事务失败,则通过UserTransaction.rollback()
方法回滚。
(三)Spring事务控制的配置
通过jdbc持久化事务,对于事务配置实现由两种方式来实现,即xml配置和注解配置
1. XML配置
(1)添加命名空间
事务
xmlns:tx="http://www.springframework.org/schema/tx"
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
AOP
xmlns:aop="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
">
(2)设置AOP代理
<!--设置aop代理-->
<aop:aspectj-autoproxy/>
(3)配置事务管理器
<!--配置JDBC事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
(4)配置事务的相关通知
<!--配置事务通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!--定义什么方法需要事务处理-->
<tx:attributes>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="delte*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="query*" read-only="true"/>
<!--为所有的方法配置通知-->
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
一般来说增删改使用propagation="REQUIRED"
,查询一般是使用read-only="true"
设置为只读。
配置事务的通知:
transaction-manager
属性表示这个事务通知是哪个事务管理器的tx:method
属性name
:是必须的,表示与事务属性关联的方法名(业务方法名),对切入点进行细化。通配符(*)可以用来指定一批关联到相同的事务属性的方法。如get*
、handle*
、on*Event
等propagation
:不是必须的,默认值是REQUIRED
,表示事务传播行为,包括REQUIRED、SUPPORTS、MANDATORY、NEVER、REQUIRES_NEW\NOT_SUPPORTED\NESTED
isolation
:不是必须的,默认值是DEFAULT
,表示事务的隔离级别(数据库的隔离级别)timeout
:不是必须的,默认值为-1(永不超时),表示事务的超时时间,以秒为单位。read-only
:不是必须的,默认值false不是只读的,表示事务是否为只读。rollback-for
:不是必须的,表示将被触发进行回滚的Exception(s)
,以逗号分开。no-rollback-for
:不是必须的,表示不被触发进行回滚的Exception(s)
,以逗号分开。
事务传播行为的介绍
对应的属性值 | 对应的意义 |
---|---|
@Transactional(propagation=Propagation.REQUIRED) | 如果有事务,那么加入事务,如果或没有事务,默认情况下新建一个。 |
@Transactional(propagation=Propagation.NOT_SUPPORTED) | 容器不为这个方法开启事务 |
@Transactional(propagation=Propagation.REQUIRES_NEW) | 不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,再继续执行老的事务。 |
@Transactional(propagation=Propagation.MANDATORY) | 必须在一个已有的事务中执行,否则会抛出异常 |
@Transactional(propagation=Propagation.NEVER) | 必须在一个没有的事务中执行,否则抛出异常(与MANDATORY相反) |
@Transactional(propagation=Propagation.SUPPORTS) | 如果其他的bean调用这个方法,在其他bean中声明事务,那就用事务。如果在其他bean中没有声明事务,那就不用事务。 |
@Transactional(propagation=Propagation.NESTED) | 支持当前事务,如果当前事务存在,则执行一个嵌套事务,如果当前没有事务,就新建一个事务。 |
(5)配置AOP
<!--定义AOP切面-->
<aop:config>
<!--定义切点-->
<aop:pointcut id="pointCut" expression="execution(* org.apache.*.*(..))"/>
<!--配置通知-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointCut"/>
</aop:config>
2. 注解配置
(1)配置事务管理器
<!--配置JDBC事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--绑定数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
(2)配置注解支持
<!--配置事务的注解支持-->
<tx:annotation-driven transaction-manager="transactionManager"/>
(3)加入事务注解
在需要使用事务的方法上添加事务的注解
@Override
@Transactional(propagation=Propagation.REQUIRED)
public void saverUser(){
xxxxx
}
(四)Spring事务控制——模拟转账的实现
1.接口方法定义
/**
* 支出方法
* @param accountId
* @param money
* @return
*/
int outAccount(int accountId,Double money);
/**
* 表示收入方法
* @param accountId
* @param money
* @return
*/
int inAccount(int accountId,Double money);
2.接口实现
/**
* 表示支出
* @param accountId
* @param money
* @return
*/
@Override
public int outAccount(int accountId, Double money) {
String sql="UPDATE tb_account SET money=money-? WHERE account_id=?";
return jdbcTemplate.update(sql,money,accountId);
}
/**
*表示收入
* @param accountId
* @param money
* @return
*/
@Override
public int inAccount(int accountId, Double money) {
String sql="UPDATE tb_account SET money=money+? WHERE account_id=?";
return jdbcTemplate.update(sql,money,accountId);
}
3.业务层方法
@Service
@Transactional(propagation = Propagation.REQUIRED)
public class AccountService {
@Autowired
private IAccountDao accountDao;
/**
* 转账的业务操作
* @param outId
* @param inId
* @param money
* @return
*/
public int updateAccountByTransfer(Integer outId,Integer inId,Double money){
//code表示状态码,0表示失败,1表示成功
int code=0;
//支出方法
int outRow=accountDao.outAccount(outId,money);
//收入方法
int inRow=accountDao.inAccount(inId,money);
if (inRow==1 && outRow==1){
code=1;
}
return code;
}
}
@Transactional(propagation = Propagation.REQUIRED)
表示用注解对该方法进行事务支持。
4.测试
public class TestTransfer extends BaseTest{
@Autowired
private AccountService accountService;
@Test
public void testTransfer(){
int i = accountService.updateAccountByTransfer(1, 2, 100.0);
if (i==1){
System.out.println("转账成功");
}
}
}
往期文章回顾
Apache Ant+Apache Maven—>Gradle