JDBC实战(四)修改事务隔离级别和调用存储过程

修改事务隔离级别和调用存储过程

1、事务隔离级别

1.1、回顾事务的隔离级别

对于同时运行的多个事务,当这些事务访问数据库中相同的数据时,如果没有采取必要的隔离机制,就会导致各种并发问题:

  • 脏读: 对于两个事物T1, T2, T1读取了已经被T2更新但还没有被提交的字段。之后, 若T2回滚,T1读取的内容就是临时且无效的。

  • 不可重复读: 对于两个事物 T1, T2, T1 读取了一个字段, 然后T2更新了该字段.之后, T1再次读取同一个字段,值就不同了。

  • 幻读: 对于两个事物T1, T2, T1 从一个表中读取了一个字段,然后 T2在该表中插入了一些新的行。之后, 如果T1再次读取同一个表,就会多出几行.

因此才需要数据库事务拥有隔离性: 数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响, 避免各种并发问题。
一个事务与其他事务隔离的程度称为隔离级别.数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高, 数据一致性就越好, 但并发性越弱。不同数据库支持的隔离级别不一定相同。

关于隔离级别可能导致的问题如图所示:
在这里插入图片描述

具体事务的解析:MySQL学习日记(五)MySQL事务和字符集

1.2、修改事务隔离级别测试

环境:MySQL8.0.21+Java11.1+MySQL-JDBC驱动

package IsolationAndProcess;

import driver.JDBCUtils;

import java.sql.*;

public class Isolation_test {
    
    
    public static void main(String[]args){
    
    
        read();
    }
    private static void read(){
    
    
        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;
        try{
    
    
            conn = JDBCUtils.getConnect();

            //设置隔离级别
            conn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
            //这里的事务级别类型是int类型,但并不是所有的都能调用,必须对应的数据库支持。
            st = conn.createStatement();

            System.out.println("连接上数据库!");
            String sql = "select * from bank";//查看表所有的数据
            rs = st.executeQuery(sql);
            while(rs.next()){
    
    
                int id = rs.getInt("id");
                String name = rs.getString("name");
                float money = rs.getFloat("money");
                System.out.printf("%d\t%s\t%.4f",id,name,money);
                System.out.println();
            }

        } catch (SQLException e) {
    
    
            e.printStackTrace();
        }finally {
    
    
            JDBCUtils.free(conn,st,rs);
        }
    }
}

注意:MySQL支持4种隔离级别。JDBC里的隔离级别并非所有都能调用。如:

扫描二维码关注公众号,回复: 11798425 查看本文章
conn.setTransactionIsolation(Connection.TRANSACTION_NONE);//该隔离级别MySQL不支持

在这里插入图片描述

JDBC里面的定义的隔离级别:
在这里插入图片描述

2、存储过程

2.1、存储过程

存储过程我通常理解为编程里边的方法、函数。

MySQL 5.0 版本开始支持存储过程。

存储过程(Stored Procedure)是一种在数据库中存储复杂程序,以便外部程序调用的一种数据库对象。

存储过程是为了完成特定功能的SQL语句集,经编译创建并保存在数据库中,用户可通过指定存储过程的名字并给定参数(需要时)来调用执行。

存储过程思想上很简单,就是数据库 SQL 语言层面的代码封装与重用。

优点

  • 存储过程可封装,并隐藏复杂的商业逻辑。
  • 存储过程可以回传值,并可以接受参数。
  • 存储过程无法使用 SELECT 指令来运行,因为它是子程序,与查看表,数据表或用户定义函数不同。
  • 存储过程可以用在数据检验,强制实行商业逻辑等。

缺点

  • 存储过程,往往定制化于特定的数据库上,因为支持的编程语言不同。当切换到其他厂商的数据库系统时,需要重写原有的存储过程。
  • 存储过程的性能调校与撰写,受限于各种数据库系统。

早期的业务逻辑是两层架构,数据库上面业务实现就是存储过程的实现,用于处理业务逻辑。而后来的三层架构模式,使用了java等编程语言,配合一些驱动来实现业务逻辑。进而代替了存储过程的位置。所以存储过程就比较少用了。也因为存储过程实现依赖特定的数据库,不同的数据库存储过程存在差异,导致其无法实现灵活的业务逻辑。

