连接池-DBUtils-事务

day04.连接池-DBUtils-事务

课前回顾:
   1.函数:
     数学函数
     流程控制函数
     日期函数
   2.JDBC:
     a.注册驱动:Class.forName("com.mysql.cj.jdbc.Driver")
     b.获取连接:DriverManager.getConnection(url,name,password)
               url:  jdbc:mysql://localhost:3306/数据库名字?时区&批量添加
               name:mysql用户名
               password:mysql密码
     c.获取执行平台:Connection接口中的方法:
                  Statement createStatement()
     d.执行sql:Statement中的方法:
                  int executeUpdate(sql)->针对于增删改操作
                  ResultSet executeQuery(sql)->针对于查询操作
     e.处理结果集:
       针对于增删改操作不用处理结果集
       针对于查询操作用处理结果集
              boolean next()-> 判断结果集中有没有下一个元素
              getxxx(列名)->获取指定列名的数据
              getxxx(第几列)->获取第几列数据
     f.关闭资源:统一用close方法
         
   3.预处理对象:PreparedStatementStatement的子接口
     a.特点:支持占位符?
     b.setxxx(第几个?,赋值)
     c.获取:Connection中的方法
           preparedStatement(sql)
     d.执行sql:
       int executeUpdate()->针对于增删改操作
       ResultSet executeQuery()->针对于查询操作
          
   
 今日重点:
  1.会使用Druid(德鲁伊)连接池,C3p0连接池
  2.会使用DBUtils工具包操作数据库
  3.会使用DBUtils中的ResultSetHandlder下面的子类处理结果集
  4.会为代码添加事务

第一章.PreparedStatement预处理对象

CREATE TABLE category(
  cid INT PRIMARY KEY AUTO_INCREMENT,
  cname VARCHAR(10)
);

1.mysql批量添加数据

properties配置文件中的url加上:rewriteBatchedStatements=true

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/220227_java4?serverTimezone=UTC&rewriteBatchedStatements=true
username=root
password=root
1.在设置完所有要添加的参数,调用PreparedStatement中的addBatch(),将SQL语句添加到PreparedStatement2.调用PreparedStatement中的executeBatch()方法批处理sql语句
/**
     * 批量添加
     */
    @Test
    public void batchAdd()throws Exception{
        //获取连接
        Connection conn = JDBCUtils.getConn();
        //准备sql
        String sql = "insert into category (cname) values (?)";
        //获取执行平台
        PreparedStatement pst = conn.prepareStatement(sql);
        for (int i = 0; i < 100; i++) {
            pst.setObject(1,"箱包"+i);
            /*
              void addBatch()
              将一组参数添加到此 PreparedStatement 对象的批处理命令中
             */
            pst.addBatch();
        }

        /*
          批量执行
          executeBatch() 将一批命令提交给数据库来执行,如果全部命令执行成功
         */
        pst.executeBatch();

        //关闭资源
        JDBCUtils.close(conn,pst,null);
    }

第二章.连接池

1.问题:
  之前的jdbc操作,每执行一个操作,就要获取一条连接对象(Connection对象),用完还要销毁,这样会耗费内存资源
2.过程:
  池子创建之后,如果来了任务,池子中有连接对象,就直接用,用完还回去
  不用频繁的去创建连接对象,销毁连接对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wSAjHcat-1649415190010)(img/image-20220408091246944.png)]

1.连接池之C3p0

1.java为连接池提供了一个标准,公共接口:javax.sql.DataSource,不同的厂商如果想要实现自己的连接池,就需要实现DataSource接口
2.常用的连接池:
  C3p0,      Druid(最常用)
1.导jar包:c3p0-0.9.1.2.jar
    
2.在resources下面创建c3p0-config.xml->名字不能错
3.在xml中配置相关信息:
<c3p0-config>
    <!-- 使用默认的配置读取连接池对象 -->
    <default-config>
        <!--  连接参数 -->
        <property name="driverClass">com.mysql.cj.jdbc.Driver</property>
        <property name="jdbcUrl">jdbc:mysql://localhost:3306/220212_java4?serverTimezone=UTC</property>
        <property name="user">root</property>
        <property name="password">root</property>

        <!-- 
          连接池参数
          初始连接数(initialPoolSize):刚创建好连接池的时候准备的连接数量
          最大连接数(maxPoolSize):连接池中最多可以放多少个连接
          最大等待时间(checkoutTimeout):连接池中没有连接时最长等待时间
          最大空闲回收时间(maxIdleTime):连接池中的空闲连接多久没有使用就会回收
         -->
        <property name="initialPoolSize">5</property>
        <property name="maxPoolSize">10</property>
        <property name="checkoutTimeout">2000</property>
        <property name="maxIdleTime">1000</property>
    </default-config>
