Java 数据库连接(JDBC)
Java 数据库连接(JDBC)是一个标准的 API,它使用 Java 编程语言与数据库进行交互。诸如 JDBC 的调用级接口是编程接口,它们允许从外部访问 SQL 命令来处理和更新数据库中的数据。通过提供与数据库连接的库例程,它们允许将 SQL 调用集成到通用的编程环境中。特别是,JDBC 有一个使接口变得极其简单和直观的例程的丰富收集。
在下面几个小节中,我们将查看通过 JDBC 与数据库连接所涉及的一些步骤。我们将特别关注与 Java 对象序列化相比,JDBC 是如何成为一个企业数据管理策略的。
建立一个数据库连接
在利用 JDBC 做任何其他事情之前,需要从驱动程序供应商那里获取数据库驱动程序,并且将该库添加到类路径中。一旦完这项工作,就可以在 Java 程序中使用类似于下面所示的代码来实现实际的连接。
2 Java.sql.Connection conn = DriverManager.getConnection(<connection URL>);
3
Java 对象序列化并不需要这个该步骤,因为使用序列化来执行持久性操作并不需要 DBMS。 序列化是一个基于文件的机制;因此,在序列化一个对象之前,需要在目标文件系统中打开一个 I/O 流。
创建 JDBC Statement 和 PreparedStatement
可以用 JDBC Statement 对象将 SQL 语句发送到数据库管理系统(DBMS),并且不应该将该对象与 SQL 语句混淆。 JDBC Statement 对象是与打开连接有关联,而不是与任何单独的 SQL 语句有关联。可以将 JDBC Statement 对象看作是位于连接上的一个通道,将一个或多个(您请求执行的)SQL 语句传送给 DBMS。
为了创建 Statement 对象,您需要一个活动的连接。通过使用我们前面所创建的 Connection 对象 con ——下面的代码来完成这项工作。
到目前为止,我们已经有了一个 Statement 对象,但是还没有将对象传递到 DBMS 的 SQL 语句。
当数据库接收到语句时,数据库引擎首先会分析该语句并查找句法错误。一旦完成对语句的分析,数据库就必须计算出执行它的最有效方法。在计算上,这可能非常昂贵。数据库会检查哪些索引可以提供帮助,如果存在这样的索引的话,或者检查是否应该完全读取表中的所有行。数据库针对数据进行统计,找出非常好的的执行方式。一旦创建好 查询计划,数据库引擎就可以执行它。
生成这样一个计划会占用 CPU 资源。理想情况是,如果我们两次发送相同的语句到数据库,那么我们希望数据库重用第一个语句的访问计划,我们可以使用 PreparedStatement 对象来获得这种效果。
这里有一个主要的特性是,将 PreparedStatement 与其超类 Statement 区别开来:与 Statement 不同,在创建 PreparedStatement 时,会提供一个 SQL 语句。然后了立即将它发送给 DBMS,在那里编译该语句。因而, PreparedStatement 实际上是作为一 个通道与连接和被编译的 SQL 语句相关联的。
那么,它的优势是什么呢?如果需要多次使用相同的查询或者不同参数的类似查询,那么利用 PreparedStatement ,语句,只需被 DBMS 编译和优化一次即可。与使用正常的 Statement 相比,每次使用相同的 SQL 语句都需要重新编译一次。
还可以通过 Connection 方法创建 PreparedStatement 。下面代码显示了如何创建一个带有三个输入参数的参数化了的 SQL 语句。
2 "UPDATE Sells SET price = ? WHERE bar = ? AND beer = ?");
3
注意,Java 序列化不支持类似于 SQL 的查询语言。使用 Java 序列化访问对象属性的惟一途径就是反序列化该对象,并调用该对象上的 getter/accessor 方法。反序列化一个完整的对象在计算上可能很昂贵,尤其是在程序的生命期中,应用程序需要重复执行它。
在执行 PreparedStatement 之前,需要向参数提供值。通过调用 PreparedStatement 中定义的 setXXX() 方法可以实现它。最常使用的方法是 setInt() , setFloat() , setDouble() ,以及 setString() 。每次执行已准备的声明之前,都需要设置这些值。
执行语句和查询
执行 JDBC 中的 SQL 语句的方式是根据 SQL 语句的目的而变化的。DDL(数据定义语言)语句(例如表建立和表更改语句)和更新表内容的语句都是通过使用 executeUpdate() 执行的。列表 2 中包含 executeUpdate() 语句的实例。
列表 2. 实际运行中的 executeUpdate()
2 stmt.executeUpdate("CREATE TABLE Sells " +
3 "(bar VARCHAR2(40), beer VARCHAR2(40), price REAL)" );
4 stmt.executeUpdate("INSERT INTO Sells " +
5 "VALUES ('Bar Of Foo', 'BudLite', 2.00)" );
6 String sqlString = "CREATE TABLE Bars " +
7 "(name VARCHAR2(40), address VARCHAR2(80), license INT)" ;
8 stmt.executeUpdate(sqlString);
9
我们将通过先前插入的参数值(如上所示)执行 PreparedStatement ,然后在这之上调用 executeUpdate() ,如下所示:
相比之下, 查询期望返回一个行作为它的结果,并且并不改变数据库的状态。这里有一个称为 executeQuery() 的相对应的方法,它的返回值是 ResultSet 对象,如列表 3 所示。
列表 3. 执行一个查询
2 float price ;
3 ResultSet rs = stmt.executeQuery("SELECT * FROM Sells");
4 while ( rs.next() ) {
5 bar = rs.getString("bar");
6 beer = rs.getString("beer");
7 price = rs.getFloat("price");
8 System.out.println(bar + " sells " + beer + " for " + price + " Dollars.");
9 }
10
由于查询而产生的行集包含在变量 rs 中,该变量是 ResultSet 的一个实例。集合对于我们来说并没有太大用处,除非我们可以访问每一个行以及每一个行中的属性。 ResultSet 提供了一个光标,可以用它依次访问每一个行。光标最初被设置在正好位于第一行之前的位置。每个方法调用都会导致光标向下一行移动,如果该行存在,则返回 true ,或者如果没有剩余的行,则返回 false 。
我们可以使用适当类型的 getXXX() 来检索某一个行的属性。在前面的实例中,我们使用 getString() 和 getFloat() 方法来访问列值。注意,我们提供了其值被期望用作方法的参数的列的名称;我们可以指定用列号来代替列名。检索到的第一列的列号为 1,第二列为 2,依次类推。
在使用 PreparedStatement 时,可以通过先前插入的参数值来执行查询,然后对它调用 executeQuery() ,如下所示:
关于访问 ResultSet 的注释
JDBC 还提供一系列发现您在结果集中的位置的方法: getRow() , isFirst() , isBeforeFirst() , isLast() ,以及 isAfterLast() 。
这里还有一些使可滚动光标能够自由访问结果集中的任意行的方法。在默认情况下,光标只向前滚动,并且是只读的。在为 Connection 创建 Statement 时,可以将 ResultSet 的类型更改为更为灵活的可滚动或可更新模型,如下所示:
2 ResultSet rs = stmt.executeQuery("SELECT * FROM Sells");
3
不同的类型选项: TYPE_FORWARD_ONLY 、 TYPE_SCROLL_INSENSITIVE 和 TYPE_SCROLL_SENSITIVE 。可以通过使用 CONCUR_READ_ONLY 和 CONCUR_UPDATABLE 选项来选择光标是只读的还是可更新的。对于默认光标,可以使用 rs.next() 向前滚动它。对于可滚动的光标,您有更多的选项,如下所示:
2 rs.previous(); // moves back one row in the retrieved result set
3 rs.relative(2); // moves forward two rows in the retrieved result set
4 rs.relative(-3); // moves back three rows in the retrieved result set
对于可滚动光标的工作方式,这里有更多的详细描述。尽管可滚动光标对于特定应用程序是有用的,但是它导致极大的性能损失,所以应该限制和谨慎使用。
在序列化中不存在与 JDBC 的 ResultSet 相对应的机制。序列化和 JDBC 观察底层的数据的角度不同。JDBC (通常)假定底层数据是关系型结构的;而序列化假定底层数据是一个对象图。两种技术的底层数据结构存在显著差异。JDBC 的 Set 结构并不能自然地映射到序列化的对象图结构,反之亦然。当通过使用序列化语义将一个 Java 对象持久化时,数据的底层结构变成了一个字节流,该字节流展示了已经序列化了的核心对象的各种内部对象之间的关联。
JDBC 中的 ResultSet 导航是从一个 Set 元素移动到其他元素的过程,而在对象序列化中,这是不可能的,因为序列化涉及到对象关联,而不是将一组行封装到一个实体集合中。因此,Java 对象序列化无法向您提供用这种方式访问数据单独某个部分的能力。
事务
JDBC 允许将 SQL 语句组合到单独一个事务中。因此,我们可以通过使用 JDBC 事务特性来确保 ACID 属性。
Connection 对象执行事务控制。当建立连接时,在默认情况下,连接是自动提交模式下。这意味着每个 SQL 语句自身都被看作是一个事务,并且一完成执行就会被提交。
可以用以下方法开启或关闭自动提交模式:
2 con.setAutoCommit(true) ;
3
一旦关闭了自动提交,除非通过调用 commit() 显式地告诉它提交语句,否则无法提交 SQL 语句(即,数据库将不会被持久地更新)。在提交之前的任何时间,我们都可以调用 rollback() 回滚事务,并恢复最近的提交值(在尝试更新之前)。
我们还可以设置期望的事务隔离等级。例如,我们可以将设置事务隔离等级为 TRANSACTION_READ_COMMITTED ,这使得在提交值之前,不允许对它进行访问。并且禁止脏读。在 Connection 接口中为隔离等级提供了五个这样的值。默认情况下,隔离等级是可序列化的。JDBC 允许我们发现数据库所设置的是什么事务隔离等级(使用 Connection 的 getTransactionIsolation() 方法)以及设置适当的等级(使用 Connection 的 setTransactionIsolation() 方法)。
回滚通常与 Java 语言的异常处理能力结合在一起使用。这种结合为处理数据完整性提供一个简单高效的机制。在下一节中,我们将研究如何使用 JDBC 进行错误处理。
注意,Java 对象序列化并不直接支持事务管理。如果您正在使用序列化,则将需要借助其他的 API,例如 JTA,来获得这个效果。然而,为了获得事务隔离的效果,可以选择在执行一个更新操作时同步该序列化对象,如下所示:
2 //Perform the updates etc...
3 }
4
利用异常处理错误
软件程序中总是出现一些错误。通常,数据库程序是关键性应用程序,而且适当地捕获和处理错误是有必要的。程序应该恢复并且让数据库处于某种一致的状态下。将回滚与 Java 异常处理程序结合使用是达到这种要求的一种简便方法。
访问服务器(数据库)的客户(程序)需要能够识别从服务器返回的所有错误。JDBC 通过提供两种等级的错误条件来访问这种信息: SQLException 和 SQLWarning 。 SQLException 是 Java 异常,它(如果未被处理)将会终止该应用程序。 SQLWarning 是 SQLException 的子类,但是它们代表的是非致命错误或意想不到的条件,因此,可以忽略它们。
在 Java 代码中,希望抛出异常或者警告的语句包含于 try 块中。如果在 try 块中的语句抛出异常或者警告,那么可以在对应的某个 catch 语句中捕获它。每个捕获语句都指出了它准备捕获的异常。
换句话说,如果数据类型是正确的,但是数据库大小超出其空间限制并且不能建立一个新表,则可能会抛出一个异常。 可以从 Connection , Statement ,以及 ResultSet 对象中获取 SQLWarning 。每个对象都只是存储最近 SQLWarning 。因此,如果通过 Statement 对象执行其他语句,则将放弃所有早期的警告。列表 4 举例说明了 SQLWarning 的使用。
列表 4. 实际运行中的 SQLWarnings
2 SQLWarning warn = stmt.getWarnings() ;
3 if (warn != null)
4 System.out.println("Message: " + warn.getMessage()) ;
5 SQLWarning warning = rs.getWarnings() ;
6 if (warning != null)
7 warning = warning.getNextWarning() ;
8 if (warning != null)
9 System.out.println("Message: " + warn.getMessage()) ;
10
实际上, SQLWarning 在某种程度上比 SQLException 更为罕见。最常见的是 DataTruncation 警告,它表示在从数据库读或写数据时存在问题。
Java 并没有提供序列化所使用的特定的异常类。使用序列化时发生的大多数异常都与执行的 I/O 操作有关,因此,在这些情况中 I/O 异常类将满足要求。
批处理
JDBC 2.0 提供一个用于批处理的强大API。批处理允许积累一组 SQL 语句,并且将它们一起发送并处理。一个典型的批处理就是银行应用程序,该应用程序每隔一刻钟就要更新许多账号。在减少从 Java 代码到数据库的往返次数方面,批处理是一个强大功能。
Statement 接口提供 addBatch(String) 方法,将 SQL 语句添加到一个批处理中。一旦已经将所有的 SQL 语句都增加到该批处理中,就可以使用 executeBatch() 方法一起执行它们。
然后,用 executeBatch() 方法执行 SQL 语句,并返回 int 值的一个数组。该数组包含受每条语句影响的行数。将 SELECT 语句或者其他返回 ResultSet 的 SQL 语句放在一个批处理中会导致 SQLException 。
列表 5 中列出了利用 java.sql.Statement 进行批处理的一个简单实例。
列表 5. 实际运行中的批处理
2 stmt.insert("DELETE FROM Users");
3 stmt.insert("INSERT INTO Users VALUES('rod', 37, 'circle')");
4 stmt.insert("INSERT INTO Users VALUES('jane', 33, 'triangle')");
5 stmt.insert("INSERT INTO Users VALUES('freddy', 29, 'square')");
6 int[] counts = stmt.executeBatch();
7
在您不知道特定语句将运行的次数时,批处理是一个处理 SQL 代码的好方法。例如,如果在不使用批处理的情况下试图插入 100 条记录,那么性能可能会受到影响。如果编写一个脚本,增加 10000 条记录,那么情况会变得更糟。添加批处理可以帮助提高性能,后者甚至能够提高代码的可读性。
Java 对象序列化并不支持批处理。通常,会在某个对象的范围(联系图)上运用序列化,在这种情况下,批处理没有意义。因此,批处理在数据更新的定时和分组方面为您提供一定的灵活性,而这些对于序列化来说不一定是可用的。