MyBatis 3源码深度解析
上QQ阅读APP看书,第一时间看更新

2.4 Statement详解

本节我们来学习JDBC API中比较重要的部分—Statement接口及它的子接口PreparedStatement和CallableStatement。Statement接口中定义了执行SQL语句的方法,这些方法不支持参数输入,PreparedStatement接口中增加了设置SQL参数的方法,CallableStatement接口继承自PreparedStatement,在此基础上增加了调用存储过程以及检索存储过程调用结果的方法。

2.4.1 java.sql.Statement接口

Statement是JDBC API操作数据库的核心接口,具体的实现由JDBC驱动来完成。Statement对象的创建比较简单,需要调用Connection对象的createStatement()方法,例如:

在应用程序中,每个Connection对象可以同时创建多个Statement对象,例如;

此外,Connection接口中还提供了几个重载的createStatement()方法,用于通过Statement对象指定ResultSet(结果集)的属性,例如:

上面的代码中,我们创建了一个Statement对象,通过参数指定该Statement对象创建的ResultSet对象是可滚动的,而且是可以修改的,当修改提交时ResultSet不会被关闭。关于ResultSet的更多细节会在本书后面的章节中介绍。

Statement的主要作用是与数据库进行交互,该接口中定义了一些数据库操作以及检索SQL执行结果相关的方法,具体如下:

Statement接口中提供的与数据库交互的方法比较多,具体调用哪个方法取决于SQL语句的类型。

如果使用Statement执行一条查询语句,并返回一个结果集(ResultSet对象),则可以调用executeQuery()方法。

如果SQL语句是一个返回更新数量的DML语句,则需要调用executeUpdate()方法,该方法有几个重载的方法,下详细介绍。

  • int executeUpdate(String sql):执行一个UPDATE、INSERT或者DELETE语句,返回更新数量。
  • int executeUpdate(String sql, int autoGeneratedKeys):执行一个UPDATE、INSERT或者DELETE语句。当SQL语句是INSERT语句时,autoGeneratedKeys参数用于指定自动生成的键是否能够被检索,取值为Statement.RETURN_GENERATED_KEYS或Statement.NO_GENERATED_KEYS。当参数值为Statement.RETURN_GENERATED_KEYS时,INSERT语句自动生成的键能够被检索。当我们向数据库中插入一条记录,希望获取这条记录的自增主键时,可以调用该方法,指定第二个参数值为Statement.RETURN_GENERATED_KEYS。
  • int executeUpdate(String sql, int[] columnIndexes):执行一个UPDATE、INSERT或者DELETE语句,通过columnIndexes参数告诉驱动程序哪些列中自动生成的键可以用于检索。columnIndexes数组用于指定目标表中列的索引,这些列中自动生成的键必须能够被检索。如果SQL语句不是INSERT语句,columnIndexes参数将会被忽略。
  • int executeUpdate(String sql, String[]columnNames):这个方法的作用和executeUpdate(String sql, int[]columnIndexes)相同,不同的是columnNames参数是一个String数组,通过字段名的方式指定哪些字段中自动生成的键能够被检索。如果SQL语句不是INSERT语句,columnNames参数就会被忽略。

注意

如果数据库支持返回的更新数量大于Integer.MAX_VALUE,则需要调用executeLargeUpdate()方法。

当我们在执行数据库操作之前,若不确定SQL语句的类型,则可以调用excute()方法。该方法也有几个重载的方法,分别说明如下。

  • boolean execute(String sql):执行一个SQL语句,通过返回值判断SQL类型,当返回值为true时,说明SQL语句为SELECT语句,可以通过Statement接口中的getResultSet()方法获取查询结果集;否则为UPDATE、INSERT或者DELETE语句,可以通过Statement接口中的getUpdateCount()方法获取影响的行数。
  • boolean execute(String sql, int autoGeneratedKeys):该方法通过autoGeneratedKeys参数(只对INSERT语句有效)指定INSERT语句自动生成的键是否能够被检索。
  • boolean execute(String sql, int[]columnIndexes):通过columnIndexes参数告诉驱动程序哪些列中自动生成的键可以用于检索。columnIndexes数组用于指定目标表中列的索引,这些列中自动生成的键必须能够被检索。如果SQL语句不是INSERT语句,则columnIndexes参数将会被忽略。
  • boolean execute(String sql, String[]columnNames):columnNames参数是一个String数组,通过字段名的方式指定哪些字段中自动生成的键能够被检索。如果SQL语句不是INSERT语句,则columnNames参数会被忽略。

注意

当数据库支持返回影响的行数大于Integer.MAX_VALUE时,需要使用getLargeUpdateCount()方法。

另外,execute()方法可能返回多个结果。我们可以通过Statement对象的getMoreResults()方法获取下一个结果,当getMoreResults()方法的返回值为true时,说明下一个结果为ResultSet对象;当返回值为false时,说明下一个结果为影响行数,或者没有更多结果。获取Statement对象所有SQL执行结果的案例代码如下:

默认情况下,每次调用getMoreResults()方法都会关闭上一次调用getResultSet()方法返回的ResultSet对象。但是,我们可以通过重载getMoreResults()方法的参数指定是否关闭ResultSet 对象。

Statement接口中定义了3个常量可以用作getMoreResults()的参数,具体如下。

  • CLOSE_CURRENT_RESULT:表明当返回下一个ResultSet对象时,当前ResultSet对象应该关闭。
  • KEEP_CURRENT_RESULT:表明当返回下一个ResultSet对象时,当前ResultSet对象不关闭。
  • CLOSE_ALL_RESULTS:表明当返回下一个ResultSet对象时,当前所有未关闭的ResultSet对象都关闭。

如果当前结果是影响行数,而不是ResultSet对象,则getMoreResults()方法的参数将会被忽略。为了确定JDBC驱动是否支持通过getMoreResults()方法获取下一个结果,我们可以调用DatabaseMetaData接口提供的supportsMultipleOpenResults()方法,DatabaseMetaData的相关细节将会在后面的章节中介绍。

除此之外,Statement接口中还提供了几个方法,用于批量执行SQL语句,分别为:

  • void addBatch(String sql):把一条SQL语句添加到批量执行的SQL列表中。
  • void clearBatch():清空批量执行的SQL列表。
  • int[]executeBatch():批量地执行SQL列表中的语句。

下面是使用Statement接口提供的方法批量执行SQL语句的一个案例,具体代码如下:

上面的代码中,我们调用addBatch()方法将两个INSERT语句添加到批量执行的SQL列表中,然后调用executeBatch()方法批量执行SQL列表中的语句。

完整代码及运行结果可参考随书源码mybatis-chapter02项目中的com.blog4java.jdbc.Example05案例。

Statement接口中除了提供操作数据库相关的方法外,还提供了一系列属性相关的方法,这些方法用于设置或获取Statement相关的属性,代码如下:

这些方法的含义可参考JDBC API文档,这里就不做过多介绍了。

2.4.2 java.sql.PreparedStatement接口

PreparedStatement接口继承自Statement接口,在Statement接口的基础上增加了参数占位符功能。PreparedStatement接口中增加了一些方法,可以为占位符设置值。PreparedStatement的实例表示可以被预编译的SQL语句,执行一次后,后续多次执行时效率会比较高。使用PreparedStatement实例执行SQL语句时,可以使用“?”作为参数占位符,然后使用PreparedStatement接口中提供的方法为占位符设置参数值。

PreparedStatement对象的创建比较简单,与Statement类似,只需要调用Connection对象的prepareStatement()方法。与创建Statement对象不同的是,prepareStatement()方法需要提供一个SQL语句作为参数,例如:

前面的章节中有提到过,使用createStatement()方法创建Statement对象时,可以通过参数指定ResultSet的特性。与createStatement()方法类似,prepareStatement()也可以通过重载的方法指定ResultSet的特性,例如:

PreparedStatement接口中定义了一系列的Setter方法,用于为SQL语句中的占位符赋值,这些Setter方法名称遵循set<Type>格式,其中Type为数据类型。例如,setString()方法用于为参数占位符设置一个字符串类型的值。这些Setter方法一般都有两个参数,第一个参数为int类型,表示参数占位符的位置(从1开始);第二个参数为占位符指定的值。下面是一个为PreparedStatement对象SQL占位符赋值的例子,具体如下:

需要注意的是,在使用PreparedStatement对象执行SQL语句之前必须为每个参数占位符设置对应的值,否则调用executeQuery()、executeUpdate()或execute()等方法时会抛出SQLException异常。

PreparedStatement对象设置的参数在执行后不能被重置,需要显式地调用clearParameters()方法清除先前设置的值,再为参数重新设置值即可。

注意

在使用PreparedStatement对象执行SQL时,JDBC驱动通过setAsciiStream()、setBinaryStream()、setCharacterStream()、setNCharacterStream()或setUnicodeStream()等方法读取参数占位符设置的值。这些参数值必须在下一次执行SQL时重置掉,否则将会抛出SQLException异常。

对于一个给定的Statement对象,在execute()、executeQuery()、executeUpdate()、executeBatch()或clearParameters()方法调用之前,如果占位符已经使用setXXX()方法设置值,应用程序不可以再次调用setXXX()方法修改已经设置的值。但是应用程序可以在execute()、executeQuery()、executeUpdate()、executeBatch()或clearParameters()方法调用后,再次调用setXXX()方法覆盖先前设置的值。不遵循这一约束可能会导致不可预知的结果。

我们在使用setXXX()方法为参数占位符设置值时存在一个数据转换过程。setXXX()方法的参数为Java数据类型,需要转换为JDBC类型(java.sql.Types中定义的SQL类型),这一过程由JDBC驱动来完成。Java类型与JDBC类型之间的对应关系如表2-2所示。

表2-2 Java类型与JDBC类型的对应关系