</c3p0-config>
 
初始连接数(initialPoolSize):刚创建好连接池的时候准备的连接数量
最大连接数(maxPoolSize):连接池中最多可以放多少个连接
最大等待时间(checkoutTimeout):连接池中没有连接时最长等待时间
最大空闲回收时间(maxIdleTime):连接池中的空闲连接多久没有使用就会回收

4.编写工具类:
  接口:DataSource
  c3p0连接池实现类:ComboPooledDataSrouce
  获取连接,是在连接池中获取:ComboPooledDataSrouce.getConnection()
/*
  读取配置文件方式的工具类
 */
public class JDBCUtils_C3P0 {
    //定义一个DataSource接口
    private static DataSource dataSource;

    static {
        //创建DataSource接口的实现类,此实现类是C3P0的连接池实现类对象

        /*
          C3P0的连接池实现类对象会自动扫描resources资源包或者src下的c3p0-config.xml
          自动解析
         */
        dataSource = new ComboPooledDataSource();
    }

    /**
     * 获取连接
     */

    public static Connection getConn() {
        //从连接池中获取连接
        Connection connection = null;
        try {
            connection = dataSource.getConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return connection;
    }

    /**
     * 此时使用连接池之后close不再是关闭资源,而是将连接对象还给连接池
     */
    public static void close(Connection connection, Statement st, ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }


        if (st != null) {
            try {
                st.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }


}

@Test
    public void add()throws Exception{
        //获取连接->从连接池中获取的
        Connection conn = JDBCUtils_C3P0.getConn();
        //准备sql
        String sql = "insert into category(cname) values (?)";
        //获取预处理对象
        PreparedStatement pst = conn.prepareStatement(sql);
        //为?赋值
        pst.setObject(1,"蔬菜");
        //执行sql
        int i = pst.executeUpdate();
        //关闭资源
        JDBCUtils_C3P0.close(conn,pst,null);

    }

2.连接池之Druid(德鲁伊)

1.概述:Druid连接池是阿里巴巴开发的
2.好处:性能好,抗造
3.使用:
  a.到jar包:druid-1.1.10.jar
  b.在resources或者src下编写properties配置文件->druid.properties
    driver=com.mysql.cj.jdbc.Driver
    url=jdbc:mysql://localhost:3306/220227_java4?serverTimezone=UTC&rewriteBatchedStatements=true
    username=root
    password=root
  c.编写属于Druid的工具类
    DruidDataSourceFactory.createDataSource(Properties集合);返回的是DataSource接口的实现类

public class DruidUtils {
    //定义一个DataSource接口
    private static DataSource dataSource;

    static {

        try {
            //创建Properties集合
            Properties properties = new Properties();
            InputStream in = DruidUtils.class.getClassLoader().getResourceAsStream("druid.properties");
            properties.load(in);

            //利用DruidDataSourceFactory,根据properties集合创建属于 Druid的连接池对象
            dataSource = DruidDataSourceFactory.createDataSource(properties);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取连接
     */

    public static Connection getConn() {
        //从连接池中获取连接
        Connection connection = null;
        try {
            connection = dataSource.getConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return connection;
    }

    /**
     * 此时使用连接池之后close不再是关闭资源,而是将连接对象还给连接池
     */
    public static void close(Connection connection, Statement st, ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }


        if (st != null) {
            try {
                st.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }


}


    @Test
    public void add()throws Exception{
        //获取连接->从连接池中获取的
        Connection conn = DruidUtils.getConn();
        //准备sql
        String sql = "insert into category(cname) values (?)";
        //获取预处理对象
        PreparedStatement pst = conn.prepareStatement(sql);
        //为?赋值
        pst.setObject(1,"水果");
        //执行sql
        int i = pst.executeUpdate();
        //关闭资源
        DruidUtils.close(conn,pst,null);

    }

第三章.DBUtils工具包

1.准备工作

2.DBUtils的介绍

1.为什么要学DBUtils:使用原生的jdbc开发,代码很多,比较恶心,会影响我们的开发效率,DBUtils可以大大的简化JDBC的开发
2.什么是DBUtils:是简化jdbc开发步骤的一个开发工具包
3.使用:
  a.导jar包:commons-dbutils-1.6.jar
4.DBUtils工具包中三大核心对象:
  a.QueryRunner:定义了执行sql的方法-> 主要用的
  b.ResultSetHandlder:有很多处理结果集的对象 -> 主要用的
  c.Dbutils:DBUtils工具包中的一个类,里面定义了很多关闭资源以及操作事务等方法

3.QueryRunner

3.1.空参的QueryRunner的介绍以及使用

1.QueryRunner()
  a.特点:需要我们自己维护连接对象(需要自己获取连接对象,释放连接对象),支持sql中使用占位符?
2.方法:
  a.int update(Connection conn, String sql, Object... params)->执行sql,针对于增删改操作
               conn:连接对象
               sql:sql语句
               params:可变参数,给sql中的?赋值
                      第一个值,就是给第一个?赋值,自动匹配
                      第二个值,就是给第二个?赋值,自动匹配
    比如:update(conn,"insert into category(cid,cname) values (?,?)",1,"蔬菜")
        
        Object[] obj = {1,"蔬菜"}
        update(conn,"insert into category(cid,cname) values (?,?)",obj)
            
  b. T query(Connection conn, String sql, ResultSetHandler<T> rsh, Object... params)->针对于查询语句执行
               conn:连接对象
               sql:sql语句
               rsh:处理结果集的方式
               params:可变参数,给sql中的?赋值
                      第一个值,就是给第一个?赋值,自动匹配
                      第二个值,就是给第二个?赋值,自动匹配    

public class Test01 {
    @Test
    public void insert()throws Exception{
        //创建空参的QueryRunner对象
        QueryRunner qr = new QueryRunner();
        //获取连接
        Connection conn = DruidUtils.getConn();
        //准备sql
        String sql = "insert into category(cname) values (?)";
        //执行sql
        //qr.update(conn,sql,"箱包");


        /*
         由于update方法中的第三个参数是可变参数
         所以我们可以在外面定义一个Object数组
         将要给?赋的值放到数组中,然后再将数组
         放到update方法中
         */
        Object[] obj = {"手机"};
        qr.update(conn,sql,obj);

        //关闭资源
        DruidUtils.close(conn,null,null);
    }

    @Test
    public void add()throws Exception{
        //创建空参的QueryRunner对象
        QueryRunner qr = new QueryRunner();
        //获取连接
        Connection conn = DruidUtils.getConn();
        //执行sql
        qr.update(conn,"insert into category(cname) values (?)","电脑");

        //关闭资源
        DruidUtils.close(conn,null,null);
    }
}

3.2.有参QueryRunner的介绍以及使用

1.使用有参QueryRunner特点:
  不需要我们自己维护连接,QueryRunner会自动维护连接对象
  说白了就是不需要自己从连接池中获取连接,也不需要我们自己关闭或者归还连接池
2.构造:
  QueryRunner(DataSource ds)
              DataSource:传递连接池对象
3.方法:
  int update(String sql, Object... params)->针对于增删改操作
             sql:sql语句
             params:可变参数,给sql中的?赋值
                    第一个值,就是给第一个?赋值,自动匹配
                    第二个值,就是给第二个?赋值,自动匹配  
  T query(String sql, ResultSetHandler<T> rsh, Object... params)
             sql:sql语句
             rsh:处理结果集的方式
             params:可变参数,给sql中的?赋值
                    第一个值,就是给第一个?赋值,自动匹配
                    第二个值,就是给第二个?赋值,自动匹配        

//改造工具类

public class DruidUtils {
    //定义一个DataSource接口
    private static DataSource dataSource;

    static {

        try {
            //创建Properties集合
            Properties properties = new Properties();
            InputStream in = DruidUtils.class.getClassLoader().getResourceAsStream("druid.properties");
            properties.load(in);

            //利用DruidDataSourceFactory,根据properties集合创建属于 Druid的连接池对象
            dataSource = DruidDataSourceFactory.createDataSource(properties);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取连接
     */

    public static Connection getConn() {
        //从连接池中获取连接
        Connection connection = null;
        try {
            connection = dataSource.getConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return connection;
    }


    /**
     * 新加的方法
     * 获取连接池对象
     */
    public static DataSource getDataSource(){
        return dataSource;
    }



    /**
     * 此时使用连接池之后close不再是关闭资源,而是将连接对象还给连接池
     */
    public static void close(Connection connection, Statement st, ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }


        if (st != null) {
            try {
                st.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }


}


public class Test02 {
    @Test
    public void insert()throws Exception{
        //创建QueryRunner对象
        QueryRunner qr = new QueryRunner(DruidUtils.getDataSource());
        //准备sql
        String sql = "insert into category (cname) values (?)";
        //执行sql
        qr.update(sql,"鼠标");

    }
    @Test
    public void delete()throws Exception{
        //创建QueryRunner对象
        QueryRunner qr = new QueryRunner(DruidUtils.getDataSource());
        //执行sql
        qr.update("delete from category where cid = ?",1);
    }
}


4.ResultSetHandler结果集

4.1.标准的JavaBean

什么是JavaBean:实体类
开发的过程中:JavaBean中的成员变量要和具体表中的字段要对应
一个标准的JavaBean如何编写:成员变量   构造    get/set 方法    hashcode和equals方法  toString
                        此类还需要实现Serializable

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r1CN9xTa-1649415190012)(img/1599356773135.png)]

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Category {
    private Integer cid;
    private String cname;
}


1.要求:定义javabean的时候里面的字段类型都要是引用数据类型,基本类型的字段也要写成包装类
2.问题:为什么定义javabean的时候,要使用包装类呢?
  a.因为包装类的默认值为NULL,如果我们的主键是自增,sql语句可以写成:
    insert into category(cid,cname) values (NULL,'蔬菜');
  b.使用包装类,如果有需要的话,可以直接调用包装类中的方法操作字段数据

4.2.BeanHandler

1.作用:将查询出来的结果集中的第一行数据封装成javabean对象
2.方法:
  query(String sql, ResultSetHandler<T> rsh, Object... params)>有参QueryRunner时使用
  query(Connection conn, String sql, ResultSetHandler<T> rsh, Object... params)->空参QueryRunner时使用
3.构造:
  BeanHandler(Class type)
  传递的class对象其实就是我们想要封装的javabbean类的class对象
  想将查询出来的数据封装成哪个javabean对象,就写哪个javabean的class对象
4.怎么理解:
  将查询出来的数据为javabean中的成员变量赋值

    @Test
    public void beanHandler()throws Exception{
        QueryRunner qr = new QueryRunner(DruidUtils.getDataSource());
        Category category = qr.query("select * from category", new BeanHandler<>(Category.class));
        System.out.println(category);
    }

4.3.BeanListHandler

1.作用:将查询出来的结果每一条数据都封装成一个一个的javabean对象,将这些javabean对象放到List集合中
2.构造:
  BeanListHandler(Class type)
  传递的class对象其实就是我们想要封装的javabbean类的class对象
  想将查询出来的数据封装成哪个javabean对象,就写哪个javabean的class对象
      
3.怎么理解:
  将查询出来的多条数据封装成多个javabean对象,将多个javabean对象放到List集合中

    @Test
    public void beanListHandler()throws Exception{
        QueryRunner qr = new QueryRunner(DruidUtils.getDataSource());
        List<Category> list = qr.query("select * from category", new BeanListHandler<>(Category.class));
        for (Category category : list) {
            System.out.println(category);
        }
    }

4.4.ScalarHandler

1.作用:主要是处理单值的查询结果的,执行的select语句,结果集只有12.构造:
  ScalarHandler(int columnIndex)->不常用->指定第几列
  ScalarHandler(String columnName)->不常用->指定列名
  ScalarHandler()->常用
3.注意:
  ScalarHandler和聚合函数使用更有意义

    @Test
    public void scalarHandler()throws Exception{
        QueryRunner qr = new QueryRunner(DruidUtils.getDataSource());
        //Object o1 = qr.query("select * from category", new ScalarHandler<>(2));
        //Object o2 = qr.query("select * from category", new ScalarHandler<>("cname"));
        //System.out.println(o2);

        Object o3 = qr.query("select count(*) from category", new ScalarHandler<>());
        System.out.println(o3);
    }

4.5.ColumnListHandler

1.作用:将查询出来的结果中的某一列数据,存储到List集合中
2.构造:
  ColumnListHandler(int columnIndex)->指定第几列
  ColumnListHandler(String columnName)->指定列名
  ColumnListHandler()-> 默认显示查询结果中的第一列数据
3.注意:
 ColumnListHandler可以指定泛型类型,如果不指定,返回的List泛型就是Object类型

    @Test
    public void ColumnListHandler()throws Exception{
        QueryRunner qr = new QueryRunner(DruidUtils.getDataSource());
        //List<Object> list = qr.query("select * from category", new ColumnListHandler<>());
        //List<Object> list = qr.query("select * from category", new ColumnListHandler<>(2));
        List<Object> list = qr.query("select * from category", new ColumnListHandler<>("cname"));
        for (Object o : list) {
            System.out.println(o);
        }
    }

第四章.事务

1.事务

1.1.事务_转账分析图

CREATE TABLE account(
  `name` VARCHAR(10),
  money INT
);

1.2.实现转账(不加事务)

public class Test01Transfer {
    @Test
    public void transfer()throws Exception{
        //创建QueryRunner对象
        QueryRunner qr = new QueryRunner();
        //获取连接
        Connection conn = DruidUtils.getConn();
        //减钱的sql
        String sqlOut = "update account set money= money-1000 where name = ?";

        //加钱的sql
        String sqlIn = "update account set money= money+1000 where name = ?";

        //执行sql
        qr.update(conn,sqlOut,"涛哥");

        //制造错误,模仿断网
        //System.out.println(1/0);

        qr.update(conn,sqlIn,"茜茜");

        //关闭资源
        DruidUtils.close(conn,null,null);
    }
}

1.3.事务的介绍

1.作用:主要是用于管理一组操作的,让这组操作(一组操作可能包含了多条sql),要么全部成功,要么全部失败
2.注意:
  mysql自带事务,但是这个事务一次只能管理一条sql,如果想要事务管理多条sql,需要将mysql自带事务关闭,开启手动事务
3.如何手动管理事务? Connection对象中的方法
  void setAutoCommit(false)->关闭mysql自带事务,开启手动事务
  void commit() -> 提交事务->当事务一旦提交,管理的sql数据将永久保存,回不去了
  void rollback() -> 回滚事务->如果事务不提交(commit)的话,调用此方法,可以将数据还原回去
4.注意:
  以上三个方法,必须是同一条连接对象调用,不然无法完成手动事务管理

总结:

1.事务可以管理多条sql.使其要么全成功,要么全失败

2.mysql自带事务管理,但是一个事务只管理一条sql

3.所以我们如果想让事务管理一组操作,需要关闭mysql自带事务,开启手动事务

4.开启事务,提交事务,回滚事务的方法都需要同一个Connection对象调用

1.4.DBUtils实现转账(添加事务)

public class Test01Transfer {
    @Test
    public void transfer() throws SQLException {
        //创建QueryRunner对象
        QueryRunner qr = new QueryRunner();
        //获取连接
        Connection conn = DruidUtils.getConn();

        try{
            //开启手动事务
            conn.setAutoCommit(false);

            //减钱的sql
            String sqlOut = "update account set money= money-1000 where name = ?";

            //加钱的sql
            String sqlIn = "update account set money= money+1000 where name = ?";

            //执行sql
            qr.update(conn,sqlOut,"涛哥");

            //制造错误,模仿断网
            System.out.println(1/0);

            qr.update(conn,sqlIn,"茜茜");

            //提交事务
            conn.commit();
            System.out.println("转账成功");
        }catch (Exception e){
            //回滚事务
            conn.rollback();
            System.out.println("转账失败");
            e.printStackTrace();
        }finally {
            //关闭资源
            DruidUtils.close(conn,null,null);
        }

    }
}

1.5.mysql中操作事务_了解

1.开启事务:begin
2.提交事务:commit
3.回滚事务:rollback

-- 开启事务
BEGIN;

UPDATE account SET money = money-1000 WHERE `name` = '涛哥';

UPDATE account SET money = money+1000 WHERE `name` = '茜茜';

-- 提交事务
COMMIT;

-- 回滚事务
ROLLBACK;

1.6.分层思想介绍以及架构搭建

三层架构思想: 表现层(web)   业务层(service)   持久层(dao)

创建package:包名都是小写单词;公司域名倒着写

com.atguigu.web-> 表现层,和页面打交道,这里面的类用于接收请求和回响应
com.atguigu.service->业务层,写业务
com.atguigu.dao->持久层,和数据库打交道
com.atguigu.pojo->javabean实体类
com.atguigu.utils-> 工具类

1.6.1.转账_web层实现
public class AccountController {
    public static void main(String[] args) {
        //创建Scanner
        Scanner sc = new Scanner(System.in);
        System.out.println("请您输入出钱的人:");
        String outName = sc.next();
        System.out.println("请您输入收钱的人:");
        String inName = sc.next();
        System.out.println("请您输入转账金额:");
        int money = sc.nextInt();
        
        
        //将outName,inName,money传递到service层
        AccountService accountService = new AccountService();
        accountService.transfer(outName,inName,money);
    }
}

温馨小提示:

开发中对一个方法的注释:一般用文档注释

1.6.2.转账_service层实现
public class AccountService {
    /**
     * @param outName  出钱人姓名
     * @param inName   收钱人姓名
     * @param money    转账金额
     */

    public void transfer(String outName, String inName, int money) {
        //直接将数据传递给dao
        AccountDao accountDao = new AccountDao();

        try{
            //减钱
            accountDao.outMoney(outName,money);

            //System.out.println(1/0);

            //加钱
            accountDao.inMoney(inName,money);

            System.out.println("转账成功");
        }catch (Exception e){
            System.out.println("转账失败");
            e.printStackTrace();
        }


    }
}


1.6.3.转账_dao层实现
public class AccountDao {
    /**
     * 转钱方法
     * @param outName  出钱人姓名
     * @param money    金额
     */
    public void outMoney(String outName, int money) throws Exception {
        //创建QueryRunner对象
        QueryRunner qr = new QueryRunner();
        //获取连接
        Connection conn = DruidUtils.getConn();
        //准备sql
        String outSql = "update account set money = money-? where name = ?";
        //执行sql
        qr.update(conn,outSql,money,outName);
        //关闭资源
        DruidUtils.close(conn,null,null);
    }

    /**
     * 加钱方法
     * @param inName  收钱人姓名
     * @param money   金额
     */
    public void inMoney(String inName, int money)throws Exception {
        //创建QueryRunner对象
        QueryRunner qr = new QueryRunner();
        //获取连接
        Connection conn = DruidUtils.getConn();
        //准备sql
        String inSql = "update account set money = money+? where name = ?";
        //执行sql
        qr.update(conn,inSql,money,inName);
        //关闭资源
        DruidUtils.close(conn,null,null);
    }
}


1.6.4.在service层添加事务(传递连接)
public class AccountService {
    /**
     * @param outName  出钱人姓名
     * @param inName   收钱人姓名
     * @param money    转账金额
     */

    public void transfer(String outName, String inName, int money) {
        //直接将数据传递给dao
        AccountDao accountDao = new AccountDao();

        //获取连接
        Connection conn = DruidUtils.getConn();

        try{
            //开启手动事务
            conn.setAutoCommit(false);

            //减钱
            accountDao.outMoney(conn,outName,money);

            System.out.println(1/0);

            //加钱
            accountDao.inMoney(conn,inName,money);

            //提交事务
            conn.commit();
            System.out.println("转账成功");
        }catch (Exception e){
            //回滚事务
            try {
                conn.rollback();
                System.out.println("转账失败");
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
            e.printStackTrace();
        }finally {
            DruidUtils.close(conn,null,null);
        }


    }
}


1.6.5.修改dao层代码
public class AccountDao {
    /**
     * 转钱方法
     * @param outName  出钱人姓名
     * @param money    金额
     */
    public void outMoney(Connection conn,String outName, int money) throws Exception {
        //创建QueryRunner对象
        QueryRunner qr = new QueryRunner();

        //准备sql
        String outSql = "update account set money = money-? where name = ?";
        //执行sql
        qr.update(conn,outSql,money,outName);

    }

    /**
     * 加钱方法
     * @param inName  收钱人姓名
     * @param money   金额
     */
    public void inMoney(Connection conn,String inName, int money)throws Exception {
        //创建QueryRunner对象
        QueryRunner qr = new QueryRunner();
        //准备sql
        String inSql = "update account set money = money+? where name = ?";
        //执行sql
        qr.update(conn,inSql,money,inName);
    }
}


1.问题描述:service层只是处理业务的,而获取连接对象这种事儿是service应该干的吗?不是
          既然获取连接对象这种不在service层干,那么dao层的连接对象怎么办?

2.解决问题:给service和dao层找一个"小秘书",让这个"小秘书"去获取连接对象,然后将获取的连接对象分别传给service层和dao层
          还要保证service层和dao层的连接对象是同一个,不然事务不能生效

3.问题描述:
  如果在多线程的情况下,一个用户就相当于一个线程,如何保证多个用户(多个线程)不会使用同一条连接对象?

2.ThreadLocal

2.1.ThreadLocal基本使用和原理

1.概述:可以看做是一个容器,专门放变量或者对象的
2.方法:
  void set(T value)->ThreadLocal中存数据
  T get()->ThreadLocal中获取数据
3.特点:
  a.ThreadLocal中获取的元素都是最后一次存进去的,一次只能存一个数据
  b.一个线程往TheadLocal中存的数据,其他线程获取不到
     在一条线程中使用的ThreadLocal中存储的对象,是同一个对象
4.作用:
  保证一条线程上使用的资源是同一个
      
5.ThreadLocal底层实质上是一个Map集合
  key:当前正在执行的线程对象->Thread.currentThread()
  value:随意->ThreadLocal中存的数据
  
  这样,存数据的时候ThreadLocal底层直接将当前线程存的值和当前线程直接绑死,所以别的线程拿不到值

public class Test01 {
    public static void main(String[] args) {
        ThreadLocal<String> tl = new ThreadLocal<>();
        tl.set("涛哥");
        tl.set("三上");
        tl.set("松下");
        System.out.println(tl.get());

        //使用了另外一个线程
        new Thread(()-> System.out.println(tl.get())).start();//null
    }
}

2.2.连接对象管理类(小秘书)

/*
  使用ThreadLocal类,将连接对象存进去
  保证一个用户使用一个连接对象
 */
public class ConnectionManager {
    //定义一个ThreadLocal对象
    private static ThreadLocal<Connection> tl = new ThreadLocal<>();

    /**
     * 定义方法,从连接池中获取连接对象,存到ThreadLocal中
     * 然后将ThreadLocal中的连接对象返回
     */
    public static Connection getConnection()throws Exception{
        //从ThreadLocal中获取连接对象,看看里面有没有Connection对象
        Connection conn = tl.get();
        //判断,如果获取出来的连接对象为null,证明之前没有在ThreadLocal中存
        if (conn==null){
            //从连接池中获取一条连接对象,存入ThreadLocal中
            conn = DruidUtils.getDataSource().getConnection();
            tl.set(conn);
        }
        return conn;
    }
}

public class AccountService {
    /**
     * @param outName  出钱人姓名
     * @param inName   收钱人姓名
     * @param money    转账金额
     */

    public void transfer(String outName, String inName, int money) {
        //直接将数据传递给dao
        AccountDao accountDao = new AccountDao();
        Connection conn = null;

        try{
            conn = ConnectionManager.getConnection();
            //开启手动事务
            conn.setAutoCommit(false);
            //减钱
            accountDao.outMoney(outName,money);

            System.out.println(1/0);

            //加钱
            accountDao.inMoney(inName,money);

            //提交事务
            conn.commit();
            System.out.println("转账成功");
        }catch (Exception e){
            //回滚事务
            try {
                conn.rollback();
                System.out.println("转账失败");
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
            e.printStackTrace();
        }finally {
            DruidUtils.close(conn,null,null);
        }


    }
}


public class AccountDao {
    /**
     * 转钱方法
     * @param outName  出钱人姓名
     * @param money    金额
     */
    public void outMoney(String outName, int money) throws Exception {
        //创建QueryRunner对象
        QueryRunner qr = new QueryRunner();

        //利用连接管理类获取连接对象
        Connection conn = ConnectionManager.getConnection();

        //准备sql
        String outSql = "update account set money = money-? where name = ?";
        //执行sql
        qr.update(conn,outSql,money,outName);

    }

    /**
     * 加钱方法
     * @param inName  收钱人姓名
     * @param money   金额
     */
    public void inMoney(String inName, int money)throws Exception {
        //创建QueryRunner对象
        QueryRunner qr = new QueryRunner();

        //利用连接管理类获取连接对象
        Connection conn = ConnectionManager.getConnection();

        //准备sql
        String inSql = "update account set money = money+? where name = ?";
        //执行sql
        qr.update(conn,inSql,money,inName);

    }
}


3.事务的特性以及隔离级别

3.1.事务特性:ACID

  • 原子性(Atomicity)原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
  • 一致性(Consistency)事务前后数据的完整性必须保持一致。
  • 隔离性(Isolation)事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离,正常情况下数据库是做不到这一点的,可以设置隔离级别,但是效率会非常低。
  • 持久性(Durability)持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

3.2 并发访问问题

如果不考虑隔离性,事务存在3中并发访问问题。

  1. 脏读:一个事务读到了另一个事务未提交的数据.
  2. 不可重复读:一个事务读到了另一个事务已经提交(update)的数据。引发另一个事务,在事务中的多次查询结果不一致。
  3. 虚读 /幻读:一个事务读到了另一个事务已经提交(insert)的数据。导致另一个事务,在事务中多次查询的结果不一致。

3.3 隔离级别:解决问题

  • 数据库规范规定了4种隔离级别,分别用于描述两个事务并发的所有情况。
  1. read uncommitted 读未提交,一个事务读到另一个事务没有提交的数据。

    a)存在:3个问题(脏读、不可重复读、虚读)。

    b)解决:0个问题

  2. read committed 读已提交,一个事务读到另一个事务已经提交的数据。

    a)存在:2个问题(不可重复读、虚读)。

    b)解决:1个问题(脏读)

  3. repeatable read:可重复读,在一个事务中读到的数据始终保持一致,无论另一个事务是否提交。

    a)存在:1个问题(虚读)。

    b)解决:2个问题(脏读、不可重复读)

    4.serializable 串行化,同时只能执行一个事务,相当于事务中的单线程。

a)存在:0个问题。

b)解决:3个问题(脏读、不可重复读、虚读)

  • 安全和性能对比
    • 安全性:serializable > repeatable read > read committed > read uncommitted
    • 性能 : serializable < repeatable read < read committed < read uncommitted
  • 常见数据库的默认隔离级别:
    • MySql:repeatable read
    • Oracle:read committed

3.4 演示

  • 隔离级别演示参考:资料/隔离级别操作过程.doc【增强内容,了解】
  • 查询数据库的隔离级别
show variables like '%isolation%';
或
select @@tx_isolation;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IrYs258q-1649415190013)(img/h.png)]

  • 设置数据库的隔离级别
    • set session transactionisolation level 级别字符串
    • 级别字符串:readuncommittedread committedrepeatable readserializable
    • 例如:set session transaction isolation level read uncommitted;
  • 读未提交:readuncommitted
    • A窗口设置隔离级别
      • AB同时开始事务
      • A 查询
      • B 更新,但不提交
      • A 再查询?-- 查询到了未提交的数据
      • B 回滚
      • A 再查询?-- 查询到事务开始前数据
  • 读已提交:read committed
    • A窗口设置隔离级别
      • AB同时开启事务
      • A查询
      • B更新、但不提交
      • A再查询?–数据不变,解决问题【脏读】
      • B提交
      • A再查询?–数据改变,存在问题【不可重复读】
  • 可重复读:repeatable read
    • A窗口设置隔离级别
      • AB 同时开启事务
      • A查询
      • B更新, 但不提交
      • A再查询?–数据不变,解决问题【脏读】
      • B提交
      • A再查询?–数据不变,解决问题【不可重复读】
      • A提交或回滚
      • A再查询?–数据改变,另一个事务
  • 串行化:serializable
    • A窗口设置隔离级别
    • AB同时开启事务
    • A查询
      • B更新?–等待(如果A没有进一步操作,B将等待超时)
      • A回滚
      • B 窗口?–等待结束,可以进行操作
- 原子性(Atomicity)原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。 
- 一致性(Consistency)事务前后数据的完整性必须保持一致。
- 隔离性(Isolation)事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离,正常情况下数据库是做不到这一点的,可以设置隔离级别,但是隔离级别越高,效率会非常低。
- 持久性(Durability)持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

如果不考虑隔离性,事务存在3中并发访问问题。(如果隔离级别低,事务跟事务之间有可能互相影响)

1. 脏读:一个事务读到了另一个事务未提交的数据.
2. 不可重复读:一个事务读到了另一个事务已经提交(update)的数据。引发另一个事务,在事务中的多次查询结果不一致。
3. 虚读 /幻读:一个事务读到了另一个事务已经提交(insert)的数据。导致另一个事务,在事务中多次查询的结果不一致。

总结:
  我们最理想的状态是:一个事务和其他事务互不影响
  
  但是如果不考虑隔离级别的话,就会出现多个事务之间互相影响
  
  而事务互相影响的表现方式为:
    脏读
    不可重复读
    虚读/幻读

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值