2.2、调用存储过程

下面我们定义一个存储过程,用于获取指定用户的银行账户财产。
创建的存储过程:

DELIMITER $$
create procedure getNameByMoney(Out Costumer_Money float,in Costumer_name varchar(20))
begin
	select money into Costumer_Money from bank where name=Costumer_name;
end;
$$
DELIMITER ;

调用存储过程查询王二小的银行账号余额:

package IsolationAndProcess;

import driver.JDBCUtils;

import java.sql.*;

public class ProcessTest {
    
    
    public static void main(String[] args) {
    
    
        getMoneyByName("王二小");
    }

    /**
     * 通过用户的名字获取其财产
     *
     * @param name
     */
    private static void getMoneyByName(String name) {
    
    
        Connection conn = null;
        CallableStatement cs = null;//继承自PreparedStatement,故用法类似于PreparedStatement
        ResultSet rs = null;
        try {
    
    
            conn = JDBCUtils.getConnect();


            System.out.println("连接上数据库!");
            String sql = "{call getNameByMoney(?,?)}";

            cs = conn.prepareCall(sql);
            //需要注册存储过程输出参数,注明其类型
            cs.registerOutParameter(1, Types.FLOAT);
            //设置占位符参数
            cs.setString(2, name);
            float money = 0f;
            cs.executeQuery();//执行存储过程
            money = cs.getFloat(1);//1表示占位符位置,必须对应参数类型
            System.out.println("名字为" + name + "的客户余额为:" + money);

        } catch (SQLException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            JDBCUtils.free(conn, cs, rs);
        }
    }
}

在这里插入图片描述

3、一些重要的API学习

3.1、获取主键,getGeneralKeys()

public ResultSet getGeneratedKeys() throws SQLException {
    
    
        try {
    
    
            if (this.wrappedStmt != null) {
    
    
                return this.wrappedStmt.getGeneratedKeys();
            } else {
    
    
                throw SQLError.createSQLException(Messages.getString("Statement.AlreadyClosed"), "S1009", this.exceptionInterceptor);
            }
        } catch (SQLException var2) {
    
    
            this.checkAndFireConnectionError(var2);
            return null;
        }
    }

有时候我们需要动态获取插入数据的主键。故测试代码如下:

package SomeAPI;

import driver.JDBCUtils;

import java.sql.*;

public class GetKey {
    
    
    public static void main(String[] args) {
    
    
        getKeys();
    }

    private static void getKeys() {
    
    
        Connection conn = null;
        PreparedStatement ps = null;//继承自PreparedStatement,故用法类似于PreparedStatement
        ResultSet rs = null;
        try {
    
    
            conn = JDBCUtils.getConnect();
            System.out.println("连接上数据库!");

            String sql = "insert into bank(name,money) values('大壮',1000)";
            ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);//规范写法,表明可以获取主键,有一些数据库你不写也能运行,有些则不行,会报错。
            //ps = conn.prepareStatement(sql);
            ps.executeUpdate();

            rs = ps.getGeneratedKeys();//因为主键不一定只有一列构成,可以为都多列,故为结果集类型。

            while (rs.next()) {
    
    
                //我们这里知道它只有一列:id
                int id = rs.getInt(1);
                System.out.println("id:" + id);
            }

        } catch (SQLException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            JDBCUtils.free(conn, ps, rs);
        }
    }
}

运行:
在这里插入图片描述

如果修改代码为:

 ps = conn.prepareStatement(sql);

运行会报错,说明Mysql8.0.21要求Statement.RETURN_GENERATED_KEYS
在这里插入图片描述

用途:
修改前面博客的DAO设计,addUser方法没有给user类的id赋值。

 public void addUser(User user) {
    
    
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try{
    
    
            conn = JDBCUtils.getConnect();
            String sql = "insert into bank(name,money) Values(?,?)";
            ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            ps.setString(1,user.getName());
            ps.setFloat(2,user.getMoney());
            ps.executeUpdate();
            //添加代码,插入一个user,就自动生成一个主键id,将其赋值给user。
            rs = ps.getGeneratedKeys();
            while(rs.next()){
    
    
                int id = rs.getInt(1);
                user.setId(id);
            }
        } catch (SQLException e) {
    
    
           throw new DaoException(e.getMessage());//千万不能随便打印堆栈跟踪或者抛出编译时异常
            //不利于将来的换其他数据库或者要修改各个接口。不知道那里出了问题。
        }finally {
    
    
            JDBCUtils.free(conn,ps,rs);
        }

    }

