2.JDBC
8541字约28分钟
2024-10-24
数据库应用程序通过调用 API 方法与数据库引擎交互。Java 应用程序使用的 API 称为 JDBC(Java 数据库连接)。JDBC 库由五个 Java 包组成,其中大部分实现了只有在大型商业应用程序中才有用的高级功能。本章主要介绍 java.sql 包中的 JDBC 核心功能。该核心功能可分为两部分:基本 JDBC(包含基本使用所需的类和方法)和高级 JDBC(包含可选功能,可提供更多的便利性和灵活性)。
2.1 基本 JDBC
JDBC 的基本功能体现在五个接口中:Driver, Connection, Statement, ResultSet 和 ResultSetMetadata。此外,这些接口中只有极少数方法是必不可少的。图 2.1 列出了这些方法。
Driver
public Connection connect(String url, Properties prop)
throws SQLException;Connection
public Statement createStatement() throws SQLException;
public void close() throws SQLException;Statement
public ResultSet executeQuery(String qry) throws SQLException;
public int executeUpdate(String cmd) throws SQLException;
public void close() throws SQLException;ResultSet
public boolean next() throws SQLException; public int getInt() throws SQLException; public String getString() throws SQLException; public void close() throws SQLException; public ResultSetMetaData getMetaData() throws SQLException;ResultSetMetaData
public int getColumnCount() throws SQLException;
public String getColumnName(int column) throws SQLException;
public int getColumnType(int column) throws SQLException;
public int getColumnDisplaySize(int column) throws SQLException;本节的示例程序将说明这些方法的使用。第一个示例程序是 CreateTestDB,它说明了程序如何连接 Derby 引擎以及如何断开与 Derby 引擎的连接。 其代码如图 2.2 所示,与 JDBC 相关的代码以粗体标出。下面各小节将详细介绍这段代码。
2.1.1 连接到数据库引擎
JDBC 驱动类(JDBC driver classes)实现了 Driver 接口(interface Driver)。Derby 和 SimpleDB 各自都有两个驱动类:一个用于基于服务器的连接(server-based connections),另一个用于嵌入式连接(embedded connections)。
对于 Derby 引擎,基于服务器的连接使用类 ClientDriver,而嵌入式连接使用 EmbeddedDriver;这两个类都位于包 org.apache.derby.jdbc 中。 对于 SimpleDB 引擎,基于服务器的连接使用类 NetworkDriver(位于包 simpledb.jdbc.network),而嵌入式连接使用类 EmbeddedDriver(位于包 simpledb.jdbc.embedded)。
客户端(client)可以通过调用 Driver 对象的 connect 方法与数据库引擎建立连接。例如,图 2.2 中的以下三行代码演示了如何与 Derby 数据库建立基于服务器的连接:
String url = "jdbc:derby://localhost/testdb;create=true";
Driver d = new ClientDriver();
Connection conn = d.connect(url, null);connect 方法接受两个参数。第一个参数是一个 URL,用于标识驱动程序(driver)、服务器(server,用于基于服务器的连接)以及数据库(database)。这个 URL 被称为连接字符串(connection string),其语法与第 1 章介绍的 ij(或 SimpleIJ)基于服务器的连接字符串相同。图 2.2 中的连接字符串由以下四部分组成:
jdbc:derby::描述客户端使用的协议(protocol)。在这里,协议表明该客户端是一个使用 JDBC 的 Derby 客户端。//localhost:描述服务器所在的机器位置。你可以用任意域名(domain name)或IP 地址替换localhost。/testdb:描述服务器上数据库的路径。对于 Derby 服务器,这个路径从启动服务器的用户的当前目录开始,而路径的最后部分(在这里是testdb)表示存储该数据库所有数据文件的目录。剩余部分:表示发送给数据库引擎的属性值(property values)。在这里,字符串
;create=true告诉引擎创建一个新数据库。通常,可以向 Derby 引擎发送多个属性值。例如,如果引擎需要用户身份验证(user authentication),还需要指定用户名和密码的属性。用户 “einstein” 的连接字符串可能如下所示:
"jdbc:derby://localhost/testdb;create=true;user=einstein; password=emc2"
connect 方法的第二个参数是一个 Properties 类型的对象。这个对象提供了另一种方式将属性值(property values)传递给数据库引擎。在图 2.2 中,该参数的值为 null,因为所有属性都在连接字符串中指定。
另外,你可以将属性规范放入第二个参数中,如下所示:
String url = "jdbc:derby://localhost/testdb";
Properties prop = new Properties();
prop.put("create", "true");
prop.put("username", "einstein");
prop.put("password", "emc2");
Driver d = new ClientDriver();
Connection conn = d.connect(url, prop);每个数据库引擎都有其独特的连接字符串语法(connection string syntax)。 SimpleDB 的基于服务器的连接字符串(server-based connection string)与 Derby 不同,因为它仅包含协议(protocol)和机器名(machine name)。 (在 SimpleDB 中,将数据库名称包含在字符串中没有意义,因为数据库是在启动 SimpleDB 服务器时指定的。同时,连接字符串也不包含属性(properties),因为 SimpleDB 服务器不支持任何属性。)
例如,以下三行代码用于与 SimpleDB 服务器建立连接:
String url = "jdbc:simpledb://localhost";
Driver d = new NetworkDriver();
conn = d.connect(url, null);尽管驱动类(driver class)和连接字符串语法(connection string syntax)是特定于供应商的(vendor-specific),但 JDBC 程序的其他部分则完全与供应商无关(vendor-neutral)。例如,考虑图 2.2 中的变量 d 和 conn。它们对应的 JDBC 类型 Driver 和 Connection 都是接口(interfaces)。从代码中可以看出,变量 d 被分配给一个 ClientDriver 对象。然而,conn 被分配给方法 connect 返回的 Connection 对象,而没有办法知道它的实际类。这种情况在所有 JDBC 程序中都是如此。除了驱动类的名称和其连接字符串外,JDBC 程序仅了解并关注与供应商无关的 JDBC 接口。因此,一个基本的 JDBC 客户端将从两个包中导入:
- 内置包
java.sql:用于获取与供应商无关的 JDBC 接口定义 - 供应商提供的包,其中包含驱动类
2.1.2 从数据库引擎断开连接
当客户端与数据库引擎连接时,引擎可能会为客户端分配资源供其使用。例如,客户端可能会从服务器请求锁,以防止其他客户端访问数据库的某些部分。即使连接到引擎的能力也可以视为一种资源。一家公司可能与商业数据库系统签订了站点许可协议,限制同时连接的数量,这意味着占用一个连接可能会使其他客户端无法连接。
由于连接占用了宝贵的资源,客户端应在不再需要数据库时尽快断开与引擎的连接。2.1 基本 JDBC 程序的客户端通过调用其 Connection 对象的 close 方法与引擎断开连接。该 close 方法的调用可在图 2.2 中看到。
2.1.3 SQL 异常(SQL Exceptions)
客户端与数据库引擎的交互可能会因多种原因引发异常。示例如下:
- 客户端请求引擎执行一个格式错误的 SQL 语句,或者一个访问不存在表的 SQL 查询,或者比较两个不兼容的值。
- 因为与并发客户端之间发生死锁,引擎中止了客户端的操作。
- 引擎代码中存在错误(bug)。
- 客户端无法访问引擎(针对基于服务器的连接)。可能是主机名错误,或者主机不可达。
不同的数据库引擎有各自处理这些异常的内部方式。例如,SimpleDB 在网络问题上抛出 RemoteException,在 SQL 语句问题上抛出 BadSyntaxException,在死锁情况下抛出 BufferAbortException 或 LockAbortException,在服务器问题上抛出通用的 RuntimeException。
为了使异常处理与供应商无关,JDBC 提供了自己的异常类,称为 SQLException。当数据库引擎遇到内部异常时,它会将其包装在 SQLException 中并发送给客户端程序。
与 SQLException 相关联的消息字符串标识引发该异常的内部异常。每个数据库引擎可以自由提供自己的消息。例如,Derby 有近 900 条错误消息,而 SimpleDB 将所有可能的问题归为六种消息:
- “network problem”(网络问题)
- “illegal SQL statement”(非法 SQL 语句)
- “server error”(服务器错误)
- “operation not supported”(不支持的操作)
- 两种形式的“transaction abort”(事务中止)。
大多数 JDBC 方法(以及图 2.1 中的所有方法)都会抛出 SQLException。SQL 异常是检查异常(checked exceptions),这意味着客户端必须明确处理它们,要么捕获它们,要么将它们向上抛出。图 2.2 中的两个 JDBC 方法在 try 块内执行;如果其中任何一个抛出异常,代码将打印堆栈跟踪并返回。
需要注意的是,图 2.2 中的代码存在一个问题,即当抛出异常时连接不会被关闭。这是一个资源泄漏(resource leak)的例子——在客户端退出后,引擎无法轻易回收连接的资源。解决此问题的一种方法是在 catch 块内关闭连接。然而,close 方法需要在 try 块内调用,这意味着图 2.2 的 catch 块应该如下所示:
catch(SQLException e) {
e.printStackTrace();
try {
conn.close();
} catch (SQLException ex) {}
}这开始看起来有些糟糕。此外,如果 close 方法抛出异常,客户端应该怎么做?上述代码忽略了这个异常,但这似乎并不正确。
更好的解决方案是通过 Java 的 try-with-resources 语法自动关闭连接。要使用它,您需要在 try 关键字后面的括号中创建 Connection 对象。当 try 块结束时(无论是正常结束还是因异常结束),Java 会隐式调用该对象的 close 方法。改进后的图 2.2 的 try 块如下所示:
try (Connection conn = d.connect(url, null)) {
System.out.println("Database Created");
} catch (SQLException e) {
e.printStackTrace();
}这段代码正确地处理了所有异常,同时保持了图 2.2 的简洁性。
2.1.4 执行 SQL 语句
可以将连接(connection)视为与数据库引擎的“会话”,在该会话期间,引擎为客户端执行 SQL 语句。JDBC 支持这一概念如下:
Connection 对象拥有一个名为 createStatement 的方法,该方法返回一个 Statement 对象。Statement 对象有两种执行 SQL 语句的方式:executeQuery 和 executeUpdate 方法。它还有一个 close 方法,用于释放对象所占用的资源。
图 2.3 展示了一个客户端程序,该程序调用 executeUpdate 来修改 Amy 的 STUDENT 记录中的 MajorId 值。该方法的参数是一个字符串,表示 SQL 更新语句;该方法返回被更新的记录数。
像 Connection 对象一样,Statement 对象也需要被关闭。最简单的解决方案是在 try 块中自动关闭这两个对象。
public class ChangeMajor {
public static void main(String[] args) {
String url = "jdbc:derby://localhost/studentdb";
String cmd = "update STUDENT set MajorId=30 where SName='amy'";
Driver d = new ClientDriver();
try (Connection conn = d.connect(url, null);
Statement stmt = conn.createStatement()) {
int howmany = stmt.executeUpdate(cmd);
System.out.println(howmany + " records changed.");
} catch (SQLException e) {
e.printStackTrace();
}
}
}关于 SQL 命令的规范说明了一个有趣的点。由于命令被存储为 Java 字符串,因此它被双引号包围。另一方面,SQL 中的字符串使用单引号。这种区别使事情变得简单,因为您不必担心引号字符具有两种不同的含义——SQL 字符串使用单引号,而 Java 字符串使用双引号。
ChangeMajor 代码假设存在一个名为“studentdb”的数据库。SimpleDB 发行版包含一个名为 CreateStudentDB 的类,它创建数据库并用图 1.1 中的表格填充数据。它应该是使用大学数据库时首先调用的程序。其代码出现在图 2.4 中。该代码执行 SQL 语句来创建五张表并向其中插入记录。为了简洁,这里只展示了 STUDENT 表的代码。
2.1.5 结果集
语句的 executeQuery 方法执行一个 SQL 查询。其方法的参数是一个表示 SQL 查询的字符串,并返回一个类型为 ResultSet 的对象。ResultSet 对象代表查询的输出记录。客户端可以搜索结果集以检查这些记录。
作为展示如何使用结果集的示例程序,请考虑图 2.5 中展示的 StudentMajor 类。其调用 executeQuery 返回一个包含每个学生姓名和专业的结果集。随后的 while 循环打印结果集中的每条记录。
一旦客户端获得一个结果集,它通过调用 next 方法来迭代输出记录。此方法移动到下一记录,如果移动成功则返回 true,如果没有更多记录则返回 false。通常,客户端使用循环来遍历所有记录,逐一处理每个记录。
新的 ResultSet 对象总是定位在第一条记录之前,因此在查看第一条记录之前需要调用 next。由于这个要求,遍历记录的典型方式如下:
public class CreateStudentDB {
public static void main(String[] args) {
String url = "jdbc:derby://localhost/studentdb;create=true";
Driver d = new ClientDriver();
try (Connection conn = d.connect(url, null); Statement stmt = conn.createStatement()) {
String s = "create table STUDENT(SId int, SName varchar(10), MajorId int, GradYear int)";
stmt.executeUpdate(s);
System.out.println("Table STUDENT created.");
s = "insert into STUDENT(SId, SName, MajorId, GradYear) values ";
String[] studvals = {"(1, 'joe', 10, 2021)",
"(2, 'amy', 20, 2020)",
"(3, 'max', 10, 2022)",
"(4, 'sue', 20, 2022)",
"(5, 'bob', 30, 2020)",
"(6, 'kim', 20, 2020)",
"(7, 'art', 30, 2021)",
"(8, 'pat', 20, 2019)",
"(9, 'lee', 10, 2021)"};
for (int i = 0; i < studvals.length; i++)
stmt.executeUpdate(s + studvals[i]);
System.out.println("STUDENT records inserted.");
...
} catch (SQLException e) {
e.printStackTrace();
}
}
}Fig. 2.4 JDBC code for the CreateStudentDB client
String qry = "select ...";
ResultSet rs = stmt.executeQuery(qry);
while (rs.next()) {
... // process the record
}图 2.5 中展示了这样的循环示例。在这个循环的第 n 次通过时,变量 rs 将被定位在结果集的第 n 条记录上。当没有更多记录需要处理时,循环将结束。
在处理记录时,客户端使用 getInt 和 getString 方法来检索其字段的值。每个方法都以字段名作为参数,并返回该字段的值。在图 2.5 中,代码检索并打印每条记录的 SName 和 DName 字段值。
public class StudentMajor {
public static void main(String[] args) {
String url = "jdbc:derby://localhost/studentdb";
String qry = "SELECT SName, DName FROM DEPT, STUDENT " +
"WHERE MajorId = DId";
Driver d = new ClientDriver();
try (
Connection conn = d.connect(url, null);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(qry)
) {
System.out.println("Name\tMajor");
while (rs.next()) {
String sname = rs.getString("SName");
String dname = rs.getString("DName");
System.out.println(sname + "\t" + dname);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}图 2.5 学生专业客户端的 JDBC 代码
结果集会占用引擎上的宝贵资源。close方法释放这些资源,并使其可供其他客户端使用。因此,客户端应当努力成为一个“守规矩的好公民”,尽快关闭结果集。一个选项是在上述 while 循环结束时明确调用 close。另一个选项,如图 2.5 所示,是使用 Java 的自动关闭机制 (Java autoclose mechanism)。
2.1.6 使用查询元数据 (Query Metadata)
结果集的架构(schema)被定义为每个字段的名称、类型和显示大小。这些信息通过 ResultSetMetaData 接口提供。
当客户端执行查询时,它通常知道输出表的架构。例如,硬编码在学生专业客户端中的知识是其结果集包含两个字符串字段 SName 和 DName。
然而,假设一个客户端程序允许用户提交查询作为输入。该程序可以调用查询结果集上的 getMetaData 方法,它会返回一个 ResultSetMetaData 类型的对象。然后,它可以调用这个对象的方法来确定输出表的架构。例如,图 2.6 中的代码使用 ResultSetMetaData 来打印一个参数结果集的架构。
void printSchema(ResultSet rs) throws SQLException {
ResultSetMetaData md = rs.getMetaData();
for (int i = 1; i <= md.getColumnCount(); i++) {
String name = md.getColumnName(i);
int size = md.getColumnDisplaySize(i);
int typecode = md.getColumnType(i);
String type;
if (typecode == Types.INTEGER) {
type = "int";
} else if (typecode == Types.VARCHAR) {
type = "string";
} else {
type = "other";
}
System.out.println(name + "\t" + type + "\t" + size);
}
}图 2.6 使用 ResultSetMetaData 打印结果集的架构
这段代码展示了 ResultSetMetaData 对象的典型用法。首先,它调用 getColumnCount 方法返回结果集中的字段数;然后,它调用 getColumnName、getColumnType 和 getColumnDisplaySize 方法来确定每个列字段的名称、类型和大小。注意,列号从 1 开始,而不是你可能期望的 0。
getColumnType 方法返回一个整数,编码了字段类型。这些代码在 JDBC 类 Types 中以常量形式定义。这个类包含了 30 种不同类型的代码,这应该能让你了解 SQL 语言的广泛性。这些类型的实际值并不重要,因为 JDBC 程序应该总是通过名称而不是值来引用这些代码。
一个需要元数据知识的客户端的典型例子是命令解释器。第 1 章中的 SimpleIJ 程序就是这样的一个程序;其代码出现在图 2.7 中。由于这是你第一个非平凡的 JDBC 客户端示例,你应该仔细检查其代码。
main 方法首先从用户那里读取连接字符串,并使用它来确定应该使用哪个驱动。代码在连接字符串中寻找“//”字符。如果这些字符出现,那么字符串必须指定一个基于服务器的连接,否则是一个嵌入式连接。然后,方法通过将连接字符串传递给适当驱动的 connect 方法来建立连接。
main 方法在其 while 循环的每次迭代中处理一行文本。如果文本是一个 SQL 语句,则根据需要调用 doQuery 或 doUpdate 方法。用户可以通过输入“exit”来退出循环,此时程序退出。
public class SimpleIJ {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Connect> ");
String s = sc.nextLine();
Driver d = (s.contains("//")) ? new NetworkDriver() : new EmbeddedDriver();
try (
Connection conn = d.connect(s, null);
Statement stmt = conn.createStatement()
) {
System.out.print("\nSQL> ");
while (sc.hasNextLine()) {
// Process one line of input
String cmd = sc.nextLine().trim();
if (cmd.equalsIgnoreCase("exit")) break;
else if (cmd.toLowerCase().startsWith("select")) doQuery(stmt, cmd);
else doUpdate(stmt, cmd);
System.out.print("\nSQL> ");
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
sc.close();
}
}
private static void doQuery(Statement stmt, String cmd) {
try (ResultSet rs = stmt.executeQuery(cmd)) {
ResultSetMetaData md = rs.getMetaData();
int numcols = md.getColumnCount();
int totalwidth = 0;
// Print header
for (int i = 1; i <= numcols; i++) {
String fldname = md.getColumnName(i);
int width = md.getColumnDisplaySize(i);
totalwidth += width;
String fmt = "%" + width + "s";
System.out.format(fmt, fldname);
}
System.out.println();
// Print separator
for (int i = 0; i < totalwidth; i++) {
System.out.print("-");
}
System.out.println();
// Print records
while (rs.next()) {
for (int i = 1; i <= numcols; i++) {
String fldname = md.getColumnName(i);
int fldtype = md.getColumnType(i);
String fmt = "%" + md.getColumnDisplaySize(i);
if (fldtype == Types.INTEGER) {
int ival = rs.getInt(fldname);
System.out.format(fmt + "d", ival);
} else {
String sval = rs.getString(fldname);
System.out.format(fmt + "s", sval);
}
}
System.out.println();
}
} catch (SQLException e) {
System.out.println("SQL Exception: " + e.getMessage());
}
}
private static void doUpdate(Statement stmt, String cmd) {
try {
int howmany = stmt.executeUpdate(cmd);
System.out.println(howmany + " records processed");
} catch (SQLException e) {
System.out.println("SQL Exception: " + e.getMessage());
}
}
}doQuery 方法执行查询并获取输出表的结果集和元数据。该方法的大部分内容都与确定值的适当间距有关。调用 getColumnDisplaySize 返回每个字段的空间需求;代码使用这些数字来构建一个格式字符串,使字段值能够正确对齐。这段代码的复杂性体现了“细节决定成败”的格言。也就是说,概念上困难的任务由于 ResultSet 和 ResultSetMetaData 方法的支持而容易编码,而将数据对齐这样一个琐碎的任务却占用了大部分编码工作。
doQuery 和 doUpdate 方法通过打印错误消息并返回来捕获异常。这种错误处理策略允许主循环继续接受语句,直到用户输入“exit”命令。
2.2 高级 JDBC
基本的 JDBC 使用起来相对简单,但它提供的与数据库引擎交互的方式相当有限。本节将考虑 JDBC 的某些额外功能,这些功能使客户端能够更灵活地控制访问数据库的方式。
2.2.1 隐藏驱动程序
在基本的 JDBC 中,客户端通过获取一个 Driver 对象的实例并调用其 connect 方法来连接到数据库引擎。这种策略的问题在于它将特定供应商的代码放入了客户端程序中。JDBC 包含了两个供应商中立的类,用于将驱动程序信息从客户端程序中分离出来:DriverManager 和 DataSource。我们将逐一考虑这些类。
使用 DriverManager
DriverManager 类保存了一组驱动程序。它包含了静态方法来将驱动程序添加到集合中,以及搜索集合以查找可以处理给定连接字符串的驱动程序。这两个方法在图 2.8 中有所展示。
理念是客户端反复调用 registerDriver 来注册它可能使用的每个数据库的驱动程序。当客户端想要连接到一个数据库时,它只需要调用 getConnection 方法并提供一个连接字符串。驱动程序管理器会尝试在其集合中的每个驱动程序上使用这个连接字符串,直到有一个返回非空连接。
例如,考虑图 2.9 的代码。前两行将基于服务器的 Derby 和 SimpleDB 驱动程序注册到驱动程序管理器中。最后两行建立到 Derby 服务器的连接。客户端在调用 getConnection 时不需要指定驱动程序;它只需要指定连接字符串。驱动程序管理器决定使用其已注册的哪个驱动程序。
static public void registerDriver(Driver driver) throws SQLException;
static public Connection getConnection(String url, Properties p) throws SQLException;图 2.8 DriverManager 类的两个方法
2.3 Java与SQL的计算
每当程序员编写JDBC客户端时,都必须做出一个重要的决定:计算的哪一部分应该由数据库引擎执行,哪一部分应该由Java客户端执行?本节将探讨这些问题。
再来看图2.5中的StudentMajor演示客户端。在该程序中,引擎执行所有计算,通过执行SQL查询来计算STUDENT和DEPT表的连接。客户端的唯一职责是检索查询输出并打印输出。
相反,您可以编写客户端,使其完成所有计算,如图2.22所示。在该代码中,引擎的唯一职责是为STUDENT和DEPT表创建结果集。客户端完成所有其他工作,计算连接并打印结果。
这两个版本哪个更好?显然,原始版本更优雅。它不仅代码更少,而且代码更易读。但效率如何呢?根据经验,在客户端执行的操作越少,效率越高。主要原因有两个:
• 从引擎到客户端传输的数据通常较少,如果它们位于不同的机器上,这一点尤为重要。
• 引擎包含关于每个表如何实现以及复杂查询(如连接)的可能计算方式的详细专业知识。客户端不太可能像引擎一样高效地计算查询。
例如,图2.22中的代码使用两个嵌套循环来计算连接。外层循环遍历STUDENT记录。对于每个学生,内层循环搜索与该学生专业匹配的DEPT记录。虽然这是一个合理的连接算法,但它并不是特别有效。第13章和第14章讨论了几种可以大大提高执行效率的技术。
图2.5和2.22展示了真正优秀和真正糟糕的JDBC代码的极端情况,因此比较起来非常简单。但有时比较起来比较困难。例如,再次考虑图2.17的PreparedFindMajors演示客户端,它返回具有特定专业系的学生。该代码要求引擎执行一个连接STUDENT和MAJOR的SQL查询。假设你知道执行连接操作可能很耗时。经过一番深思熟虑,你意识到无需使用连接操作即可获得所需数据。我们的想法是使用两个单表查询。第一个查询遍历DEPT表,查找具有指定专业名称的记录并返回其DId值。然后,第二个查询使用该值搜索STUDENT记录的MajorID值。该算法的代码见图2.23。
该算法简单、优雅且高效。它只需要对两个表中的每个表进行顺序扫描,应该比连接更快。你可以为你的努力感到自豪。
不幸的是,你的努力是徒劳的。新算法其实并不新,只是巧妙地实现了连接——特别是,它是
public class BadStudentMajor {
public static void main(String[] args) {
ClientDataSource ds = new ClientDataSource();
ds.setServerName("localhost");
ds.setDatabaseName("studentdb");
Connection conn = null;
try {
conn = ds.getConnection();
conn.setAutoCommit(false);
try (Statement stmt1 = conn.createStatement(); Statement stmt2 = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); ResultSet rs1 = stmt1.executeQuery("select * from STUDENT"); ResultSet rs2 = stmt2.executeQuery(" select * from DEPT")) {
System.out.println("Name\tMajor");
while (rs1.next()) { // get the next student
String sname = rs1.getString("SName");
String dname = null;
rs2.beforeFirst();
while (rs2.next()) // search for the major department of that student
if (rs2.getInt("DId") == rs1.getInt("MajorId")) {
dname = rs2.getString("DName");
break;
}
System.out.println(sname + "\t" + dname);
}
}
conn.commit();
conn.close();
} catch (SQLException e) {
e.printStackTrace();
try {
if (conn != null) {
conn.rollback();
conn.close();
}
} catch (SQLException e2) {
}
}
}
}图2.22A 另一种(但糟糕的)StudentMajor客户端编码方法
public class CleverFindMajors {
public static void main(String[] args) {
String major = args[0];
String qry1 = "select DId from DEPT where DName = ?";
String qry2 = "select * from STUDENT where MajorId = ?";
ClientDataSource ds = new ClientDataSource();
ds.setServerName("localhost");
ds.setDatabaseName("studentdb");
try (Connection conn = ds.getConnection()) {
PreparedStatement stmt1 = conn.prepareStatement(qry1);
stmt1.setString(1, major);
ResultSet rs1 = stmt1.executeQuery();
rs1.next();
int deptid = rs1.getInt("DId"); // Get the department ID
rs1.close();
stmt1.close();
PreparedStatement stmt2 = conn.prepareStatement(qry2);
stmt2.setInt(1, deptid);
ResultSet rs2 = stmt2.executeQuery();
System.out.println("Here are the " + major + " majors");
System.out.println("Name\tGradYear");
while (rs2.next()) {
String sname = rs2.getString("sname");
int gradyear = rs2.getInt("gradyear");
System.out.println(sname + "\t" + gradyear);
}
rs2.close();
stmt2.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}图2.23:FindMajors客户端实现的一种巧妙方式
第14章,带有实体化内部表。一个编写良好的数据库引擎会知道这个算法(以及其他几个算法),如果发现这个算法最有效,就会用它来计算连接。因此,你的所有聪明才智都被数据库引擎抢先一步。这个道理与 StudentMajor 客户端是一样的:让引擎完成工作往往是最有效的策略(也是最容易编码的策略)。
初学者在 JDBC 编程时犯的一个错误是,他们试图在客户端做太多的事情。程序员可能认为自己知道一种非常巧妙的方法,可以用 Java 实现查询。或者,程序员可能不确定如何用 SQL 表达查询,而更习惯用 Java 编写查询。在上述每种情况下,用 Java 编写查询的决定几乎都是错误的。程序员必须相信数据库引擎会完成其工作。
2.4 章节总结
• JDBC 方法管理 Java 客户端和数据库引擎之间的数据传输。
• 基本 JDBC 由五个接口组成:驱动程序、连接、语句、结果集和结果集元数据。
• 驱动程序对象封装了与引擎连接的低级细节。如果客户端想要连接到引擎,它必须获得相应驱动程序类的副本。驱动程序类及其连接字符串是JDBC程序中唯一特定于供应商的代码。其他所有内容均参考了与供应商无关的 JDBC 接口。
• 结果集和连接包含其他客户端可能需要的资源。JDBC客户端应尽快关闭它们。
• 每个 JDBC 方法都可能引发SQLException。客户端有义务检查这些异常。
• ResultSetMetaData 方法提供输出表的结构信息,即每个字段的名称、类型和显示大小。当客户端直接接受来自用户的查询时,这些信息非常有用,例如在SQL解释器中。
• 基本的JDBC客户端直接调用驱动程序类。完整的JDBC提供 DriverManager 类和 DataSource 接口,以简化连接过程,使其更中立于供应商。
• DriverManager 类包含一系列驱动程序。客户端通过驱动程序管理器注册驱动程序,可以显式注册,也可以(更可取地)通过系统属性文件注册。当客户端想要连接数据库时,它会向驱动程序管理器提供一个连接字符串,然后由驱动程序管理器为客户端建立连接。
• 数据源对象更加中立,因为它封装了驱动程序和连接字符串。因此,客户端可以连接到数据库引擎,而无需了解任何连接细节。数据库管理员可以创建各种数据源对象,并将其放置在服务器上供客户端使用。
• 基本的 JDBC 客户端忽略事务的存在。数据库引擎以自动提交模式执行这些客户端,这意味着每个SQL语句都是其自己的事务。
事务中的所有数据库交互都被视为一个单元。当当前工作单元成功完成时,事务即提交。当事务无法提交时,事务即回滚。数据库引擎通过撤销事务所做的所有更改来实现回滚。
• 自动提交是简单、不重要的 JDBC 客户端的合理默认模式。如果客户端执行关键任务,那么其程序员应仔细分析其事务需求。客户端通过调用 setAutoCommit(false) 关闭自动提交。此调用导致引擎启动新事务。然后,当客户端需要完成当前事务并开始新事务时,它调用提交或回滚。当客户端关闭自动提交时,必须通过回滚相关事务来处理失败的SQL语句。
• 客户端还可以使用 setTransactionIsolation 方法来指定其隔离级别。JDBC 定义了四个隔离级别:
- 读未提交隔离表示完全没有隔离。事务可能会因读取未提交的数据、不可重复读取或幻影记录而出现问题。
- 读提交隔离禁止事务访问未提交值。仍然可能出现与不可重复读取和幻影相关的错误。
- 可重复读取隔离扩展了读提交,使读取始终可重复。唯一可能出现的问题是由幻影引起的。
- 可序列化隔离确保不会出现任何问题。
• 可序列化隔离显然是首选,但其实施往往会导致事务运行缓慢。程序员必须与客户一起分析可能并发错误的风险,并在风险似乎可以接受的情况下选择限制性较低的隔离级别。
• 预编译语句有一个关联的 SQL 语句,该语句可以有参数占位符。然后,客户端可以在以后为参数分配值,然后执行语句。预编译语句是处理动态生成的 SQL 语句的便捷方法。此外,预编译语句可以在为其参数分配值之前进行编译,这意味着多次执行预编译语句(例如在循环中)将非常高效。
• 完整的JDBC允许结果集可滚动和可更新。默认情况下,记录集只能向前,且不可更新。如果客户端需要更强大的记录集,它可以在连接的 createStatement 方法中指定。
编写JDBC客户端时,经验法则是让引擎尽可能多地工作。数据库引擎非常复杂,通常知道获取所需数据的最高效方法。对于客户端来说,确定一个SQL语句来精确检索所需数据并将其提交给引擎几乎总是明智之举。简而言之,程序员必须相信引擎能够胜任工作。
2.5 建议阅读
Fisher等人(2003)所著的《JDBC》一书内容全面、文笔流畅,部分内容以在线教程的形式发布在 docs.oracle.com/javase/tutorial/jdbc 网站上。此外,每个数据库供应商都提供文档,解释其驱动程序的使用方法以及其他供应商特有的问题。如果您打算为特定引擎编写客户端,那么熟悉文档是必不可少的。
Fisher, M., Ellis, J., & Bruce, J. (2003). JDBC API tutorial and reference (3rd ed.).
Addison Wesley.
2.6 练习
概念性练习
2.1. Derby文档建议在执行一系列插入操作时关闭自动提交。解释为什么你认为它提出了这个建议。
编程练习
2.2. 为大学数据库编写一些SQL查询。对于每个查询,使用Derby编写一个程序,执行该查询并打印其输出表。
2.3. SimpleIJ程序要求每个SQL语句为一行文本。修改它,使语句可以包含多行,并以分号结尾,类似于Derby的ij程序。
2.4. 为SimpleDB编写一个类NetworkDataSource,其工作方式与Derby类ClientDataSource类似。将此类添加到包simpledb.jdbc.network中。您的代码无需实现接口javax.sql.DataSource(及其超类)的所有方法;事实上,它只需要实现其中不带参数的方法getConnection()。NetworkDataSource应该有哪些特定于供应商的方法?
2.5. 能够创建包含 SQL 命令的文本文件通常很有用。然后,这些命令可以由 JDBC 程序批量执行。编写一个 JDBC 程序,从指定的文本文件中读取命令并执行它们。假设文件的每一行都是一个单独的命令。
2.6. 研究如何使用结果集填充Java JTable对象。(提示:您需要扩展AbstractTableModel类。)然后修改演示客户端FindMajors,使其具有一个GUI界面,用于在JTable中显示输出。
2.7. 为以下任务编写JDBC代码:
(a) 将数据从文本文件导入到现有表中。文本文件每行应包含一个记录,每个字段用制表符分隔。文件2.6练习
47
应该是字段的名称。客户应该将文件名和表名作为输入,并将记录插入表中。 (b) 将数据导出到文本文件。客户端应该以文件名和表名作为输入,将每条记录的内容写入文件。文件的第一行应该是字段的名称。
2.8. 本章忽略了结果集中空值的可能性。要检查空值,可以使用ResultSet中的wasNull方法。假设您调用getInt或getString来检索字段值。如果您紧接着调用wasNull,如果检索到的值是空值,它将返回true。例如,下面的循环打印毕业年份,假设其中一些可能是空值:
while (rs.next()) {
int gradyr = rs.getInt("gradyear");
if (rs.wasNull()) {
System.out.println("null");
} else {
System.out.println(gradyr);
}
}(a) 假设学生姓名可能为空,重写 StudentMajor 演示客户端的代码。
(b) 修改 SimpleIJ 演示客户端,使其连接到 Derby(而不是SimpleDB)。然后假设任何字段值可能为空,重写代码。
