Spring JDBC 和 事务管制
次要内容
Spring 整合 JDBC 环境
Spring 框架除了提供 IOC 与 AOP 外围性能外,同样提供了基于 JDBC 的数据拜访性能,使得拜访长久层数据更加不便。应用 Spring JDBC 环境,首先须要一套 Spring 整合 JDBC 的环境。
增加依赖坐标
<!-- 增加相干的依赖坐标 -->
<!-- spring 框架坐标依赖增加 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.4.RELEASE</version>
</dependency>
<!-- spring 测试环境 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.4.RELEASE</version>
<scope>test</scope>
</dependency>
<!-- aop -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
<!-- spring jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.4.RELEASE</version>
</dependency>
<!-- spring 事物 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.4.RELEASE</version>
</dependency>
<!-- mysql 驱动包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<!-- c3p0 连接池 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.5</version>
</dependency>
增加 jdbc 配置文件
在 src/main/resources 目录下新建 jdbc.properties 配置文件,并设置对应的配置信息
# 驱动名
jdbc.driver=com.mysql.cj.jdbc.Driver
# 数据库连贯
jdbc.url=jdbc:mysql://localhost:3306/(数据库名称)?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
# 数据库用户名称
jdbc.user=(数据库账号)
# 数据库用户明码
jdbc.password=(数据库明码)
以下为可选配置
# 指定连接池的初始化连接数。取值应在 minPoolSize 与 maxPoolSize 之间.Default:3
initialPoolSize=20
# 指定连接池中保留的最大连接数. Default:15
maxPoolSize=100
# 指定连接池中保留的最小连接数
minPoolSize=10
# 最大闲暇工夫,60 秒内未应用则连贯被抛弃。若为 0 则永不抛弃。Default:0
maxIdleTime=600
# 当连接池中的连贯耗尽的时候 c3p0 一次同时获取的连接数. Default:3
acquireIncrement=5
# JDBC 的规范, 用以控制数据源内加载的 PreparedStatements 数量。maxStatements=5
# 每 60 秒查看所有连接池中的闲暇连贯.Default:0
idleConnectionTestPeriod=60
批改 spring 配置文件
<!-- 加载 properties 配置文件,用来读取 jdbc.properties 文件中的数据 -->
<context:property-placeholder location="jdbc.properties" />
spring.xml
<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
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Spring 扫描注解的配置 -->
<context:component-scan base-package="com.xxxx" />
<!-- 加载 properties 配置文件 -->
<context:property-placeholder location="jdbc.properties" />
</beans>
配置数据源
因为建设数据库连贯是一个十分耗时耗资源的行为,所以通过连接池事后同数据库建设一些连贯,放在内存中,应用程序须要建设数据库连贯时间接到连接池中申请一个就行,用完后再放回去。
C3P0 与 DBCP 二选一即可
DBCP(DataBase connection pool),数据库连接池。是 apache 上的一个 java 连接池我的项目,也是 tomcat 应用的连接池组件。独自应用 dbcp 须要 2 个包:commons-dbcp.jar,commons-pool.jar dbcp,没有主动回收闲暇连贯的性能。
C3P0 是一个开源的 JDBC 连接池,它实现了数据源,反对 JDBC3 标准和 JDBC2 的规范扩大。目前应用它的开源我的项目有 Hibernate,Spring 等。c3p0 有主动回收闲暇连接功能。
C3P0 数据源配置
<!-- 配置 c3p0 数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- property 标签的 value 属性对应的是 jdbc.properties 中的值 -->
<property name="driverClass" value="${jdbc.driver}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
C3P0 其余额定配置(对应的值在 jdbc.properties 文件中指定)
<!-- 指定连接池中保留的最大连接数。Default:15-->
<property name="maxPoolSize" value="${maxPoolSize}"/>
<!-- 指定连接池中保留的最小连接数。-->
<property name="minPoolSize" value="${minPoolSize}"/>
<!-- 指定连接池的初始化连接数。取值应在 minPoolSize 与 maxPoolSize 之间.Default:3-->
<property name="initialPoolSize" value="${initialPoolSize}"/>
<!-- 最大闲暇工夫,60 秒内未应用则连贯被抛弃。若为 0 则永不抛弃。Default:0-->
<property name="maxIdleTime" value="${maxIdleTime}"/>
<!-- 当连接池中的连贯耗尽的时候 c3p0 一次同时获取的连接数。Default:3-->
<property name="acquireIncrement" value="${acquireIncrement}"/>
<!-- JDBC 的规范, 用以控制数据源内加载的 PreparedStatements 数量。但因为预缓存的 statements 属于单个 connection,而不是整个连接池所以设置这个参数须要思考到多方面的因数。如果 maxStatements 与 maxStatementsPerConnection 均为 0,则缓存被敞开。Default:0-->
<property name="maxStatements" value="${maxStatements}"/>
<!-- 每 60 秒查看所有连接池中的闲暇连贯。Default:0 -->
<property name="idleConnectionTestPeriod" value="${idleConnectionTestPeriod}"/>
DBCP 数据源配置
<!-- 配置 dbcp 数据源 -->
<bean id="myDataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>
<!-- 连接池启动时的初始值 -->
<property name="initialSize" value="1"/>
<!-- 最大闲暇值. 当通过一个顶峰工夫后,连接池能够将曾经用不到的连贯缓缓开释一部分,始终缩小到 maxIdle 为止 -->
<property name="maxIdle" value="2"/>
<!-- 最小闲暇值. 当闲暇的连接数少于阀值时,连接池就会预申请一些连贯,以防止洪峰来时再申请而造成的性能开销 -->
<property name="minIdle" value="1"/>
</bean>
模板类配置
Spring 把 JDBC 中反复的操作建设成了一个模板类:org.springframework.jdbc.core.JdbcTemplate。
<!-- 配置 JdbcTemplate 实例,并注入一个 dataSource 数据源 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
JDBC 测试
创立指定数据库
抉择连贯,右键抉择 ” 新建数据库 ”,设置数据库的名称和编码格局
创立数据表
应用 JUnit 测试
通过 junit 测试 jdbcTemplate bean 是否获取到
JUnit 测试
public class SpringJdbcTest01 {
@Test
public void testQueryCount() {
// 获取 spring 上下文环境
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring.xml");
// 失去模板类 JdbcTemplate 对象
JdbcTemplate jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
// 定义 sql 语句
String sql = "select count(1) from tb_account";
// 执行查问操作(无参数)Integer total= jdbcTemplate.queryForObject(sql, Integer.class);
System.out.println("总记录数:" + total);
}
@Test
public void testQueryCountByUserId() {
// 获取 spring 上下文环境
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring.xml");
// 失去模板类 JdbcTemplate 对象
JdbcTemplate jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
// 定义 sql 语句
String sql = "select count(1) from tb_account where user_id = ?";
// 执行查问操作(有参数)Integer total = jdbcTemplate.queryForObject(sql, Integer.class, 1);
System.out.println("总记录数:" + total);
}
}
简略封装
public class SpringJdbcTest02 {
private JdbcTemplate jdbcTemplate;
@Before
public void init() {
// 失去 Spring 上下文环境
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
// 失去模板类 JdbcTemplate 对象
jdbcTemplate = (JdbcTemplate) ac.getBean("jdbcTemplate");
}
@Test
public void testQueryCount() {
// 定义 sql 语句
String sql = "select count(1) from tb_account";
// 执行查问操作(无参数)Integer total= jdbcTemplate.queryForObject(sql, Integer.class);
System.out.println("总记录数:" + total);
}
@Test
public void testQueryCountByUserId() {
// 定义 sql 语句
String sql = "select count(1) from tb_account where user_id = ?";
// 执行查问操作(有参数)Integer total = jdbcTemplate.queryForObject(sql, Integer.class, 1);
System.out.println("总记录数:" + total);
}
}
注解封装
@RunWith
就是一个运行器
@RunWith(JUnit4.class) 就是指用 JUnit4 来运行
@RunWith(SpringJUnit4ClassRunner.class) 让测试运行于 Spring 测试环境
@ContextConfiguration
Spring 整合 JUnit4 测试时,应用注解引入多个配置文件
@ContextConfiguration(Locations="classpath:applicationContext.xml")
@ContextConfiguration(locations = {"classpath:spring.xml", "classpath:bean.xml"})
@RunWith(SpringJUnit4ClassRunner.class) // 将 junit 测试加到 spring 环境中
@ContextConfiguration(locations = {"classpath:spring.xml"}) // 设置要加载的资源文件
public class SpringJdbcTest03 {
@Resource
private JdbcTemplate jdbcTemplate;
@Test
public void testQueryCount() {
// 定义 sql 语句
String sql = "select count(1) from tb_account";
// 执行查问操作(无参数)Integer total= jdbcTemplate.queryForObject(sql, Integer.class);
System.out.println("总记录数:" + total);
}
}
通用封装
-
定义一个父类,设置通用的配置信息
/** * 通用的测试环境,须要应用环境的间接继承类即可 */ @RunWith(SpringJUnit4ClassRunner.class) // 将 junit 测试加到 spring 环境中 @ContextConfiguration(locations = {"classpath:spring.xml"}) // 设置要加载的资源文件 public class BaseTest {}
-
继承通用的测试类
public class SpringJdbcTest04 extends BaseTest { @Resource private JdbcTemplate jdbcTemplate; @Test public void testQueryCount() { // 定义 sql 语句 String sql = "select count(1) from tb_account"; // 执行查问操作(无参数)Integer total= jdbcTemplate.queryForObject(sql, Integer.class); System.out.println("总记录数:" + total); } }
长久层账户模块操作
当实现 Spring Jdbc 环境集成后,这里应用 spring jdbc 实现账户单表 crud 操作。
账户接口办法定义
定义实体类
Account.java
package com.xxxx.entity;
import java.util.Date;
/**
* 用户账户类
*/
public class Account {
private Integer accountId; // 账户 ID,主键
private String accountName; // 账户名称
private String accountType; // 账户类型
private Double money; // 账户金额
private String remark; // 账户备注
private Integer userId; // 用户 ID,账户所属用户
private Date createTime; // 创立工夫
private Date updateTime; // 批改工夫
public Account() {}
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;
}
@Override
public String toString() {
return "Account{" +
"accountId=" + accountId +
", accountName='" + accountName + '''+", accountType='"+ accountType +''' +
", money=" + money +
", remark='" + remark + '''+", userId="+ userId +", createTime="+ createTime +", updateTime="+ updateTime +'}';
}
public Integer getAccountId() {return accountId;}
public void setAccountId(Integer accountId) {this.accountId = accountId;}
public String getAccountName() {return accountName;}
public void setAccountName(String accountName) {this.accountName = accountName;}
public String getAccountType() {return accountType;}
public void setAccountType(String accountType) {this.accountType = accountType;}
public Double getMoney() {return money;}
public void setMoney(Double money) {this.money = money;}
public String getRemark() {return remark;}
public void setRemark(String remark) {this.remark = remark;}
public Integer getUserId() {return userId;}
public void setUserId(Integer userId) {this.userId = userId;}
public Date getCreateTime() {return createTime;}
public void setCreateTime(Date createTime) {this.createTime = createTime;}
public Date getUpdateTime() {return updateTime;}
public void setUpdateTime(Date updateTime) {this.updateTime = updateTime;}
}
定义接口类
IAccountDao.java
package com.xxxx.dao;
import com.xxxx.entity.Account;
import java.util.List;
/**
* 用户模块 接口定义
* 1. 增加账户
* 增加账户记录,返回受影响的行数
* 增加账户记录,返回记录的主键
* 批量增加账户记录,返回受影响的行数
* 2. 查问账户
* 查问指定用户的账户总记录数,返回记录数
* 查问指定账户记录详情,返回账户对象
* 多条件查问指定用户的账户列表,返回账户汇合
* 3. 更新账户
* 更新账户记录,返回受影响的行数
* 批量更新账户记录,返回受影响的行数
* 4. 删除账户
* 删除账户记录,返回受影响的行数
* 批量删除账户记录,返回受影响的行数
*/
public interface IAccountDao {
/**
* 增加账户
* 增加账户记录,返回受影响的行数
* @param account
* @return
*/
public int addAccount(Account account) ;
/**
* 增加账户
* 增加账户记录,返回记录的主键
* @param account
* @return
*/
public int addAccountHasKey(Account account);
/**
* 增加账户
* 批量增加账户记录,返回受影响的行数
* @param accounts
* @return
*/
public int addAccountBatch(List<Account> accounts);
/**
* 查问账户
* 查问指定用户的账户总记录数,返回记录数
* @param userId
* @return
*/
public int queryAccountCount(Integer userId);
/**
* 查问账户
* 查问指定账户记录详情,返回账户对象
* @param accountId
* @return
*/
public Account queryAccountById(Integer accountId);
/**
* 查问账户
* 多条件查问指定用户的账户列表,返回账户汇合
* @param userId
* @param accountName
* @param accountType
* @param createTime
* @return
*/
public List<Account> queryAccountsByParams(Integer userId, String accountName, String accountType, String createTime);
/**
* 更新账户
* 更新账户记录,返回受影响的行数
* @param account
* @return
*/
public int updateAccountById(Account account);
/**
* 更新账户
* 批量更新账户记录,返回受影响的行数
* @param accounts
* @return
*/
public int updateAccountBatch(List<Account> accounts);
/**
* 删除账户
* 删除账户记录,返回受影响的行数
* @param accountId
* @return
*/
public Integer deleteAccoutById(Integer accountId);
/**
* 删除用户
* 批量删除账户记录,返回受影响的行数
* @param ids
* @return
*/
public int deleteAccountBatch(Integer[] ids);
}
定义接口实现类
package com.xxxx.dao.impl;
import com.xxxx.dao.IAccountDao;
import com.xxxx.entity.Account;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
/**
* 账户模块接口实现类
*/
@Repository
public class AccountDaoImpl implements IAccountDao {
// JdbcTemplate 模板类注入
@Resource
private JdbcTemplate jdbcTemplate;
@Override
public int addAccount(Account account) {return 0;}
@Override
public int addAccountHasKey(Account account) {return 0;}
@Override
public int addAccountBatch(List<Account> accounts) {return 0;}
@Override
public int queryAccountCount(Integer userId) {return 0;}
@Override
public Account queryAccountById(Integer accountId) {return null;}
@Override
public List<Account> queryAccountsByParams(Integer userId, String accountName, String accountType, String createTime) {return null;}
@Override
public int updateAccountById(Account account) {return 0;}
@Override
public int updateAccountBatch(List<Account> accounts) {return 0;}
@Override
public Integer deleteAccoutById(Integer accountId) {return null;}
@Override
public int deleteAccountBatch(Integer[] ids) {return 0;}
}
账户记录增加实现
在企业我的项目开发时,对于记录的增加可能波及到多种增加形式,比方增加单条记录,批量增加多条记录等状况。这里对于账户记录增加形式分为三种形式:增加单条记录返回受影响行数、增加单条记录返回主键、批量增加多条记录。
增加账户记录
/**
* 增加单条记录,返回受影响的行数
* @param account
* @return
*/
@Override
public int addAccount(Account account) {
String sql = "insert into tb_account(account_name,account_type,money,remark," +
"user_id,create_time,update_time) values (?,?,?,?,?,now(),now())";
Object[] objs = {account.getAccountName(),account.getAccountType(),
account.getMoney(),account.getRemark(),account.getUserId()};
return jdbcTemplate.update(sql,objs);
}
测试方法
/**
* 增加账户记录,失去受影响的行数
*/
@Test
public void testAddAccount() {
// 筹备要增加的数据
Account account = new Account("张三","建设银行",100.0,"零花钱",1);
// 调用对象的增加办法,返回受影响的行数
int row = accountDao.addAccount(account);
System.out.println("增加账户受影响的行数:" + row);
}
增加记录返回主键
/**
* 增加单条记录,返回主键
* @param account
* @return
*/
@Override
public int addAccountHasKey(Account account) {
String sql = "insert into tb_account(account_name,account_type,money,remark," +
"user_id,create_time,update_time) values (?,?,?,?,?,now(),now())";
// 定义 keyHolder 对象 获取记录主键值
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
// 预编译 sql 语句,并设置返回主键
PreparedStatement ps =
connection.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);
// 失去返回的主键
Integer key = keyHolder.getKey().intValue();
return key;
}
测试方法
/**
* 增加账户记录,返回主键
*/
@Test
public void testAddAccountHasKey() {
// 筹备要增加的数据
Account account = new Account("李四","招商银行",200.0,"兼职费",2);
// 调用对象的增加办法,返回主键
int key = accountDao.addAccountHasKey(account);
System.out.println("增加账户返回的主键:" + key);
}
批量增加账户记录
/**
* 增加多条记录,返回受影响的行数
* @param accounts
* @return
*/
@Override
public int addAccountBatch(final List<Account> accounts) {
String sql = "insert into tb_account(account_name,account_type,money,remark," +
"user_id,create_time,update_time) values (?,?,?,?,?,now(),now())";
int rows = jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement preparedStatement, int i)
throws SQLException {
// 设置参数
preparedStatement.setString(1,accounts.get(i).getAccountName());
preparedStatement.setString(2,accounts.get(i).getAccountType());
preparedStatement.setDouble(3,accounts.get(i).getMoney());
preparedStatement.setString(4,accounts.get(i).getRemark());
preparedStatement.setInt(5,accounts.get(i).getUserId());
}
@Override
public int getBatchSize() {return accounts.size();
}
}).length;
return rows;
}
测试方法
/**
* 批量增加数据,返回受影响的行数
*/
@Test
public void testAddAccountBatch() {
// 筹备要增加的数据
Account account = new Account("王五","农业银行",2000.0,"工资",3);
Account account2 = new Account("赵六","中国银行",280.0,"奖金",3);
Account account3 = new Account("田七","工商银行",800.0,"零花钱",3);
List<Account> accountList = new ArrayList<>();
accountList.add(account);
accountList.add(account2);
accountList.add(account3);
// 调用对象的增加办法,返回主键
int rows = accountDao.addAccountBatch(accountList);
System.out.println("批量增加账户受影响的行数:" + rows);
}
账户记录查问实现
账户记录查问这里提供了三种查问形式,查问指定用户所有账户记录数,查问单条账户记录详情,多条件查问指定用户账户记录。
查问用户的账户总记录数
/**
* 查问指定用户的账户总记录数,返回记录数
* @param userId
* @return
*/
@Override
public int queryAccountCount(Integer userId) {String sql = "select count(1) from tb_account where user_id = ?";
int count = jdbcTemplate.queryForObject(sql,Integer.class,userId);
return count;
}
测试方法
/**
* 查问用户的账户总记录数,返回总记录数
*/
@Test
public void testQueryAccountCount(){
// 查问 ID 为 1 的用户的账户总记录数
int total = accountDao.queryAccountCount(1);
System.out.println("总记录数:" + total);
}
查问指定账户记录详情
/**
* 查问某个账户记录详情,返回账户对象
* @param accountId
* @return
*/
@Override
public Account queryAccountById(Integer accountId) {
String sql = "select * from tb_account where account_id = ?";
Account account = jdbcTemplate.queryForObject(sql, new Object[]{accountId}, (resultSet, i) -> {Account acc = new Account();
acc.setAccountId(resultSet.getInt("account_id"));
acc.setMoney(resultSet.getDouble("money"));
acc.setAccountName(resultSet.getString("account_name"));
acc.setAccountType(resultSet.getString("account_type"));
acc.setRemark(resultSet.getString("remark"));
acc.setCreateTime(resultSet.getDate("create_time"));
acc.setUpdateTime(resultSet.getDate("update_time"));
acc.setUserId(resultSet.getInt("user_id"));
return acc;
});
return account;
}
测试方法
/**
* 查问指定账户的记录详情,返回账户对象
*/
@Test
public void testQueryAccountById(){
// 查问 ID 为 1 的账户记录的详情
Account account = accountDao.queryAccountById(1);
System.out.println("账户详情:" + account.toString());
}
多条件查问用户账户记录
/**
* 多条件查问指定用户的账户列表,返回账户汇合
* @param userId 用户 Id
* @param accountName 账户名称(含糊查问)* @param accountType 账户类型
* @param createTime 账户创立工夫
* @return
*/
@Override
public List<Account> queryAccountsByParams(Integer userId, String accountName, String accountType,
String createTime) {
String sql = "select * from tb_account where user_id = ?";
List<Object> params = new ArrayList<>();
params.add(userId);
// 判断是否有条件查问
// 如果账户名称不为空,通过账户名称含糊匹配
if (StringUtils.isNotBlank(accountName)) {sql += "and account_name like concat('%',?,'%')";
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[] objs = params.toArray();
List<Account> accountList = jdbcTemplate.query(sql, objs, (resultSet, rowNum) -> {Account acc = new Account();
acc.setAccountId(resultSet.getInt("account_id"));
acc.setMoney(resultSet.getDouble("money"));
acc.setAccountName(resultSet.getString("account_name"));
acc.setAccountType(resultSet.getString("account_type"));
acc.setRemark(resultSet.getString("remark"));
acc.setCreateTime(resultSet.getDate("create_time"));
acc.setUpdateTime(resultSet.getDate("update_time"));
acc.setUserId(resultSet.getInt("user_id"));
return acc;
});
return accountList;
}
测试方法
/**
* 多条件查问用户的账户记录,返回账户汇合
*/
@Test
public void testQueryAccountByParams(){
// 查问用户的账户列表
List<Account> accountList = accountDao.queryAccountsByParams(3,null,null,null);
// 通过指定条件查问用户的账户列表
List<Account> accountList02 = accountDao.queryAccountsByParams(3,"张",null,null);
System.out.println(accountList.toString());
System.out.println(accountList02.toString());
}
账户记录更新实现
更新账户记录
/**
* 更新指定账户记录,返回受影响的行数
* @param account
* @return
*/
@Override
public int updateAccountById(Account account) {
String sql = "update tb_account set account_name = ?, account_type = ?," +
"money = ? ,remark = ?,user_id = ? ,update_time = now()" +
"where account_id = ?";
Object[] objs = {account.getAccountName(),account.getAccountType(),
account.getMoney(), account.getRemark(),account.getUserId(),
account.getAccountId()};
return jdbcTemplate.update(sql,objs);
}
测试方法
/**
* 更新指定账户记录,返回受影响的行数
*/
@Test
public void testUpdateAccount(){
// 筹备要批改的数据
Account account = new Account("张三 1","建设银行 1",500.0,"零花钱加倍",1);
account.setAccountId(1);
int row = accountDao.updateAccountById(account);
System.out.println("批改账户返回受影响的行数:" + row);
}
批量更新账户记录
/**
* 批量新账户记录,返回受影响的行数
* @param accounts
* @return
*/
@Override
public int updateAccountBatch(List<Account> accounts) {
String sql = "update tb_account set account_name = ?, account_type = ?," +
"money = ? ,remark = ?,user_id = ? ,update_time = now()" +
"where account_id = ?";
int rows = jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
// 设置参数
ps.setString(1,accounts.get(i).getAccountName());
ps.setString(2,accounts.get(i).getAccountType());
ps.setDouble(3,accounts.get(i).getMoney());
ps.setString(4,accounts.get(i).getRemark());
ps.setInt(5,accounts.get(i).getUserId());
ps.setInt(6,accounts.get(i).getAccountId());
}
@Override
public int getBatchSize() {return accounts.size();
}
}).length;
return rows;
}
测试方法
/**
* 批量更新账户记录,返回受影响的行数
*/
@Test
public void testUpdateAccountBatch(){
// 筹备要批改的数据
Account account = new Account("a3","建设银行 3",300.0,"零花钱加倍 3",3);
account.setAccountId(3);
Account account2 = new Account("a4","建设银行 4",400.0,"零花钱加倍 4",3);
account2.setAccountId(4);
List<Account> accountList = new ArrayList<>();
accountList.add(account);
accountList.add(account2);
int rows = accountDao.updateAccountBatch(accountList);
System.out.println("批量批改账户记录返回受影响的行数:" + rows);
}
账户记录删除实现
删除账户记录
/**
* 删除账户记录,返回受影响的行数
* @param accountId
* @return
*/
@Override
public Integer deleteAccoutById(Integer accountId) {
String sql = "delete from tb_account where account_id= ?";
Object[] objs = {accountId};
return jdbcTemplate.update(sql,objs);
}
测试方法
/**
* 删除账户记录,返回受影响的行数
*/
@Test
public void testDeleteAccount(){
// 删除 ID 为 1 的账户记录
int row = accountDao.deleteAccoutById(1);
System.out.println("删除账户记录返回受影响的行数:" + row);
}
批量删除账户记录
/**
* 批量删除账户记录,返回受影响的行数
* @param ids
* @return
*/
@Override
public int deleteAccountBatch(Integer[] ids) {
String sql = "delete from tb_account where account_id = ?";
int row = jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {ps.setInt(1,ids[i]);
}
@Override
public int getBatchSize() {return ids.length;}
}).length;
return row;
}
测试方法
/**
* 批量删除账户记录,返回受影响的行数
*/
@Test
public void testDeleteAccountBatch(){
// 删除多个 id 的账户记录
Integer[] ids = new Integer[]{2,3};
int rows = accountDao.deleteAccountBatch(ids);
System.out.println("批量删除账户记录返回受影响的行数:" + rows);
}
Spring 事务管制
转账场景模仿实现
接口办法定义
/**
* 支出
* @param tarAid 支出金额的账户 ID
* @param money 支出金额
* @return
*/
public int inAccount(Integer tarAid, Double money);
/**
* 收入
* @param outAid 收入金额的账户 ID
* @param money 收入金额
* @return
*/
public int outAccount(Integer outAid, Double money);
实现对应接口
对于转账波及到单方账户以及对应转账金额,所以有入账和出账两个办法。
/**
* 账户支出
* @param tarAid 账户 ID
* @param money 支出金额
* @return
*/
@Override
public int inAccount(Integer tarAid, Double money) {// 批改指定 ID 的金额 ( 加上金额)
String sql = "update tb_account set money = money + ? where account_id = ?";
Object[] objs = {money, tarAid};
return jdbcTemplate.update(sql,objs);
}
/**
* 账户收入
* @param outAid 账户 ID
* @param money 收入金额
* @return
*/
@Override
public int outAccount(Integer outAid, Double money) {
// 批改指定 ID 的金额(减去金额)String sql = "update tb_account set money = money - ? where account_id = ?";
Object[] objs = {money, outAid};
return jdbcTemplate.update(sql,objs);
}
转账办法实现
package com.xxxx.service;
import com.xxxx.dao.IAccountDao;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class AccountService {
@Resource
private IAccountDao accountDao;
/**
* 转账业务操作
* @param outAid 收入账户
* @param inAid 支出账户
* @param money 收入金额 / 支出金额
* @return
*/
public int updateAccountByTransfer(Integer outAid, Integer inAid, Double money){
int row = 0;
/**
* 张三账户向李四账户转账 100 元
* 张三账户的金额 - 100
* 李四账户的金额 + 100
*/
// 收入,批改金额返回受影响的行数
int outRow = accountDao.outAccount(1,100.0);
// 支出,批改金额返回受影响的行数
int inRow = accountDao.inAccount(2,100.0);
// 当两个操作都执行胜利时,转账胜利
if (outRow == 1 && inRow == 1) {row = 1; // 胜利}
return row;
}
}
认真思考代码会发现,在程序运行中无奈保障 service 层业务代码不产生异样,如果通过 jdbc 的形式处理事务,此时须要手动形式管制事务,这样的话但凡波及到事务管制的业务办法均须要开发人员手动来进行事务处理,无奈满足生产的须要。
Spring 事务概念
事务的四大个性(ACID)
- 原子性(Atomicity)
共生死,要么全副胜利,要么全副失败!
- 一致性(Consistency)
事务在执行前后,数据库中数据要放弃一致性状态。(如转账的过程 账户操作后数据必须保持一致)
- 隔离性(Isolation)
事务与事务之间的执行该当是互相隔离互不影响的。(多个角色对对立记录进行操作必须保障没有任何烦扰),当然没有影响是不可能的,为了让影响级别降到最低,通过隔离级别加以限度:
1. READ_UNCOMMITTED(读未提交)
隔离级别最低的一种事务级别。在这种隔离级别下,会引发脏读、不可反复读和幻读。
2. READ_COMMITTED(读已提交)
读到的都是他人提交后的值。这种隔离级别下,会引发不可反复读和幻读,但防止了脏读。
3. REPEATABLE_READ(可反复读)
这种隔离级别下,会引发幻读,但防止了脏读、不可反复读。
4. SERIALIZABLE(串行化)
最严格的隔离级别。在 Serializable 隔离级别下,所有事务依照秩序顺次执行。
脏读、不可反复读、幻读都不会呈现。
- 持久性(Durability)
事务提交结束后,数据库中的数据的扭转是永恒的。
Spring 事务外围接口
Spring 事务管理的实现有许多细节,如果对整个接口框架有个大体理解会十分有利于咱们了解事务,上面通过解说 Spring 的事务接口来理解 Spring 实现事务的具体策略。
Spring 并不间接治理事务,而是提供了多种事务管理器,他们将事务管理的职责委托给 Hibernate 或者 JTA 等长久化机制所提供的相干平台框架的事务来实现。
Spring 事务管理器的接口是 org.springframework.transaction.PlatformTransactionManager,通过这个接口,Spring 为各个平台如 JDBC、Hibernate 等都提供了对应的事务管理器,然而具体的实现就是各个平台本人的事件了。此接口的内容如下:
public interface PlatformTransactionManager(){
// 由 TransactionDefinition 失去 TransactionStatus 对象
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
// 提交
void commit(TransactionStatus status) throws TransactionException;
// 回滚
void rollback(TransactionStatus status) throws TransactionException;
}
从这里可知具体的具体的事务管理机制对 Spring 来说是通明的,它并不关怀那些,那些是对应各个平台须要关怀的,所以 Spring 事务管理的一个长处就是为不同的事务 API 提供统一的编程模型,如 JTA、JDBC、Hibernate、JPA。上面别离介绍各个平台框架实现事务管理的机制。
JDBC 事务
如果应用程序中间接应用 JDBC 来进行长久化,此时应用 DataSourceTransactionManager 来处理事务边界。为了应用 DataSourceTransactionManager,须要应用如下的 XML 将其拆卸到应用程序的上下文定义中:
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
实际上,DataSourceTransactionManager 是通过调用 java.sql.Connection 来治理事务,而后者是通过 DataSource 获取到的。通过调用连贯的 commit() 办法来提交事务,同样,事务失败则通过调用 rollback() 办法进行回滚。
Hibernate 事务
如果应用程序的长久化是通过 Hibernate 实现的,那么你须要应用 HibernateTransactionManager。对于 Hibernate3,须要在 Spring 上下文定义中增加如下的申明:
<bean id="transactionManager"
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() 办法。
Java 长久化 API 事务(JPA)
Hibernate 多年来始终是 Java 长久化规范,然而当初 Java 长久化 API 作为真正的 Java 长久化规范进入大家的视线。如果你打算应用 JPA 的话,那你须要应用 Spring 的 JpaTransactionManager 来处理事务。你须要在 Spring 中这样配置 JpaTransactionManager:
<bean id="transactionManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
JpaTransactionManager 只须要拆卸一个 JPA 实体治理工厂(javax.persistence.EntityManagerFactory 接口的任意实现)。JpaTransactionManager 将与由工厂所产生的 JPA EntityManager 单干来构建事务。
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 配置,注解配置。
XML 配置
增加命名空间
在 spring.xml 配置文件的增加事务和 aop 的命名空间
事务
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
配置如下
<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
http://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">
设置 aop 代理
<!-- 开启 AOP 代理 -->
<aop:aspectj-autoproxy />
配置事务管理器
<!-- 事务管理器定义 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 数据源 -->
<property name="dataSource" ref="dataSource"></property>
</bean>
配置事务相干告诉
一般来说增删改办法 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);以逗号离开。如:'com.foo.MyBusinessException,ServletException'
no-rollback-for
不是必须的
示意不被触发进行回滚的 Exception(s);以逗号离开。如:'com.foo.MyBusinessException,ServletException'
任何 RuntimeException 将触发事务回滚
-->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- 对以 add update delete query 结尾的所有办法进行事务处理 -->
<tx:attributes>
<!-- 定义什么办法须要应用事务 name 代表的是办法名(或办法匹配)-->
<!-- 匹配以 add 结尾的所有办法均退出事务 -->
<tx:method name="add*" propagation="REQUIRED" />
<!-- 匹配以 update 结尾的所有办法均退出事务 -->
<tx:method name="update*" propagation="REQUIRED" />
<!-- 匹配以 delete 结尾的所有办法均退出事务 -->
<tx:method name="delete*" propagation="REQUIRED" />
<!-- 匹配以 query 结尾的所有办法均退出事务 -->
<tx:method name="query*" read-only="true" />
</tx:attributes>
</tx:advice>
事务流传行为介绍:
@Transactional(propagation=Propagation.REQUIRED)
如果有事务, 那么退出事务, 没有的话新建一个 (默认状况下)
@Transactional(propagation=Propagation.NOT_SUPPORTED)
容器不为这个办法开启事务
@Transactional(propagation=Propagation.REQUIRES_NEW)
不论是否存在事务, 都创立一个新的事务, 原来的挂起, 新的执行结束, 继续执行老的事务
@Transactional(propagation=Propagation.MANDATORY)
必须在一个已有的事务中执行, 否则抛出异样
@Transactional(propagation=Propagation.NEVER)
必须在一个没有的事务中执行, 否则抛出异样 (与 Propagation.MANDATORY 相同)
@Transactional(propagation=Propagation.SUPPORTS)
如果其余 bean 调用这个办法, 在其余 bean 中申明事务, 那就用事务.
如果其余 bean 没有申明事务, 那就不必事务.
@Transactional(propagation=Propagation.NESTED)
反对以后事务,如果以后事务存在,则执行一个嵌套事务,如果以后没有事务,就新建一个事务。
配置 aop
<!-- aop 切面定义(切入点和告诉)-->
<aop:config>
<!-- 设置切入点 设置须要被拦挡的办法 -->
<aop:pointcut expression="execution(* com.xxxx.service..*.*(..) )" id="cut" />
<!-- 设置告诉 事务告诉 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="cut"/>
<aop:adviso
注解配置
配置事务管理器
<!-- spring 注解式事务申明 -->
<!-- 事务管理器定义 -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
配置注解反对
<tx:annotation-driven transaction-manager="txManager"/>
办法上退出事务注解
Service 办法上在须要增加事务的办法上退出事务注解
@Override
@Transactional(propagation=Propagation.REQUIRED)
public void saveUser(String userName,String userPwd){User user1=new User();
user1.setUserName(userName);
user1.setUserPwd(userPwd);
userDao.saveUser(user1);
userDao.delUserById(2);
}
备注:默认 spring 事务只在产生未被捕捉的 runtimeexcetpion 时才回滚。
spring aop 异样捕捉原理:
被拦挡的办法需显式抛出异样,并不能经任何解决,这样 aop 代理能力捕捉到办法的异样,能力进行回滚,默认状况下 aop 只捕捉 runtimeexception 的异样,但能够通过配置来捕捉特定的异样并回滚换句话说在 service 的办法中不应用 try catch 或者在 catch 中最初加上 throw new RunTimeexcetpion(),这样程序异样时能力被 aop 捕捉进而回滚.
ropagation.MANDATORY 相同 )
@Transactional(propagation=Propagation.SUPPORTS)
如果其余 bean 调用这个办法, 在其余 bean 中申明事务, 那就用事务.
如果其余 bean 没有申明事务, 那就不必事务.
@Transactional(propagation=Propagation.NESTED)
反对以后事务,如果以后事务存在,则执行一个嵌套事务,如果以后没有事务,就新建一个事务。
##### 配置 aop
<!– aop 切面定义(切入点和告诉)–>
<aop:config>
<!-- 设置切入点 设置须要被拦挡的办法 -->
<aop:pointcut expression="execution(* com.xxxx.service..*.*(..) )" id="cut" />
<!-- 设置告诉 事务告诉 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="cut"/>
<aop:adviso
#### 注解配置
##### 配置事务管理器
<!– spring 注解式事务申明 –>
<!– 事务管理器定义 –>
<bean id=”txManager”
class=”org.springframework.jdbc.datasource.DataSourceTransactionManager”>
<property name="dataSource" ref="dataSource"></property>
</bean>
##### 配置注解反对
<tx:annotation-driven transaction-manager=”txManager”/>
##### 办法上退出事务注解
Service 办法上在须要增加事务的办法上退出事务注解
@Override
@Transactional(propagation=Propagation.REQUIRED)
public void saveUser(String userName,String userPwd){
User user1=new User();
user1.setUserName(userName);
user1.setUserPwd(userPwd);
userDao.saveUser(user1);
userDao.delUserById(2);
}
** 备注:默认 spring 事务只在产生未被捕捉的 runtimeexcetpion 时才回滚。**
**spring aop 异样捕捉原理:**