测试代码:

public static void main(String[]args){
    
    
        //System.out.println(DaoFactory.class.getClassLoader().getResource("/"));
        User user = new User();
        user.setName("黄天霸");
        user.setMoney(10900.0f);
        Dao daoImpl = DaoFactory.getInstance();
        daoImpl.addUser(user);
        System.out.println(user.toString());

    }

运行结果:
在这里插入图片描述

3.2、批处理命令

addBatch()和executeBatch()

package SomeAPI;

import driver.JDBCUtils1;
import java.sql.*;

/**
 * 批处理命令
 * 如一次插入40个数据
 */
public class AddBatch_Test {
    
    
    public static void main(String[] args) {
    
    
        insert_Batch();
    }
    private static void insert_Batch() {
    
    
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
    
    
            System.out.println("connect to database....");
            conn = JDBCUtils1.getInstance().getConnect();
            System.out.println("create statement...");
            String sql = "insert into test_user(name) value(?)";
            ps = conn.prepareStatement(sql);
            for (int i = 0; i < 40; i++) {
    
    
                ps.setString(1, "name" + i);
                ps.addBatch();//打包,处理
            }
            ps.executeBatch();//运行批处理命令
        } catch (SQLException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            JDBCUtils1.getInstance().free(conn, ps, rs);
        }
    }
}

在这里插入图片描述

效率上,我们不用批处理命令和用批处理命令插入相同条数的数据,比较快慢如下:

//批处理
 for (int i = 0; i < 40; i++) {
    
    
                ps.setString(1, "name_batch" + i);
                ps.addBatch();//打包,处理
            }
            ps.executeBatch();//运行批处理命令
//不用批处理
 for (int i = 0; i < 40; i++) {
    
    
                ps.setString(1, "name" + i);
                ps.executeUpdate();
            }

批处理快了35%
在这里插入图片描述

解释:Update大量的数据时, 先Prepare一个INSERT语句再多次的执行, 会导致很多次的网络连接.编译多次的SQL语句。而 要减少JDBC的调用次数改善性能, 你可以使用PreparedStatement的AddBatch()方法一次性发送多个查询给数据库. 这个Batch分批处理,超级多命令条数则分成几批。加快效率。
关于jdbc批量操作(addBatch, executeBatch)的测试

3.3、可滚动的结果集和分页技术

当我们需要对结果集中数据,进行定位查询。可以使用可滚动的结果集。
在这里插入图片描述
测试代码:

package SomeAPI;

import driver.JDBCUtils1;

import java.sql.*;

/**
 * 可滚动的结果集:
 */

public class Result_Scroll {
    
    
    public static void main(String[]args){
    
    
        result_scroll();
    }
    private static void result_scroll(){
    
    
        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;
        try {
    
    
            System.out.println("connect to database....");
            conn = JDBCUtils1.getInstance().getConnect();
            System.out.println("create statement...");
            String sql = "select * from bank where id<10";

            //设置可滚动的结果集,SCROLL,SENSITIVE表示:对数据库的数据修改敏感,可以感知。
            st = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,ResultSet.CONCUR_READ_ONLY);
            rs = st.executeQuery(sql);
            while(rs.next()){
    
    
                System.out.println("id:"+rs.getInt("id")+" name:"+rs.getString("name")
                +" money"+rs.getFloat("money"));
            }
            System.out.println("--------");
            if(rs.previous())//往前面滚动
            {
    
    
                System.out.println("id:"+rs.getInt("id")+" name:"+rs.getString("name")
                        +" money"+rs.getFloat("money"));
            }

            rs.first();//直接回到第一条数据
            System.out.println("id:"+rs.getInt("id")+" name:"+rs.getString("name")
                    +" money"+rs.getFloat("money"));
           /* rs.last();//最后一条

            rs.beforeFirst();//第一条的前面,结果集的初始态
            rs.afterLast();//最后一条数据后面
            rs.isAfterLast();//boolean,判断是否位于末尾*/
            rs.absolute(5);//直接回到第五行数据,绝对定位行号
            System.out.println("id:"+rs.getInt("id")+" name:"+rs.getString("name")
                    +" money"+rs.getFloat("money"));

        } catch (SQLException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            JDBCUtils1.getInstance().free(conn, st, rs);
        }
    }
}

