Spring JDBC 和 事务控制
一、Spring 整合 JDBC 环境
Spring 框架除了提供 IOC 与 AOP 核心功能外,同样提供了基于JDBC 的数据访问功能,使得访问持久层数据更加方便。使用Spring JDBC 环境,首先需要一套 Spring 整合 JDBC 的环境。
1.添加依赖坐标
<!-- 配置相关的依赖坐标 -->
<!-- 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>
<!-- commons-lang-->
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
2.添加 jdbc 配置文件
在src/main/resources目录下新建jdbc.properties配置文件,并设置对应的配置信息
# 驱动名
jdbc.driver=com.mysql.cj.jdbc.Driver
# 数据库连接
jdbc.url=jdbc:mysql://localhost:3306/spring_jdbc?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
# 数据库用户名称
jdbc.user=root
# 数据库用户密码
jdbc.password=root
#以下为可选配置
# 指定连接池的初始化连接数。取值应在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
3.修改 spring 配置文件
<!-- 加载properties配置,可以读取jdbc.properties配置文件中的数据 -->
<context:property-placeholder location="jdbc.properties"/>
<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"/>
<!-- 开启自动化扫描 -->
<context:component-scan base-package="com.xxxx" />
<!-- 加载properties配置,可以读取jdbc.properties配置文件中的数据 -->
<context:property-placeholder location="jdbc.properties"/>
</beans>
4.配置数据源
由于建立数据库连接是一个非常耗时耗资源的行为,所以通过连接池预先同数据库建立一些连接,放在内存中,应用程序需要建立数据库连接时直接到连接池中申请一个就行,用完后再放回去。
DBCP(DataBase connection pool),数据库连接池。是 apache 上的一个 java 连接池项目,也是 tomcat 使用的连接池组件。单独使用dbcp需要2个包:commons-dbcp.jar,commons-pool.jardbcp,没有自动回收空闲连接的功能。
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.user}"/>
<property name="password" value="${jdbc.password}"/>
</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>
5.模板类配置
Spring把 JDBC 中重复的操作建立成了一个模板类:org.springframework.jdbc.core.JdbcTemplate 。
<!-- 配置JdbcTemplate模板对象,并注入一个数据源 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
6.使用 JUnit 测试
通过 junit 测试 jdbcTemplate bean 是否获取到
@Test
public void testJdbc(){
// 获取Spring的上下文环境
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
// 得到模板类 JdbcTemplate对象
JdbcTemplate jdbcTemplate = (JdbcTemplate) ac.getBean("jdbcTemplate");
// crud 操作
// 定义sql语句
String sql = "select count(1) from tb_account";
// 执行查询操作 (无参数)
Integer total = jdbcTemplate.queryForObject(sql,Integer.class);
System.out.println("总记录数:" + total);
}
@Test
public void testJdbc02(){
// 获取Spring的上下文环境
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
// 得到模板类 JdbcTemplate对象
JdbcTemplate jdbcTemplate = (JdbcTemplate) ac.getBean("jdbcTemplate");
// crud 操作
// 定义sql语句
String sql = "select count(1) from tb_account where user_id = ?";
// 执行查询操作 (无参数)
Integer total = jdbcTemplate.queryForObject(sql,Integer.class,2);
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 testJdbc(){
// crud 操作
// 定义sql语句
String sql = "select count(1) from tb_account";
// 执行查询操作 (无参数)
Integer total = jdbcTemplate.queryForObject(sql,Integer.class);
System.out.println("总记录数:" + total);
}
@Test
public void testJdbc02(){
// crud 操作
// 定义sql语句
String sql = "select count(1) from tb_account where user_id = ?";
// 执行查询操作 (无参数)
Integer total = jdbcTemplate.queryForObject(sql,Integer.class,2);
System.out.println("总记录数:" + total);
}
}
注解封装
@RunWith(SpringJUnit4ClassRunner.class) // 将测试运行在Spring测试环境中
@ContextConfiguration(locations = {"classpath:spring.xml"}) // 设置要加载的配置文件
public class SpringJdbcTest03 {
@Resource
private JdbcTemplate jdbcTemplate;
@Test
public void testJdbc(){
// crud 操作
// 定义sql语句
String sql = "select count(1) from tb_account";
// 执行查询操作 (无参数)
Integer total = jdbcTemplate.queryForObject(sql,Integer.class);
System.out.println("总记录数:" + total);
}
@Test
public void testJdbc02(){
// crud 操作
// 定义sql语句
String sql = "select count(1) from tb_account where user_id = ?";
// 执行查询操作 (无参数)
Integer total = jdbcTemplate.queryForObject(sql,Integer.class,2);
System.out.println("总记录数:" + total);
}
}
通用封装
- 定义一个父类,设置通用的配置信息
@RunWith(SpringJUnit4ClassRunner.class) // 将测试运行在Spring测试环境中
@ContextConfiguration(locations = {"classpath:spring.xml"}) // 设置要加载的配置文件
public class BaseTest {
}
继承通用的测试类
public class SpringJdbcTest04 extends BaseTest {
@Resource
private JdbcTemplate jdbcTemplate;
@Test
public void testJdbc(){
// crud 操作
// 定义sql语句
String sql = "select count(1) from tb_account";
// 执行查询操作 (无参数)
Integer total = jdbcTemplate.queryForObject(sql,Integer.class);
System.out.println("总记录数:" + total);
}
@Test
public void testJdbc02(){
// crud 操作
// 定义sql语句
String sql = "select count(1) from tb_account where user_id = ?";
// 执行查询操作 (无参数)
Integer total = jdbcTemplate.queryForObject(sql,Integer.class,2);
System.out.println("总记录数:" + total);
}
}
二、持久层账户模块操作
当完成 Spring Jdbc 环境集成后,这里使用spring jdbc 完成账户单表crud 操作。
1.账户接口方法定义
定义实体类
Account.java
定义接口类
IAccountDao.java
定义接口实现类
AccountDaoImpl.java
2.账户记录添加实现
在企业项目开发时,对于记录的添加可能涉及到多种添加方式,比如添加单条记录,批量添加多条记录等情况。这里对于账户记录添加方式分为三种方式:添加单条记录返回受影响行数、添加单条记录返回主键、批量添加多条记录。
添加账户记录
添加单条记录,返回受影响的行数
测试方法
准备要添加的数据
调用对象的添加方法,返回受影响的行数
添加记录返回主键
添加单条记录,返回主键
测试方法
准备要添加的数据
调用对象的添加方法,返回主键
批量添加账户记录
添加多条记录,返回受影响的行数
测试方法
准备要添加的数据
调用对象的添加方法,返回受影响的行数
3.账户记录查询实现
账户记录查询这里提供了三种查询方式,查询指定用户所有账户记录数,查询单条账户记录详情,多条件查询指定用户账户记录。
查询用户的账户总记录数
查询指定用户的账户总记录数,返回记录数
测试方法
查询用户的账户总记录数,返回总记录数
查询指定账户记录详情
查询某个账户记录详情,返回账户对象
测试方法
查询指定账户的记录详情,返回账户对象
多条件查询用户账户记录
多条件查询指定用户的账户列表,返回账户集合
测试方法
多条件查询用户的账户记录,返回账户集合
4.账户记录更新实现
更新账户记录
更新指定账户记录,返回受影响的行数
批量更新账户记录
批量新账户记录,返回受影响的行数
5.账户记录删除实现
删除账户记录
删除账户记录,返回受影响的行数
批量删除账户记录
批量删除账户记录,返回受影响的行数
三、Spring 事务控制
1.转账场景模拟实现
接口方法定义
/**
* 收入
* @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 的方式处理事务,此时需要手动方式控制事务,这样的话凡是涉及到事务控制的业务方法均需要开发人员手动来进行事务处理,无法满足生产的需要。
2.Spring 事务概念
事务的四大特性(ACID)
原子性(Atomicity)
共生死,要么全部成功,要么全部失败!
一致性(Consistency)
事务在执行前后,数据库中数据要保持一致性状态。(如转账的过程 账户操作后数据必须保持一致)
隔离性(Isolation)
事务与事务之间的执行应当是相互隔离互不影响的。(多个角色对统一记录进行操作必须保证没有任何干扰),当然没有影响是不可能的,为了让影响级别降到最低,通过隔离级别加以限制:
- READ_UNCOMMITTED (读未提交)
隔离级别最低的一种事务级别。在这种隔离级别下,会引发脏读、不可重复读和幻读。 - READ_COMMITTED (读已提交)
读到的都是别人提交后的值。这种隔离级别下,会引发不可重复读和幻读,但避免了脏读。 - REPEATABLE_READ (可重复读)
这种隔离级别下,会引发幻读,但避免了脏读、不可重复读。 - SERIALIZABLE (串行化)
最严格的隔离级别。在Serializable隔离级别下,所有事务按照次序依次执行。
脏读、不可重复读、幻读都不会出现。
持久性(Durability)
事务提交完毕后,数据库中的数据的改变是永久的。
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;
}