JDBC 事务_连接池

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;
}

注意事项:

  1. 项目中的事务管理一般都位于业务逻辑层(service)
  2. 正常执行成功后需要提交事务(commit)
  3. 遇到任何异常都需要让事务回滚(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

  1. 导入相关依赖

  2. 编写实现代码

    //创建数据源
    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)方式。

使用单元测试可以对类中的每个方法进行独立的测试,并且生成对应的测试报告;目前单元测试使用的版本主要有两个:Junit4Junit5.

4.2. Idea中创建单元测试的方式

  1. 直接在需要创建测试的类名称上:

  2. 在需要创建测试的类上方按下快捷键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());
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

西伯利亚大熊猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值