JDBC_事务_连接池
文章目录
1.JDBC事务管理(重点)
1.1. JDBC事务管理概述
事务(Transaction):将多个更性能操作捆绑为一个逻辑单元,这些操作在该逻辑单元中,要么同时成功,要么同时失败。事务的特征:
- 原子性(不可再分)
- 一致性(执行前后总数据结果一致)
- 隔离性(事务间相互隔离,互不影响)
- 持久性(数据最终持久化保存)
JDBC中用于事务管理的接口是:java.sql.Connection
,内部包含一些预定义的事务管理方法:
setAutoCommit(b)
:设置事务是否自动提交(默认值true)commit()
:提交事务rollback()
:事务回滚
JDBC中
Connection
默认是进行事务自动提交的
1.2. 事务自动提交存在的问题
由于JDBC·Connection
事务提交默认是自动开启,所以在实际开发中可能遇到如下问题:
/**
* 业务逻辑层:
* 1. 完成业务方法逻辑判断
* 2. 事务管理
*
* 业务逻辑功能举例:
* 1.注册(检查是否存在相同账户,完成注册)
* 2.登录(检查账号密码是否正确,检查账号是否可用)
* 3.转帐(检查账户余额,修改两个(多个)账户的数据)
* 4.订单创建(订单数据新增,商品库存减少,购物车清理)
* @author mrchai
* 2021/8/13 9:48
*/
public class AccountService {
/**
* 将一个账户的钱转向另一个账户
* @param from 原账户
* @param to 目标账户
* @param b 转出金额
* @return 转出是否成功
*/
public boolean transfer(Account from, Account to, BigDecimal b) throws SQLException {
//创建数据访问对象
AccountDAO dao = new AccountDAO();
//获取原账户的余额
double d1 = from.getMoney().doubleValue();
double d2 = b.doubleValue();
//判断余额是否足够
if(d1 >= d2){
//减少原账户余额
from.setMoney(from.getMoney().subtract(b));
//增加目标账户的余额
to.setMoney(to.getMoney().add(b));
//分别修改两个账户的金额
dao.update(from);
//在执行完第一次更新之后抛出异常
System.out.println(5/0);
//后续代码无法执行
dao.update(to);
System.out.println("转账成功");
}else{
System.out.println("余额不足");
}
return false;
}
}
以上的操作将会导致最终数据库中结果不再一致
1.3. 事务手动提交
如果需要保证多个更新操作位于同一个事务的前提是:这个多个更新应该使用同一个Connection
对象,以上问题经过处理之后解决方案如下:
public boolean transfer2(Account from, Account to, BigDecimal b) {
//获取原账户的余额
double d1 = from.getMoney().doubleValue();
double d2 = b.doubleValue();
//判断余额是否足够
if (d1 >= d2) {
//减少原账户余额 50000 - 5000
from.setMoney(from.getMoney().subtract(b));
//增加目标账户的余额
to.setMoney(to.getMoney().add(b));
//分别修改两个账户的金额
String sql = "update account set money=? where id=?";
Connection conn = null;
PreparedStatement ps = null;
try {
conn = DBUtils.getConn();
//关闭事务自动提交
conn.setAutoCommit(false);
//减少from账户的余额
ps = conn.prepareStatement(sql);
ps.setBigDecimal(1, from.getMoney());
ps.setLong(2, from.getId());
int i = ps.executeUpdate();
//判断第一次更新是否成功
if (i > 0) {
//增加to账户的余额
ps = conn.prepareStatement(sql);
ps.setBigDecimal(1, to.getMoney());
ps.setLong(2, to.getId());
i = ps.executeUpdate();
if (i > 0) {
//制造异常
System.out.println(5 / 0);
//提交事务
conn.commit();
System.out.println("转账成功");
return true;
} else {
//事务回滚
conn.rollback();
}
} else {
//事务回滚
conn.rollback();
}
} catch (Exception throwables) {
throwables.printStackTrace();
try {
if (conn != null) {
//事务回滚
conn.rollback();
}
} catch (SQLException e) {
e.printStackTrace();
}
} finally {
DBUtils.close(null, ps, conn);
}
} else {
System.out.println("余额不足");
}
return false;
}
1.4. 对DBUtils新增重载方法用于事务手动提交
- 新增重载方法到DBUtils
/**
* 执行通用的更新操作,外部传入连接对象(为事务手动提交提供支持)
* @param conn
* @param sql
* @param pramas
* @return
* @throws SQLException
*/
public static boolean exeUpdate(Connection conn,String sql,Object... pramas) throws SQLException {
PreparedStatement ps = null;
try {
//对sql预处理,获取执行对象
ps = conn.prepareStatement(sql);
if(Objects.nonNull(pramas)){
//获取所有的参数列表
for (int i = 0; i < pramas.length; i++) {
//为每一个?填充具体值
ps.setObject(i+1,pramas[i]);
}
}
return ps.executeUpdate() > 0;
} finally {
//只能关闭PreparedStatement,不能关闭连接
close(null,ps,null);
}
}
- DAO类中使用方式
public class AccountDAO {
private Connection conn;
public AccountDAO() {
}
public AccountDAO(Connection conn) {
this.conn = conn;
}
public boolean update(Account a) throws SQLException {
String sql = "update account set money=? where id=?";
return DBUtils.exeUpdate(conn,sql,a.getMoney(),a.getId());
}
}
- 业务逻辑层实现
/**
* 将一个账户的钱转向另一个账户
* @param from 原账户
* @param to 目标账户
* @param b 转出金额
* @return 转出是否成功
*/
public boolean transfer(Account from, Account to, BigDecimal b) {
//获取连接
Connection conn = DBUtils.getConn();
//创建数据访问对象
AccountDAO dao = new AccountDAO(conn);
//获取原账户的余额
double d1 = from.getMoney().doubleValue();
double d2 = b.doubleValue();
//判断余额是否足够
if(d1 >= d2){
//减少原账户余额
from.setMoney(from.getMoney().subtract(b));
//增加目标账户的余额
to.setMoney(to.getMoney().add(b));
try {
//设置事务手动提交
conn.setAutoCommit(false);
//分别修改两个账户的金额
dao.update(from);
System.out.println(5 / 0);
dao.update(to);
System.out.println("转账成功");
//提交事务
conn.commit();
return true;
}catch (Exception e){
e.printStackTrace();
try {
//事务回滚
conn.rollback();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}finally{
DBUtils.close(null,null,conn);
}
}else{
System.out.println("余额不足");
}
return false;
}
注意事项:
- 项目中的事务管理一般都位于业务逻辑层(service)
- 正常执行成功后需要提交事务(commit)
- 遇到任何异常都需要让事务回滚(rollback)
2. 连接池技术(重点)
2.1. 概述
在JDBC操作中任何一次的数据库访问都需要先获取一个数据连接对象Connection
,但是连接的获取是非常耗时的过程,每次的连接获取都是通过DriverManager.getConnection(url,user,password)
,因此在实际开发中如果每次访问数据库之前都需要创建连接,必然会带来较大性能损耗(时间,内存),所以在目前互联网最求高效背景下,产生了资源池技术,对应的JDBC中连接池。
连接池的原理:一般是在服务器器启动的时候首先创建连接池(集合)对象,先创建一定数目的数据库连接,存储到连接池中,并且通过一些算法进行连接的调度(取出或返还),在某个JDBC请求到达,需要连接时从连接池中获取,当使用完毕时再返还到连接池中,始终保持连接池中维护一定数量的数据库连接对象。
2.2. 连接池产品
目前在开源市场中存在很多连接池产品,其中以以下的常用连接池为代表:
dbcp
c3p0
proxool
tomcat-dbcp
druid
HikariCP
2.3. Druid连接池使用
Druid
是由阿里巴巴开源的一个为监控而生,号称世界上最快的连接池
,Druid是Java语言中最好的数据库连接池,Druid能够提供强大的监控和扩展功能。目前该项目被托管在Github
地址:https://github.com/alibaba/druid
2.3.1. 导入连接池依赖
2.3.3. 使用方式
//创建连接池对象
DruidDataSource dataSource = new DruidDataSource();
//设置url地址
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/lishipin?serverTimezone=UTC");
//设置用户名
dataSource.setUsername("root");
//设置密码
dataSource.setPassword("123456");
//设置最大活动连接数
dataSource.setMaxActive(5);
//设置最大等待连接获取时间:如果连接被耗尽了,则等待5秒,5秒之后如果还未获取则抛出异常
dataSource.setMaxWait(5000);
//初始连接数
dataSource.setInitialSize(5);
//设置最小闲置连接数
dataSource.setMinIdle(1);
Connection c1 = dataSource.getConnection();
Connection c2 = dataSource.getConnection();
Connection c3 = dataSource.getConnection();
Connection c4 = dataSource.getConnection();
Connection c5 = dataSource.getConnection();
System.out.println("第1次获取"+c1);
System.out.println("第2次获取"+c2);
System.out.println("第3次获取"+c3);
System.out.println("第4次获取"+c4);
System.out.println("第5次获取"+c5);
2.4 HikariCp连接池(Springboot默认)
在SpringBoot官网还推荐一个高效的连接池解决方案:hikariCP
-
导入相关依赖
-
编写实现代码
//创建数据源 HikariDataSource dataSource = new HikariDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/lishipin?serverTimezone=UTC"); dataSource.setUsername("root"); dataSource.setPassword("123456"); dataSource.setMaximumPoolSize(10); dataSource.setMinimumIdle(1); dataSource.setConnectionTimeout(5000); Connection connection = dataSource.getConnection(); System.out.println(connection);
3. 调用存储过程
JDBC除了支持直接发送sql语句到数据库之外,还允许向数据库中发送调用存储过程的操作:
3.1. 调用只有输入参数的存储过程
/**
* 调用只有输入参数的存储过程
*/
public static void insert(String name,String pwd,String phone) throws SQLException {
Connection conn = DBUtils.getConn();
//获取一个预处理存储过程的执行器
CallableStatement cs = conn.prepareCall("{call sp_insert_tbadmin(?,?,?)}");
cs.setString(1,name);
cs.setString(2,pwd);
cs.setString(3,phone);
//执行
boolean b = cs.execute();
System.out.println("是否存在结果集:"+b);
DBUtils.close(null,cs,conn);
}
以上操作对应的存储过程实现
create procedure sp_insert_tbadmin(name varchar(30),pwd varchar(64),phone varchar(16)) BEGIN insert into tbadmin(username,password,phone) values(name,pwd,phone); end;
3.2. 调用含输出参数的存储过程
/**
* 调用含输出参数的存储过程
* @param tname
* @param pageNum
* @param pageSize
* @throws SQLException
*/
public static void selectByPage(String tname,int pageNum,int pageSize) throws SQLException {
Connection conn = DBPoolUtils.getConn();
CallableStatement cs = conn.prepareCall("{call sp_page(?,?,?,?,?)}");
//为输入参数填充值
cs.setString(1,tname);
cs.setInt(2,pageNum);
cs.setInt(3,pageSize);
//对于存储过程的输出参数需要使用指定的数据类型注册
cs.registerOutParameter(4, Types.INTEGER);
cs.registerOutParameter(5, Types.INTEGER);
//执行存储过程
boolean b = cs.execute();
System.out.println("是否存在结果集:"+b);
//获取输出参数的值
int totalNum = cs.getInt(4);
int totalPage = cs.getInt(5);
System.out.println("总数据条数:"+totalNum);
System.out.println("总页码数:"+totalPage);
//获取结果集
ResultSet rs = cs.getResultSet();
while(rs.next()){
String username = rs.getString("username");
String password = rs.getString("password");
String phone = rs.getString("phone");
int status = rs.getInt("status");
System.out.println(username+"/"+password+"/"+phone+"/"+status);
}
DBPoolUtils.close(rs,cs,conn);
}
以上操作对应的存储过程代码
create procedure sp_page(tname varchar(64),pageNum int,pageSize int,out totalNum int,out totalPage int) BEGIN declare startNum int; -- 计算起始查询的条目位置 set startNum = (pageNum-1)*pageSize; set @vsql = concat('select count(*) into @vcount from ',tname); prepare stat from @vsql; execute stat; -- 将动态sql的执行结果赋值给输出参数 set totalNum = @vcount; DEALLOCATE prepare stat; -- 计算总页码数 set totalPage = ceil(totalNum/pageSize); set @vsql = concat('select * from ',tname,' limit ',startNum,',',pageSize); PREPARE stat from @vsql; EXECUTE stat; DEALLOCATE prepare stat; END
4. 单元测试(Junit:重要)
4.1. 概述
在传统的对Java类中的方法测试,一般是直接声明主方法,并且在主方法创建对象,然后再依次调用不同方法执行;但是以上测试过程中一旦由其中某一个方法出现异常,则JVM会终止执行,导致后续的方法没法再继续执行,所以以上测试方法在实际开发中显然不合理。因此在Java中引入单元测试(Junit)方式。
使用单元测试可以对类中的每个方法进行独立的测试,并且生成对应的测试报告;目前单元测试使用的版本主要有两个:Junit4
和Junit5
.
4.2. Idea中创建单元测试的方式
-
直接在需要创建测试的类名称上:
-
在需要创建测试的类上方按下快捷键
ctrl+shift+t
4.3. 单元测试使用
class TbadminDAOTest {
private IBaseDAO<Tbadmin> baseDAO;
/**
* 执行所有测试方法之前先执行该方法,将一些公共的操作统一编写
*/
@BeforeEach
void createAdminDao(){
baseDAO = new TbadminDAO();
}
@Test
void insert() {
assertTrue(baseDAO.insert(null));
}
@Test
void deleteById() {
assertTrue(baseDAO.deleteById(1));
}
@Test
void update() {
assertTrue(baseDAO.update(null));
}
@Test
void selectById() {
assertNotNull(baseDAO.selectById(1));
}
@Test
void selectAll() {
assertNotNull(baseDAO.selectAll());
}
@Test
void selectByPage() {
assertNotNull(baseDAO.selectByPage());
}
}