浅析JDBC使用步骤及主要对象

浅析JDBC使用步骤及主要对象

一、JDBC是什么

JDBC(Java Database Connectivity,简称JDBC)即Java数据库连接(JDBC)API,是Java编程语言与各种数据库、SQL数据库和其他表格数据源(如电子表格或平面文件)之间独立于数据库的连接的行业标准。JDBC API为基于SQL的数据库访问提供了一个调用级API。

二、为什么使用JDBC

JDBC技术允许您使用Java编程语言为需要访问企业数据的应用程序开发“一次编写,随时随地运行”的功能。使用支持JDBC技术的驱动程序,您甚至可以在异构环境中连接所有公司数据。

JDBC是Java语言中用来规范客户端程序如何来访问(关系型)数据库的应用程序接口,也就是说JDBC只是提供了标准规范,并不提供具体的实现,具体的实现是由各个数据库厂商完成的,即各个数据库厂商按照JDBC的规范来实现相应的数据库驱动程序(就是实现了java.sql.Driver接口的类),用户在使用不同的数据库时只需要使用不同厂商提供的驱动类就可以了,并不需要更改业务代码。

三、JDBC的使用

3.1、JDBC使用完整实例:

package JDBC;

import java.sql.*;

public class MainTest {
    public static final String Driver="com.mysql.jdbc.Driver";
    public static final String URL = "jdbc:mysql://localhost:3306/mybatis-study?useSSL=false";
    public static final String USER = "root";
    public static final String PASSWORD = "root";