运行不会报错。
但是如果修改部分代码如下:

st = conn.createStatement();//不显式说明可滚动的结果集

报错:结果集类型为只能向前读。不允许previous等操作。也就是说不同的数据库提供的驱动实现默认的结果集类型可能不同。
在这里插入图片描述

利用可滚动的结果集,我们可以做到对数据进行分页读取。但是MySQL内部支持了分页技术。而且直接使用可滚动的结果集来进行数据分页,当数据量多时,效率低下。

MySQL的分页技术
利用SQL关键字 LIMIT ,如Limit 10,5;从第五条记录开始,查询10条。
方法1: 直接使用数据库提供的SQL语句

  • 语句样式: MySQL中,可用如下方法: SELECT * FROM 表名称 LIMIT M,N。
  • 适应场景: 适用于数据量较少的情况(元组百/千级)。
    原因/缺点: 全表扫描,速度会很慢 且 有的数据库结果集返回不稳定(如某次返回1,2,3,另外的一次返回2,1,3)。Limit限制的是从结果集的M位置处取出N条输出,其余抛弃。

方法2: 建立主键或唯一索引, 利用索引(假设每页10条)

  • 语句样式: MySQL中,可用如下方法: SELECT * FROM 表名称 WHERE id_pk > (pageNum*10) LIMIT M。
  • 适应场景: 适用于数据量多的情况(元组数上万)。
    原因: 索引扫描,速度会很快。有朋友提出因为数据查询出来并不是按照pk_id排序的,所以会有漏掉数据的情况,只能方法3。

方法3: 基于索引再排序

  • 语句样式: MySQL中,可用如下方法: SELECT * FROM 表名称 WHERE id_pk > (pageNum*10) ORDER BY id_pk ASC LIMIT M。
  • 适应场景: 适用于数据量多的情况(元组数上万). 最好ORDER BY后的列对象是主键或唯一所以,使得ORDERBY操作能利用索引被消除但结果集是稳定的(稳定的含义,参见方法1)。

详情请参考博客:MySQL的分页技术总结

3.4、可更新的结果集和敏感结果集

可更新的结果集:是指我们在查询得到的数据ResultSet后,可以按照需求对里面的数据进行修改,进而修改数据库的数据。

 //设置可滚动更新的结果集
 st = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,ResultSet.CONCUR_UPDATABLE);
 rs = st.executeQuery(sql);
 while(rs.next()){
    
    
      String name = rs.getString("name");
     if("大壮".equals(name)){
    
    
          rs.updateFloat("money",400);//把大壮的钱修改为400块
      }
     rs.updateRow();//更新数据
     System.out.println("id:"+rs.getInt("id")+" name:"+name
             +" money"+rs.getFloat("money"));
 }

敏感结果集:当我们修改数据库的时候,之前获取的数据集能够感知到修改的数据,并将其读取出来。(不好。)

  • ResultSet.TYPE_SCROLL_SENSITIVE,int类型,值为1005
  • ResultSet.TYPE_SCROLL_INSENSITIVE,int类型,值为1004

测试代码:设置为数据集,然后,在数据库服务端修改id为8的数据。

 st = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,ResultSet.CONCUR_UPDATABLE);
            rs = st.executeQuery(sql);
            int i=0;
            while(rs.next()){
    
    
                Thread.sleep(10000);//睡眠10秒
                System.out.println("show:。。。。"+i);
                i++;
                String name = rs.getString("name");
                System.out.println("id:"+rs.getInt("id")+" name:"+name
                +" money"+rs.getFloat("money"));
            }
            //

如图,左边是读取的数据,当读到id=5时,如右图,将id=8的大壮的数据money改为2,但是并没有读出2,还是原来的1块钱。说明MySQL该JDBC驱动不支持敏感数据集。怕破坏业务逻辑性。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_44861675/article/details/107825968