这些Java教程是针对JDK 8编写的。本页面中描述的示例和实践不利用后续版本中引入的改进,并可能使用不再可用的技术。
有关Java SE 9及后续版本中更新的语言特性的摘要,请参阅Java语言变更。
有关所有JDK版本的新功能、增强功能以及已删除或弃用选项的信息,请参阅JDK发行说明。
下面的方法CoffeesTable.viewTable
输出COFFEES
表的内容,并演示了ResultSet
对象和游标的使用:
public static void viewTable(Connection con) throws SQLException { String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES"; try (Statement stmt = con.createStatement()) { ResultSet rs = stmt.executeQuery(query); while (rs.next()) { String coffeeName = rs.getString("COF_NAME"); int supplierID = rs.getInt("SUP_ID"); float price = rs.getFloat("PRICE"); int sales = rs.getInt("SALES"); int total = rs.getInt("TOTAL"); System.out.println(coffeeName + ", " + supplierID + ", " + price + ", " + sales + ", " + total); } } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); } }
ResultSet
对象是表示数据库结果集的数据表,通常通过执行查询数据库的语句生成。例如,当执行CoffeeTables.viewTable
方法时,会通过Statement
对象stmt
执行查询,并创建一个ResultSet
对象rs
。注意,ResultSet
对象可以通过实现Statement
接口的任何对象创建,包括PreparedStatement
、CallableStatement
和RowSet
。
通过游标访问ResultSet
对象中的数据。注意,这里的游标不是数据库游标,而是指向ResultSet
中一行数据的指针。初始时,游标位于第一行之前。方法ResultSet.next
将游标移动到下一行。如果游标位于最后一行之后,该方法返回false
。该方法使用while
循环反复调用ResultSet.next
方法,以迭代遍历ResultSet
中的所有数据。
本页面涵盖以下主题:
ResultSet
接口提供了检索和操作执行查询的结果的方法,ResultSet
对象可以具有不同的功能和特性。这些特性包括类型、并发性和游标可保持性。
ResultSet
对象的类型决定了它在两个方面的功能级别:光标可以如何操作以及底层数据源的并发更改如何反映在ResultSet
对象中。
ResultSet
对象的灵敏度由三种不同的ResultSet
类型之一确定:
TYPE_FORWARD_ONLY
:结果集无法滚动;其光标只能从第一行之前移动到最后一行之后。结果集中包含的行取决于底层数据库如何生成结果。也就是说,它包含在查询执行时或在检索行时满足查询的行。TYPE_SCROLL_INSENSITIVE
:结果集可以滚动;其光标可以相对于当前位置向前和向后移动,并且可以移动到绝对位置。结果集对在其打开状态下对底层数据源所做的更改不敏感。结果集中包含的行取决于在查询执行时或在检索行时满足查询的行。TYPE_SCROLL_SENSITIVE
:结果集可以滚动;其光标可以相对于当前位置向前和向后移动,并且可以移动到绝对位置。结果集反映了在结果集保持打开状态时对底层数据源所做的更改。默认的ResultSet
类型是TYPE_FORWARD_ONLY
。
注意:并非所有数据库和JDBC驱动程序都支持所有的ResultSet
类型。方法DatabaseMetaData.supportsResultSetType
如果指定的ResultSet
类型被支持则返回true
,否则返回false
。
ResultSet
对象的并发性确定了支持的更新功能级别。
有两个并发级别:
CONCUR_READ_ONLY
:不能使用ResultSet
接口更新ResultSet
对象。CONCUR_UPDATABLE
:可以使用ResultSet
接口更新ResultSet
对象。默认的ResultSet
并发性是CONCUR_READ_ONLY
。
注意:并非所有的JDBC驱动程序和数据库都支持并发性。方法DatabaseMetaData.supportsResultSetConcurrency
如果驱动程序支持指定的并发级别则返回true
,否则返回false
。
方法CoffeesTable.modifyPrices
演示了如何使用并发级别为CONCUR_UPDATABLE
的ResultSet
对象。
调用Connection.commit
方法可以关闭在当前事务中创建的ResultSet
对象。然而,在某些情况下,这可能不是期望的行为。ResultSet
属性holdability可以使应用程序控制在调用commit时是否关闭ResultSet
对象(游标)。
以下ResultSet
常量可供Connection
方法createStatement
、prepareStatement
和prepareCall
使用:
HOLD_CURSORS_OVER_COMMIT
:不关闭ResultSet
游标;它们是可保持的:当调用commit
方法时它们保持打开。如果应用程序主要使用只读的ResultSet
对象,则保持游标可能是理想的选择。CLOSE_CURSORS_AT_COMMIT
:在调用commit
方法时关闭ResultSet
对象(游标)。对于某些应用程序,当调用此方法时关闭游标可能会提高性能。默认的游标保持性取决于您的DBMS。
注意:并非所有的JDBC驱动程序和数据库都支持可保持和不可保持的游标。以下方法JDBCTutorialUtilities.cursorHoldabilitySupport
输出ResultSet
对象的默认游标保持性以及是否支持HOLD_CURSORS_OVER_COMMIT
和CLOSE_CURSORS_AT_COMMIT
:
public static void cursorHoldabilitySupport(Connection conn) throws SQLException { DatabaseMetaData dbMetaData = conn.getMetaData(); System.out.println("ResultSet.HOLD_CURSORS_OVER_COMMIT = " + ResultSet.HOLD_CURSORS_OVER_COMMIT); System.out.println("ResultSet.CLOSE_CURSORS_AT_COMMIT = " + ResultSet.CLOSE_CURSORS_AT_COMMIT); System.out.println("Default cursor holdability: " + dbMetaData.getResultSetHoldability()); System.out.println("Supports HOLD_CURSORS_OVER_COMMIT? " + dbMetaData.supportsResultSetHoldability( ResultSet.HOLD_CURSORS_OVER_COMMIT)); System.out.println("Supports CLOSE_CURSORS_AT_COMMIT? " + dbMetaData.supportsResultSetHoldability( ResultSet.CLOSE_CURSORS_AT_COMMIT)); }
ResultSet
接口声明了获取列值的getter方法(例如getBoolean
和getLong
)。可以使用列的索引号或别名或名称来检索值。列索引通常更有效率。列从1开始编号。为了最大的可移植性,应该按从左到右的顺序读取每行中的结果集列,并且每列只能读取一次。
例如,下面的方法CoffeesTable.alternateViewTable
通过编号检索列值:
public static void alternateViewTable(Connection con) throws SQLException { String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES"; try (Statement stmt = con.createStatement()) { ResultSet rs = stmt.executeQuery(query); while (rs.next()) { String coffeeName = rs.getString(1); int supplierID = rs.getInt(2); float price = rs.getFloat(3); int sales = rs.getInt(4); int total = rs.getInt(5); System.out.println(coffeeName + ", " + supplierID + ", " + price + ", " + sales + ", " + total); } } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); } }
作为getter方法的输入的字符串是不区分大小写的。当使用一个字符串调用getter方法时,如果有多个列具有相同的别名或名称与该字符串相匹配,则返回第一个匹配列的值。使用字符串而不是整数的选项是为了在生成结果集的SQL查询中使用列别名和名称。对于在查询中没有显式命名的列(例如select * from COFFEES
),最好使用列号。如果使用列名,则开发人员应该使用列别名来确保它们唯一地指向所需的列。列别名有效地重命名了结果集的列。要指定列别名,请在SELECT
语句中使用SQL AS
子句。
适当类型的getter方法检索每列的值。例如,在方法CoffeeTables.viewTable
中,ResultSet
rs
的每一行的第一列是COF_NAME
,它存储了一个SQL类型VARCHAR
的值。检索SQL类型VARCHAR
的值的方法是getString
。每一行的第二列存储了一个SQL类型INTEGER
的值,检索该类型值的方法是getInt
。
请注意,尽管建议使用getString
方法检索SQL类型CHAR
和VARCHAR
,但也可以使用它来检索任何基本SQL类型。使用getString
获取所有值可能非常有用,但它也有其局限性。例如,如果用于检索数字类型,getString
会将数字值转换为Java的String
对象,然后该值必须转换回数字类型才能作为数字进行操作。在将该值视为字符串的情况下,没有缺点。此外,如果希望应用程序检索除SQL3类型以外的任何标准SQL类型的值,请使用getString
方法。
如前所述,您可以通过游标访问ResultSet
对象中的数据,游标指向ResultSet
对象中的一行。但是,当ResultSet
对象首次创建时,游标位于第一行之前。方法CoffeeTables.viewTable
通过调用ResultSet.next
方法移动游标。还有其他可用于移动游标的方法:
next
:将游标向前移动一行。如果游标现在位于一行上,则返回true
,如果游标位于最后一行之后,则返回false
。previous
:将游标向后移动一行。如果游标现在位于一行上,则返回true
,如果游标位于第一行之前,则返回false
。first
:将游标移动到ResultSet
对象中的第一行。如果游标现在位于第一行上,则返回true
,如果ResultSet
对象不包含任何行,则返回false
。last:
:将游标移动到ResultSet
对象中的最后一行。如果游标现在位于最后一行上,则返回true
,如果ResultSet
对象不包含任何行,则返回false
。beforeFirst
:将游标定位在ResultSet
对象的开头,在第一行之前。如果ResultSet
对象不包含任何行,则此方法无效。afterLast
:将游标定位在ResultSet
对象的末尾,在最后一行之后。如果ResultSet
对象不包含任何行,则此方法无效。relative(int rows)
:相对于当前位置移动游标。absolute(int row)
:将游标定位到由参数row
指定的行。请注意,默认的ResultSet
的灵敏度为TYPE_FORWARD_ONLY
,这意味着它不能滚动;如果您的ResultSet
不能滚动,则不能调用移动游标的这些方法之一,除了next
。以下部分将介绍方法CoffeesTable.modifyPrices
,演示如何移动ResultSet
的游标。
您无法更新默认的ResultSet
对象,并且只能向前移动其游标。但是,您可以创建可滚动(游标可以向后移动或移动到绝对位置)和可更新的ResultSet
对象。
以下方法CoffeesTable.modifyPrices
,将每行的PRICE
列乘以参数percentage
:
public void modifyPrices(float percentage) throws SQLException { try (Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE)) { ResultSet uprs = stmt.executeQuery("SELECT * FROM COFFEES"); while (uprs.next()) { float f = uprs.getFloat("PRICE"); uprs.updateFloat("PRICE", f * percentage); uprs.updateRow(); } } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); } }
字段ResultSet.TYPE_SCROLL_SENSITIVE
创建一个ResultSet
对象,它的游标可以相对于当前位置向前和向后移动到绝对位置。字段ResultSet.CONCUR_UPDATABLE
创建一个可以更新的ResultSet
对象。请参阅ResultSet
Javadoc以了解可以指定的其他字段,以修改ResultSet
对象的行为。
方法ResultSet.updateFloat
更新光标所在行的指定列(在本例中为PRICE
)为指定的float
值。ResultSet
包含各种更新器方法,使您能够更新各种数据类型的列值。但是,这些更新器方法都不会修改数据库;您必须调用方法ResultSet.updateRow
来更新数据库。
Statement
、PreparedStatement
和CallableStatement
对象有一个与之关联的命令列表。该列表可以包含用于更新、插入或删除行的语句,也可以包含DDL语句,例如CREATE TABLE
和DROP TABLE
。但是,它不能包含会生成ResultSet
对象的语句,如SELECT
语句。换句话说,该列表只能包含生成更新计数的语句。
与Statement
对象关联的列表在创建时为空。您可以使用方法addBatch
将SQL命令添加到该列表中,并使用方法clearBatch
将其清空。当您添加完所有语句到列表后,调用方法executeBatch
将它们作为一个单元或批量发送到数据库执行。
例如,以下方法CoffeesTable.batchUpdate
通过批量更新向COFFEES
表中添加了四行:
public void batchUpdate() throws SQLException { con.setAutoCommit(false); try (Statement stmt = con.createStatement()) { stmt.addBatch("INSERT INTO COFFEES " + "VALUES('Amaretto', 49, 9.99, 0, 0)"); stmt.addBatch("INSERT INTO COFFEES " + "VALUES('Hazelnut', 49, 9.99, 0, 0)"); stmt.addBatch("INSERT INTO COFFEES " + "VALUES('Amaretto_decaf', 49, 10.99, 0, 0)"); stmt.addBatch("INSERT INTO COFFEES " + "VALUES('Hazelnut_decaf', 49, 10.99, 0, 0)"); int[] updateCounts = stmt.executeBatch(); con.commit(); } catch (BatchUpdateException b) { JDBCTutorialUtilities.printBatchUpdateException(b); } catch (SQLException ex) { JDBCTutorialUtilities.printSQLException(ex); } finally { con.setAutoCommit(true); } }
以下代码行禁用了Connection
对象con的自动提交模式,这样当调用executeBatch
方法时,事务将不会自动提交或回滚。
con.setAutoCommit(false);
为了正确处理错误,您应该在开始批量更新之前始终禁用自动提交模式。
方法Statement.addBatch
将一个命令添加到与Statement
对象stmt
关联的命令列表中。在这个示例中,这些命令都是INSERT INTO
语句,每个语句都添加了一个由五个列值组成的行。列COF_NAME
和PRICE
的值分别为咖啡的名称和价格。每行的第二个值是49,因为这是供应商Superior Coffee的标识号。最后两个值,即列SALES
和TOTAL
的条目,初始值都为零,因为目前还没有销售。(SALES
是本行咖啡在当前周销售的磅数;TOTAL
是本咖啡所有累积销售额的总和。)
以下代码行将已添加到其命令列表的四个SQL命令发送到数据库以作为批处理执行:
int[] updateCounts = stmt.executeBatch();
请注意,stmt
使用executeBatch
方法发送批量插入,而不是executeUpdate
方法,后者只发送一个命令并返回单个更新计数。数据库管理系统按照添加到命令列表的顺序执行命令,因此它将首先添加Amaretto的值行,然后添加Hazelnut的行,然后是Amaretto decaf,最后是Hazelnut decaf。如果所有四个命令都成功执行,数据库管理系统将按照执行顺序为每个命令返回一个更新计数。表示每个命令受影响的行数的更新计数存储在数组updateCounts
中。
如果批处理中的所有四个命令都成功执行,updateCounts
将包含四个值,所有这些值都为1,因为插入影响一行。与stmt
关联的命令列表现在为空,因为之前添加的四个命令在stmt
调用executeBatch
方法时已发送到数据库。您可以随时使用clearBatch
方法显式地清空这个命令列表。
Connection.commit
方法将对COFFEES
表的更新批量操作变为永久性。之前禁用了此连接的自动提交模式,因此需要显式调用此方法。
以下代码行为当前Connection
对象启用自动提交模式。
con.setAutoCommit(true);
现在,示例中的每个语句在执行后都会自动提交,并且不再需要调用commit
方法。
还可以执行参数化的批量更新,如下代码片段所示,其中con
是一个Connection
对象:
con.setAutoCommit(false); PreparedStatement pstmt = con.prepareStatement( "INSERT INTO COFFEES VALUES( " + "?, ?, ?, ?, ?)"); pstmt.setString(1, "Amaretto"); pstmt.setInt(2, 49); pstmt.setFloat(3, 9.99); pstmt.setInt(4, 0); pstmt.setInt(5, 0); pstmt.addBatch(); pstmt.setString(1, "Hazelnut"); pstmt.setInt(2, 49); pstmt.setFloat(3, 9.99); pstmt.setInt(4, 0); pstmt.setInt(5, 0); pstmt.addBatch(); // ... 根据每种新咖啡进行类似的操作 int[] updateCounts = pstmt.executeBatch(); con.commit(); con.setAutoCommit(true);
如果您在调用executeBatch
方法时,批处理中的某个SQL语句(通常是查询)产生了结果集,或者批处理中的某个SQL语句由于其他原因未能成功执行,则会抛出BatchUpdateException
异常。
您不应将查询(SELECT
语句)添加到SQL命令的批处理中,因为executeBatch
方法会返回一个更新计数的数组,该数组期望每个成功执行的SQL语句都返回一个更新计数。这意味着只有返回更新计数的命令(如INSERT INTO
、UPDATE
、DELETE
)或返回0的命令(如CREATE TABLE
、DROP TABLE
、ALTER TABLE
)可以成功地与executeBatch
方法一起批量执行。
BatchUpdateException
包含一个与executeBatch
方法返回的数组类似的更新计数数组。在两种情况下,更新计数与产生它们的命令的顺序相同。这告诉您有多少个命令在批处理中成功执行,以及它们是哪些命令。例如,如果有五个命令成功执行,数组将包含五个数字:第一个数字是第一个命令的更新计数,第二个数字是第二个命令的更新计数,依此类推。
BatchUpdateException
派生自SQLException
。这意味着您可以使用所有适用于SQLException
对象的方法。下面的方法JDBCTutorialUtilities.printBatchUpdateException
打印了所有SQLException
信息以及BatchUpdateException
对象中包含的更新计数。由于BatchUpdateException.getUpdateCounts
返回一个int
数组,代码使用for
循环打印每个更新计数:
public static void printBatchUpdateException(BatchUpdateException b) { System.err.println("----BatchUpdateException----"); System.err.println("SQLState: " + b.getSQLState()); System.err.println("Message: " + b.getMessage()); System.err.println("Vendor: " + b.getErrorCode()); System.err.print("Update counts: "); int[] updateCounts = b.getUpdateCounts(); for (int i = 0; i < updateCounts.length; i++) { System.err.print(updateCounts[i] + " "); } }
注意:并非所有的 JDBC 驱动程序都支持使用 ResultSet 接口插入新行。如果您尝试插入新行,而您的 JDBC 驱动程序数据库不支持此功能,则会抛出 SQLFeatureNotSupportedException 异常。
下面的方法 CoffeesTable.insertRow
通过 ResultSet 对象将一行插入到 COFFEES 表中:
public void insertRow(String coffeeName, int supplierID, float price, int sales, int total) throws SQLException { try (Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE)) { ResultSet uprs = stmt.executeQuery("SELECT * FROM COFFEES"); uprs.moveToInsertRow(); uprs.updateString("COF_NAME", coffeeName); uprs.updateInt("SUP_ID", supplierID); uprs.updateFloat("PRICE", price); uprs.updateInt("SALES", sales); uprs.updateInt("TOTAL", total); uprs.insertRow(); uprs.beforeFirst(); } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); } }
此示例调用 Connection.createStatement
方法,并传入两个参数,ResultSet.TYPE_SCROLL_SENSITIVE
和 ResultSet.CONCUR_UPDATABLE
。第一个值使得 ResultSet 对象的游标可以前后移动。第二个值 ResultSet.CONCUR_UPDATABLE
是插入行到 ResultSet 对象所需的;它指定结果集可以被更新。
使用字符串的 getter 方法的限制同样适用于 updater 方法。
方法 ResultSet.moveToInsertRow
将游标移动到插入行。插入行是与可更新结果集关联的特殊行。它本质上是一个缓冲区,在将行插入结果集之前,可以通过调用更新方法构造新行。例如,该方法调用方法 ResultSet.updateString
来将插入行的 COF_NAME
列更新为 Kona
。
ResultSet.insertRow
方法将插入行的内容插入到 ResultSet
对象和数据库中。
注意:使用 ResultSet.insertRow
插入行后,应将游标移动到插入行以外的行。例如,此示例使用 ResultSet.beforeFirst
方法将游标移动到结果集中的第一行之前。如果应用程序的其他部分使用相同的结果集并且游标仍指向插入行,则可能会导致意外的结果。