PreparedStatement接口中提供了一个setObject()方法,可以将Java类型转换为JDBC类型。该方法可以接收三个参数,第一个参数为占位符位置,第二个参数为Java对象,第三个参数是要转换成的JDBC类型。如果Java对象与JDBC类型不兼容,就会抛出SQLException异常。

下面是使用setObject()方法将Java中的Integer类型转换为JDBC中的SHORT类型的案例,具体代码如下:

另外,setObject()方法可以只接收两个参数,不用指定JDBC类型。这种情况下,JDBC驱动会按照表2-2中的映射关系将Java类型隐式地转换为对应的JDBC类型,例如:

PreparedStatement接口中提供了一个setNull()方法,可以将占位符参数设置为JDBC的NULL。该方法接收两个参数,第一个参数为占位符的位置,第二个参数为JDBC类型。该方法的语法格式如下:

如果接收Java对象的setXXX()方法参数为null,则该参数的占位符被设置为JDBC的NULL。

JDBC API中提供了一个ParameterMetaData接口,用于描述PreparedStatement对象的参数信息,包括参数个数、参数类型等。PreparedStatement接口中提供了一个getParameterMetaData()方法,用于获取ParameterMetaData实例。下面是使用ParameterMetaData获取参数信息的案例,代码如下:

上面的代码中,我们通过getParameterMetaData()方法获取PreparedStatement对象相关的ParameterMetaData对象,然后通过getParameterCount()方法获取参数数量,接着调用getParameterTypeName()方法获取参数的类型,完整代码可参考随书源码mybatis-chapter02项目的com.blog4java.jdbc.Example06案例。运行上面的代码,控制台输出内容如下:

2.4.3 java.sql.CallableStatement接口

CallableStatement接口继承自PreparedStatement接口,在PreparedStatement的基础上增加了调用存储过程并检索调用结果的功能。

与Statement、PreparedStatement一样,CallableStatement对象也是通过Connection对象创建的,我们只需要调用Connection对象的prepareCall()方法即可,例如:

CallableStatement对象可以使用3种类型的参数:IN、OUT和INOUT。可以将参数指定为序数参数或命名参数,必须为IN或INOUT参数的每个参数占位符设置一个值,必须为OUT或INOUT参数中的每个参数占位符调用registerOutParameter()方法。存储过程参数的数量、类型和属性可以使用DatabaseMetaData接口提供的getProcedureColumns()方法获取。需要注意的是,使用setXXX()方法为参数占位符设置值时,下标必须从1开始。语句中的字面量参数值不会增加参数占位符的序数值,例如:

命名参数可以用来指定特定的参数,这在存储过程有多个给定默认值的参数时特别有用,命名参数可以用于为那些没有默认值的参数设置值,参数名称可以通过DatabaseMetaData对象的getProcedureColumns()方法返回的COLUMN_NAME字段获取。

例如,在下面的案例中,COMPLEX_PROC存储过程可以接收10个参数,但是只有第1个和第5个参数(PARAM_1和PARAM_5)需要设置值。

CallableStatement接口中新增了一些额外的方法允许参数通过名称注册和检索。

DatabaseMetaData接口中提供了supportsNamedParameters()方法,用于判断JDBC驱动是否支持指定命名参数。

注意

为存储过程调用语句设置参数时,不能够将下标和名称混合使用,否则会抛出SQLException异常。

对于IN参数的设置,调用CallableStatement接口中提供的setXXX()方法即可;但是对于OUT和INOUT参数,在CallableStatement执行之前,必须为每个参数调用CallableStatement接口中提供的registerOutParameter()方法,例如:

与Statement、PreparedStatement类似,CallableStatement也是使用executeQuery()、executeUpdate()、execute()等方法执行存储过程的调用,返回结果可能是ResultSet对象或者影响的行数,存储过程调用结果的处理与Statement对象执行SQL结果的处理过程类似,这里就不重复介绍了。

2.4.4 获取自增长的键值

目前大多数数据库都支持自增长主键,当向表中插入数据时,数据库引擎可以自动生成自增长主键。Statement接口中提供了getGeneratedKeys()方法,用于获取数据库自动生成的值,该方法返回一个ResultSet对象,我们可以从ResultSet对象中获取数据库中所有自增长的键值。

Statement接口中的execute()、executeUpdate()和Connection接口的prepareStatement()方法都可以接收一个可选的参数,该参数用于指定由数据库生成的值是否可以被检索,例如:

另外,Statement接口中还提供了execute()、executeUpdate()重载方法,能够通过下标或者字段名指定哪些字段中自动生成的值可以被检索,例如:

如上面的代码所示,通过keyColumn数组指定在所有自动生成的值中只有名称为ORDER_ID的字段可以被检索。

接下来我们看一个完整的获取数据库自增长主键的案例,代码如下:

完整代码,读者可参考随书源码mybatis-chapter02项目中的com.blog4java.jdbc.Example07案例。

注意

使用Statement对象执行完SQL后也需要关闭。Statement接口中提供了close()方法,用于关闭Statement对象;另外还提供了isClosed()方法,用于判断该Statement对象是否关闭。