    public static void main(String[] args) throws Exception {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        try {
            //1、加载驱动程序
            Class.forName("com.mysql.jdbc.Driver");
            //2、获取数据库连接
            conn = DriverManager.getConnection(URL, USER, PASSWORD);
            conn.setAutoCommit(false); //JDBC中默认是true,自动提交事务
            //3、获取可以执行SQL的对象(Statement\PreparedStatement对象:)
            stmt = conn.createStatement();
            //4、执行SQL并获取返回结果
            rs = stmt.executeQuery("SELECT user_name, age FROM tb_user");
            conn.commit();
            while (rs.next()) {
                System.out.println(rs.getString("user_name") + " 年龄:" + rs.getInt("age"));
            }
        } catch (Exception e) {
            e.printStackTrace();
            conn.rollback();
        } finally {
            //5、释放资源并关闭连接
            if (rs != null) {
                try {
                    rs.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            if (stmt != null) {
                try {
                    stmt.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            if (conn != null) {
                try {
                    conn.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

3.2、JDBC的使用步骤

1、加载驱动程序
  • 驱动加载的三种方式
    • 第一种方式:Class.forName(“com.mysql.jdbc.Driver”),通过JVM动态加载驱动类,常见,推荐使用
      1. 我们知道Class.forName()方法的目的就是为了能够动态的加载类。通过此种方式来加载驱动类的时候只是通过Class.forName()方法让JVM查找并加载了驱动类而并未进行其他操作,但是JDBC规范又规定了任何一个Driver类都必须向DriverManger类去注册自己,但是这里并没有注册操作,为什么就可以直接使用了呢?

        其实在JVM加载指定类的过程中,如果类中有静态代码块的话,JVM就会执行该部分代码,静态代码是和类绑定的,类装载成功就表示已经执行了静态代码块,而且以后不会再走这段静态代码了。也就是说在驱动类(此例为com.mysql.jdbc.Driver)中一定是有一段类似的静态代码块来完成了注册操作,查看源码后果然发现了以下代码块:

        	static {
        		try {
        			java.sql.DriverManager.registerDriver(new Driver());
        		} catch (SQLException E) {
        			throw new RuntimeException("Can't register driver!");
        		}
        	}
        

        在JDBC 4.0之后实际上我们不需要再调用Class.forName来加载驱动程序了,我们只需要把驱动的jar包放到工程的类加载路径里,那么驱动就会被自动加载。
        这个自动加载采用的技术叫做SPI,数据库驱动厂商也都做了更新。可以看一下jar包里面的META-INF/services目录,里面有一个java.sql.Driver的文件,文件里面包含了驱动的全路径名。比如mysql-connector里面的内容:

        com.mysql.jdbc.Driver
        com.mysql.fabric.jdbc.FabricMySQLDriver
        
        
      2. 那么SPI技术是如何加载数据库驱动,又是在什么阶段加载的呢?

        static {
            loadInitialDrivers();
            println("JDBC DriverManager initialized");
        }
        
        ......
        
        private static void loadInitialDrivers() {
            String drivers;
            try {
                drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                    public String run() {
                        return System.getProperty("jdbc.drivers");
                    }
                });
            } catch (Exception ex) {
                drivers = null;
            }
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
        
                    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                    Iterator<Driver> driversIterator = loadedDrivers.iterator();
                    try{
                        while(driversIterator.hasNext()) {
                            driversIterator.next();
                        }
                    } catch(Throwable t) {
                    // Do nothing
                    }
                    return null;
                }
            });
        
            println("DriverManager.initialize: jdbc.drivers = " + drivers);
        
            if (drivers == null || drivers.equals("")) {
                return;
            }
            String[] driversList = drivers.split(":");
            println("number of Drivers:" + driversList.length);
            for (String aDriver : driversList) {
                try {
                    println("DriverManager.Initialize: loading " + aDriver);
                    Class.forName(aDriver, true,
                            ClassLoader.getSystemClassLoader());
                } catch (Exception ex) {
                    println("DriverManager.Initialize: load failed: " + ex);
                }
            }
        }
        

        查看DriverManager类源码可以看出,在静态代码块中调用loadInitialDrivers方法,而在loadInitialDrivers方法里又调用了ServiceLoader.load(Driver.class)方法,此方法就会加载所有在META-INF/services/java.sql.Driver文件里边的类到JVM内存,完成驱动的自动加载。
        这就是SPI的优势所在,能够自动的加载类到JVM内存。这个技术在阿里的dubbo框架里面也占到了很大的分量,有兴趣的朋友可以看一下dubbo的代码,或者百度一下dubbo的扩展机制,详细的SPI机制就不深入了。

    • 第二种方式:DriverManager.register(new com.mysql.jdbc.Driver()),使用DriverManager注册驱动,在编译时需要导入对应的lib,不推荐
      • 需要注意的是,在分析第一种方式的时候我们看到Driver类中是有静态代码块的,在静态代码块中,会向DriverManager注册一次驱动,那么也就是说此种方式在new Driver的时候就会注册一次,然后外层又会注册一次,所以注册了两次驱动,虽不影响使用,但是没必要。那既然new对象的时候会执行静态代码块,那也就表示直接new com.mysql.jdbc.Driver()也能达到注册驱动的效果。
    • 第三种方式:System.setProperty(“jdbc.driver”,“com.mysql.jdbc.Driver”),通过系统的属性设置,可加载多个驱动
      • 虽然也可以脱离JDBC驱动进行编译,但是方法参数设置相对来说较为复杂
        此种方式的特点是它可以设置多个驱动,在连接时JDBC会按顺序搜索,直到找到第一个能成功连接指定URL的驱动程序。
        多个驱动使用冒号分隔开,比如
          System.setProperty("jdbc.drivers","XXXDriver:XXXDriver:XXXDriver");
      
2、获取数据库连接
  • 从前面的JDBC使用实例中可以看出,获取数据库连接的核心代码是DriverManager.getConnection()方法,那么如果工程中有多个驱动的时候,JDBC是如何区分多个驱动的?
    • 这就需要DriverManager.getConnection()的源码了,源码如下:
      @CallerSensitive
      public static Connection getConnection(String url,
          String user, String password) throws SQLException {
          java.util.Properties info = new java.util.Properties();
      
          if (user != null) {
              info.put("user", user);
          }
          if (password != null) {
              info.put("password", password);
          }
      
          return (getConnection(url, info, Reflection.getCallerClass()));
      }
      
      ......
      
      private static Connection getConnection(
          String url, java.util.Properties info, Class<?> caller) throws SQLException {
          ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
          synchronized(DriverManager.class) {
              if (callerCL == null) {
                  callerCL = Thread.currentThread().getContextClassLoader();
              }
          }
          if(url == null) {
              throw new SQLException("The url cannot be null", "08001");
          }
          println("DriverManager.getConnection(\"" + url + "\")");
          SQLException reason = null;
          for(DriverInfo aDriver : registeredDrivers) {
              if(isDriverAllowed(aDriver.driver, callerCL)) {
                  try {
                      println("    trying " + aDriver.driver.getClass().getName());
                      Connection con = aDriver.driver.connect(url, info);
                      if (con != null) {
                          println("getConnection returning " + aDriver.driver.getClass().getName());
                          return (con);
                      }
                  } catch (SQLException ex) {
                      if (reason == null) {
                          reason = ex;
                      }
                  }
              } else {
                  println("    skipping: " + aDriver.getClass().getName());
              }
          }
          if (reason != null)    {
              println("getConnection failed: " + reason);
              throw reason;
          }
          println("getConnection: no suitable driver found for "+ url);
          throw new SQLException("No suitable driver found for "+ url, "08001");
      }
      
    • 从源码中我们可以看出DriverManager.getConnection方法会遍历所有已经加载的驱动实例(registeredDrivers)去创建连接(调用Driver.connect()),当一个驱动创建连接成功时就会返回这个连接(return (con)),同时不再调用其他的驱动实例。
    • 那么JDBC又是如何判断驱动是否建立连接成功的呢,其核心在于各个驱动实现类中的connect()方法,以com.mysql.jdbc.Driver为例,其核心源码位于com.mysql.jdbc.NonRegisteringDriver如下:
          public Connection connect(String url, Properties info) throws SQLException {
              if (url == null) {
                  throw SQLError.createSQLException(Messages.getString("NonRegisteringDriver.1"), "08001", (ExceptionInterceptor)null);
              } else if (StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:loadbalance://")) {
                  return this.connectLoadBalanced(url, info);
              } else if (StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:replication://")) {
                  return this.connectReplicationConnection(url, info);
              } else {
                  Properties props = null;
                  if ((props = this.parseURL(url, info)) == null) {
                      return null;
                  } else if (!"1".equals(props.getProperty("NUM_HOSTS"))) {
                      return this.connectFailover(url, info);
                  } else {
                      try {
                          com.mysql.jdbc.Connection newConn = ConnectionImpl.getInstance(this.host(props), this.port(props), props, this.database(props), url);
                          return newConn;
                      } catch (SQLException var6) {
                          throw var6;
                      } catch (Exception var7) {
                          SQLException sqlEx = SQLError.createSQLException(Messages.getString("NonRegisteringDriver.17") + var7.toString() + Messages.getString("NonRegisteringDriver.18"), "08001", (ExceptionInterceptor)null);
                          sqlEx.initCause(var7);
                          throw sqlEx;
                      }
                  }
              }
          }
          
          ......
          
      	public boolean acceptsURL(String url) throws SQLException {
              if (url == null) {
                  throw SQLError.createSQLException(Messages.getString("NonRegisteringDriver.1"), "08001", (ExceptionInterceptor)null);
              } else {
                  return this.parseURL(url, (Properties)null) != null;
              }
          }
          
          ......
          
          public Properties parseURL(String url, Properties defaults) throws SQLException {
              Properties urlProps = defaults != null ? new Properties(defaults) : new Properties();
              if (url == null) {
                  return null;
              } else if (!StringUtils.startsWithIgnoreCase(url, "jdbc:mysql://") && !StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:mxj://") && !StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:loadbalance://") && !StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:replication://")) {
                  return null;
              } else {
              
                  ......
                  
                  return urlProps;
              }
          }
      
    • 在连接数据库时,其核心是判断传入的URL是否符合自己的处理规则(parseURL方法),是的话才会和db建立连接。
      比如Mysql的URL规则 : jdbc:mysql://:/<service_name>等等
      Oracle定义的URL规则 : jdbc:oracle:thin:@//:/<service_name>等等
    • 也就是说java.sql.Driver接口规定了厂商实现该接口,并且定义自己的URL协议。厂商们实现的Driver接口通过acceptsURL(url)(一般来说内部调用的都是parseURL方法)来判断此url是否符合自己的协议,如果符合自己的协议,则可以使用本驱动进行数据库连接操作。
    • 通过以上源码分析我们可以看出,获取数据库连接时有两种写法:
      1.
      //通过DriverManager找到合适的驱动进行连接
      Connection connection = DriverManager.getConnection(url, props);  
      
      //直接从DriverManager中取出合适的Driver,然后在调用Driver的connect()方法进行连接
      Connection connection = DriverManager.getDriver(url).connect(url, props);  
      
    两种方式并没有本质上的区别,其内部实现都是通过Driver.acceptsURL()找到合适的Driver,在调用其内部实现都是通过acceptsURL找到合适的Driver.connect()方法进行连接
3、获取可以执行SQL的对象(Statement\PreparedStatement对象)
  • 前面两个步骤主要功能是使用驱动连接数据库,那么在工程与数据库连接完成后,是如何向数据库发送SQL语句并执行的呢?

    这就不得不提到Statement对象了,Statement对象的主要作用就是在 已经建立数据库连接的基础上来执行静态SQL语句并返回其生成的结果的对象,从开篇的JDBC使用实例中可以看出,Statement对象是和Connection对象相关联的(通过conn.createStatement()方法创建的),这也就是为什么说Statement对象是在已经建立数据库连接的基础上执行静态SQL语句并返回其生成的结果的对象。

    注:默认情况下,每个Statement对象只能同时打开一个ResultSet对象。因此,如果一个ResultSet对象的读取与另一个ResultSet对象的读取交织在一起,则每个ResultSet对象都必须由不同的语句对象生成。如果当前Statement对象存在打开的ResultSet对象,则Statement接口中的所有执行方法都会隐式关闭已打开的ResultSet对象。(什么意思?)

  • JDBC中的三种Statement,它们都作为在给定连接上执行 SQL语句的包容器,Statement、PreparedStatement(继承Statement )和CallableStatement(继承PreparedStatement)。
    • 1、Statement用于通用查询

      • 用于通用查询,该对象用于执行静态的 SQL语句,并且返回执行结果,此处的SQL语句必须是完整的。
      • 对于大量的批处理建议使用Statement,因为PreparedStatement的预编译空间有限,数据量大的时候可能有异常
    • 2、PreparedStatement用于执行参数化查询

      • 用于执行参数化查询,SQL语句被预编译并存储在 PreparedStatement对象中,这个语句只在第一次执行的时候编译一次,然后保存在缓存中,使用PreparedStatement的参数化的查询可以阻止大部分的SQL注入;
      • SQL语句在程序运行前已经进行了预编译,当运行时动态地把参数传给PreprareStatement时,即使参数里有敏感字符数据库也会作为一个字段的属性值来处理
      • 始终使用(?)作为占位符,并为占位符?的两边自动加上引号(’),所以PreparedStatement只能用来为可以加引号(’)的参数(如参数值)设置动态参数,即用?占位,不可用于表名、字段名等
    • 3、CallableStatement用于存储过程

      • 有些 DBMS将已存储过程中的每条语句视为独立的语句;而另外一些则将整个过程视为一个复合语句。在启用自动提交时,这种差别就变得非常重要,因为它影响什么时候调用commit 方法。在前一种情况中,每条语句单独提交;在后一种情况中,所有语句同时提交。
  • SQL注入是什么,以及Statement为什么有SQL注入的风险,而PreparedStatement又是如何防止SQL注入的?
    • SQL注入是指攻击者通过在应用程序原有查询语句的结尾添加额外的SQL语句,或者通过参数传入额外的SQL语句,从而在应用程序对用户输入数据的合法性没有判断或过滤不严的情况下,是的应用程序正常执行这些额外的SQL语句,从而在管理员不知情的情况下实现非法操作。

    • Statement为什么有SQL注入的风险呢,其关键点就在于Statement对象所执行的SQL语句必须是完整的,为什么这么说呢,看以下示例:

      Statement st = con.CreateStatement();    //创建一个Statement对象
      String name = "'张三'";                        //参数
      String sq = "delete from table1 where name="+name; //拼接完整的SQL语句delete from table1 where name='张三'
      st.execute(sq); //执行SQL
      

      由于Statement对象所执行的SQL语句必须是完整的,所以必须将SQL语句中的参数全部拼接完整才能传入Statement对象进行执行,那么如果在拼接过程中,传入的参数被篡改了,那么拼接出来的SQL语句也就被篡改了,比如说如下示例:

      Statement st = con.CreateStatement();    //创建一个Statement对象
      String name = "'张三' or 1=1";                        //参数
      String sq = "delete from table1 where name="+name; //拼接完整的SQL语句delete from table1 where name='张三' or 1=1
      st.execute(sq); //执行SQL
      

      这时数据库执行此条SQL语句的时候就会删除表中的所有数据,这就是SQL注入。

    • 那么PreparedStatement对象又是如何防止SQL注入的呢,其关键在于PreparedStatement执行的是参数化查询,也就是说他执行的SQL语句是预编译的,而且语句中使用了占位符,规定了SQL语句的结构。用户可以设置"?"的值,但是不能改变sql语句的结构。请看下面的示例:

      String sq = "delete from table1 where  name=?";//构建sql语句,以?作为占位符,这个位置的值待设置
      PreparedStatement ps = con.prepareStatement(sq);    //创建PreparedStatement时就传入sql语句,实现了预编译
      ps.setString(1,"张三");          //设置sql语句的占位符的值,注意第一个参数位置是1不是0
      ps.execute();          //执行这个PreparedStatement delete from table1 where name='张三'
      

      小节最开始的介绍中已经说了PreparedStatement会为占位符?的两边自动加上引号(’),那也就是说所有传入的参数都会被引号’'包裹,这样数据库在执行的时候,只会将引号中的内如视为参数,比如说如下示例:

      String sq = "delete from table1 where  name=? or id=?"//构建sql语句,以?作为占位符,这个位置的值待设置
      PreparedStatement ps = con.prepareStatement(sq);   //创建PreparedStatement时就传入sql语句,实现了预编译 
      ps.setString(1,"张三 or 1=1");  //设置sql语句的占位符的值,注意第一个参数位置是1不是0
      ps.execute(); //执行这个PreparedStatement delete from table1 where name='张三 or 1=1'
      

      这时数据库在执行这条sql的时候,删除的只是name为’张三 or 1=1’的数据,并不会删除全部数据。

    • 那么可能有人会说如果我在参数中传入一个(’)呢,那SQL语句在?两边自动添加的(’)不就被截断了吗,我不就可以进行SQL注入了吗,那是不是这样呢,我们看以下示例:

      String sq = "delete from table1 where name=?"//构建sql语句,以?作为占位符,这个位置的值待设置
      PreparedStatement ps = con.prepareStatement(sq);   //创建PreparedStatement时就传入sql语句,实现了预编译 
      ps.setString(1,"张三' or '1'='1");  //设置sql语句的占位符的值,注意第一个参数位置是1不是0
      ps.execute();//执行这个PreparedStatement
      

      这个时候可能我们期待的SQL语句应该是:

      delete from table1 where  name='张三' or '1'='1'
      

      那真实情况是不是这样的呢,显然并不是,将PreparedStatement.toString()打印出来可以看到,真实的sql其实是:

      delete from table1 where  name='张三\' or \'1\'=\'1'
      

      也就是说prepareStatement对象把参数中的单引号用\反斜杠做了转义,从而达到了防止sql注入的目的。

    • 综上所述:PrepareStatement通过在参数两侧添加引号和转义参数中的引号的方式(只要用户输入的字符串中有单引号,那mysql数据库产商的setString()这个函数,就会把单引号做转义)来防止SQL注入,这样就有效屏蔽了非法的SQL和数据库关键字引起的SQL注入问题。

    • 所以在实际开发中,一般采用PreparedStatement访问数据库,它不仅能防止sql注入,还是预编译的(不用改变一次参数就要重新编译整个sql语句,效率高),此外,它执行查询语句得到的结果集是离线的,连接关闭后,仍然可以访问结果集。

4、执行SQL并获取返回结果
  • Statement 接口提供了三种执行 SQL 语句的方法:ResultSet executeQuery()、int executeUpdate() 和 boolean execute();以及一个用于批量处理SQL的方法: int[] executeBatch()
    • 方法 executeQuery 用于产生单个 结果集的语句,例如 SELECT 语句
    • 方法 executeUpdate 用于执行 INSERT、UPDATE 或 DELETE 语句以及 SQL DDL( 数据定义语言)语句,例如 CREATE TABLE 和 DROP TABLE。INSERT、UPDATE 或 DELETE 语句的效果是修改表中零行或多行中的一列或多列。executeUpdate 的返回值是一个整数,指示受影响的行数(即更新计数)。对于 CREATE TABLE 或 DROP TABLE 等不操作行的语句,executeUpdate 的返回值总为零。
    • 方法 execute 用于执行返回多个结果集、多个更新计数或二者组合的语句。Statement的execute可以测试所执行的SQL是执行查询或是更新,传回 true的话表示SQL执行将传回ResultSet表示查询结果,此时可以使用getResultSet()取得ResultSet对象,如果 execute传回false,表示SQL执行会传回更新笔数或没有结果,此时可以使用getUpdateCount()取得更新笔数。如果事先无法得知是进行查询或是更新,就可以使用execute。
    • 当需要向数据库发送一批SQL语句执行时,应避免向数据库一条条发送执行,采用批处理以提升执行效率,批处理又两种方式
      • Statement
        //将sql添加到批处理
        Statement statement = connection.createStatement();
        statement.addBatch(sql1);
        statement.addBatch(sql2);
        //执行批处理
        statement.executeBatch();
        //清空批处理的sql
        statement.clearBatch();
        
      • PreparedStatement
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        for (int i = 1; i <= 205; i++) {
            preparedStatement.setInt(1, i);
            preparedStatement.setString(2, "TEST"+i);
            //添加到批处理中
            preparedStatement.addBatch();
            if (i % 100 ==0) {//每到100条的时候,执行一下当前对象中的sql,并清空
                //执行批处理
                preparedStatement.executeBatch();
                //清空批处理【如果数据量太大,所有数据存入批处理,内存肯定溢出】
                preparedStatement.clearBatch();
            }
        }
        //剩下的再执行一次批处理
        preparedStatement.executeBatch();
        //再清空
        preparedStatement.clearBatch();
        
        为什么每到100条的时候,执行一下当前PreparedStatement对象中的SQL并清空呢?这就是因为之前说的PreparedStatement的预编译空间是有限的,如果数据量过大的话,就必然会出现内存溢出,所以说对于大量的批处理建议使用Statement。
  • ResultSet用法
    • ResultSet是数据中查询结果返回的一种对象,可以说ResultSet是一个存储查询结果的对象,但是ResultSet并不仅仅具有存储的功能,他同时还具有操纵数据的功能,可能完成对数据的更新等。

    • 四种Resultset

      1. 最基本的ResultSet
        • 这个ResultSet他起到的作用就是完成了查询结果的存储功能,而且只能读取一次,不能够来回的滚动读取。这种结果集的创建方式如下:
          Statement st = conn.CreateStatement
          ResultSet rs = Statement.excuteQuery(sqlStr);
          
        • 由于这种结果集不支持,滚动的读去功能所以,如果获得这样一个结果集,只能使用它里面的next()方法,逐个的读去数据。
      2. 可滚动的ResultSet类型
        • 这个类型支持前后滚动取得纪录next()、previous(),回到第一行first(),同时还支持要去的ResultSet中的第几行absolute(int n),以及移动到相对当前行的第几行relative(int n),要实现这样的ResultSet在创建Statement时用如下的方法:
          Statement st = conn.createStatement(int resultSetType, int resultSetConcurrency)
          ResultSet rs = st.executeQuery(sqlStr)
          
          • 1、resultSetType是设置ResultSet对象的类型可滚动,或者是不可滚动。取值如下:

            ResultSet.TYPE_FORWARD_ONLY只能向前滚动,
            ResultSet.TYPE_SCROLL_INSENSITIVE和Result.TYPE_SCROLL_SENSITIVE任意的前后滚动, 二者的区别在于前者对于修改不敏感,而后者对于修改敏感。

          • 2、resultSetConcurency是设置ResultSet对象能够修改的,取值如下:

            ResultSet.CONCUR_READ_ONLY 设置为只读类型的参数。
            ResultSet.CONCUR_UPDATABLE 设置为可修改类型的参数。

      3. 可更新的ResultSet
      • 这样的ResultSet对象可以完成对数据库中表的修改,但是我知道ResultSet只是相当于数据库中表的视图,所以并不时所有的ResultSet只要设置了可更新就能够完成更新的,能够完成更新的ResultSet的SQL语句必须要具备如下的属性:
        • a、只引用了单个表。
        • b、不含有join或者group by子句。
        • c、那些列中要包含主关键字。
          具有上述条件的,可更新的ResultSet可以完成对数据的修改,可更新的结果集的创建方法是:
        Statement st = conn.createstatement(Result.TYPE_SCROLL_INSENSITIVE,Result.CONCUR_UPDATABLE)
        
      • 这样的Statement的执行结果得到的就是可更新的结果集。更新的方法是,把ResultSet的游标移动到你要更新的行,然后调用updateXXX()。此方法有两个参数,第一个是要更新的列,可以是列名或者序号。第二个是要更新的数据,这个数据类型要和XXX相同。每完成对一行的update要调用updateRow()完成对数据库的写入,而且是在ResultSet的游标没有离开该修改行之前,否则修改将不会被提交。
      • 使用updateXXX方法还可以完成插入操作。但是首先要介绍两个方法:
        1. moveToInsertRow()是把ResultSet移动到插入行,这个插入行是表中特殊的一行,不需要指定具体那一行,只要调用这个方法系统会自动移动到那一行的。
        2. moveToCurrentRow()这是把ResultSet移动到记忆中的某个行,通常当前行。如果没有使用insert操作,这个方法没有什么效果,如果使用了insert操作,这个方法用于返回到insert操作之前的那一行,离开插入行,当然也可以通过next(),previous()等方法离开插入行。
        3. 要完成对数据库的插入,首先调用moveToInsertRow()移动到插入行,然后调用updateXXX的方法完成对各列数据的更新,完成更新后和更新操作一样,要写到数据库,不过这里使用的是insertRow(),也要保证在该方法执行之前ResultSet没有离开插入列,否则插入不被执行,并且对插入行的更新将丢失。
      1. 可更新的ResultSet
      • 正常情况下如果使用Statement执行完一个查询,又去执行另一个查询时这时候第一个查询的结果集就会被关闭,也就是说,所有的Statement的查询对应的结果集是一个,如果调用Connection的commit()方法也会关闭结果集。可保持性就是指当ResultSet的结果被提交时,是被关闭还是不被关闭。JDBC2.0和1.0提供的都是提交后ResultSet就会被关闭。不过在JDBC3.0中,我们可以设置ResultSet是否关闭。要完成这样的ResultSet的对象的创建方法如下:
        Statement st=conn.createStatement(int resultSetType,int resultSetConcurrency,int resultSetHoldability)
        ResultSet rs = st.excuteQuery(sqlStr);
        
        • 前两个参数和之前两个参数的含义一致,resultSetHoldability表示在结果集提交后结果集是否打开,取值如下:

            ResultSet.HOLD_CURSORS_OVER_COMMIT:表示修改提交时,不关闭ResultSet。
            ResultSet.CLOSE_CURSORS_AT_COMMIT:表示修改提交时,关闭ResultSet。  
          

注:执行语句的所有方法都将关闭所调用的 Statement 对象的当前打开 结果集(如果存在)。这意味着在重新执行 Statement 对象之前,需要完成对当前 ResultSet 对象的处理。

5、释放资源并关闭连接
  • Connection、Statement和ResulSet?这三个对象是在方法内部定义的,则这三个对象不是在方法执行完毕就消失了么,为什么还要单独去关闭它们呢?
    • 这个连接是与数据库服务器的一个连接,虽然你的方法结束了,但是这个资源依然存在数据库连接并没有释放
  • 为什么在JDBC对数据库访问结束后,要按先关闭ResultSet,然后关闭PreparedStatement,最后关闭Connection,直接关闭Connection不就行了吗?
    • 感觉上好象是只要把connection给关闭了,系统就能正常运行了。 那在查询或是其它操作中,如果只关闭Connection,不作ResultSet 和 Statement 的关闭的话,对系统性能是否会有影响呢。或者是其它实方面的不良影响。
    • 如果你不使用连接池,那么就没有什么问题,一旦Connection关闭,数据库物理连接就被释放,所有相关Java资源也可以被GC回收了。
    • 但是如果你使用连接池,那么请注意,Connection关闭并不是物理关闭,只是归还连接池,所以PreparedStatement和ResultSet都被持有,并且实际占用相关的数据库的游标资源,在这种情况下,只要长期运行,往往就会报“游标超出数据库允许的最大值”的错误,导致程序无法正常访问数据库。
    • 因为你打开的时候有顺序,这个就像栈,后进先出
      • 打开时:Connection -> PreparedStatement -> ResultSet
      • 关闭时:ResultSet-> PreparedStatement -> Connection
  • 如果不按顺序释放资源会出现什么问题?

    如果直接关闭connection的话,就是说将其他也关闭了,再对其他的如statement进行操作就会出现:"Operation not allowed after ResultSet closed(操作不允许在关闭以后进行)"的异常。

拓展阅读

  1. SPI服务机制
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值