JDBC--高级JDBC


高级JDBC

  基本的JDBC使用起来相对简单,但它提供了一组相当有限的与数据库引擎交互的方法。本节将考虑JDBC的一些附加特性,这些特性使客户端能够更多地控制访问数据库的方式。

隐藏驱动程序(Driver)

  在基础JDBC中,客户端通过获取Driver对象的实例并调用其连接(connect)方法来连接到数据库引擎。该策略的一个问题是,它将特定于供应商的代码放置到客户端程序中。JDBC包含两个与供应商无关的类,用于将驱动程序信息保持在客户端程序之外:驱动器管理器(DriverManager)和数据源(DataSource)。

使用驱动器管理器(DriverManager)

  类驱动器管理器包含一组驱动程序。它包含一些静态方法,可以将驱动程序添加到集合中,并在集合中搜索可以处理给定连接字符串的驱动程序。其中两种方法如下:

static public void registerDriver(Driver driver) throws SQLException;
static public Connection getConnection(String url, Properties p) throws SQLException;

  其想法是,客户机反复调用注册程序驱动程序,以为它可能使用的每个数据库注册驱动程序。当客户端想要连接到数据库时,它只需要调用getConnection方法并为其提供一个连接字符串。驱动程序管理器在其集合中的每个驱动程序上尝试连接字符串,直到其中一个返回一个非空连接。

  如下代码前两行向驱动程序管理器注册基于服务器的Derby和SimpleDB驱动程序。最后两行建立了到Derby服务器的连接。客户端在调用getConnection方法获取连接时不需要指定驱动程序;它只指定连接字符串。驱动程序管理器决定使用哪个已注册的驱动程序。

DriverManager.registerDriver(new ClientDriver());
DriverManager.registerDriver(new NetworkDriver());
String url = "jdbc:derby://localhost/studentdb";
Connection c = DriverManager.getConnection(url);

  在上述例子中使用驱动器管理器并不是特别令人满意,因为驱动程序信息并没有被隐藏——它就在对注册驱动程序的调用中。JDBC通过允许在Java系统属性文件中指定驱动程序来解决这个问题。例如,可以通过向Properties文件添加以下行来注册Derby和SimpleDB驱动程序:“jdbc.drivers=org.apache.derby.jdbc.ClientDriver:simpledb.remote.NetworkDriver”。将驱动程序注册信息放在属性文件中是从客户端代码中删除驱动程序规范的一种优雅方法。通过更改这个文件,可以修改所有JDBC客户端使用的驱动程序信息,而不必重新编译任何代码。

使用数据源(DataSource)

  尽管驱动程序管理器可以对JDBC客户端隐藏驱动程序,但它不能隐藏连接字符串。特别是,上面示例中的连接字符串包含“jdbc:derby”,因此很明显,目标驱动器是哪个驱动程序。JDBC最近的一个添加项是包javax.sql中的接口数据源(DataSource)。这是目前管理驱动程序的首选策略。
  数据源对象同时封装了驱动程序和连接字符串,从而使客户端能够在不知道任何连接细节的情况下连接到引擎。要在Derby中创建数据源,您需要Derby提供的类客户端数据源(ClientDataSource,用于基于服务器的连接)和嵌入式数据源(EmbeddedDataSource,用于嵌入式连接),这两者都实现了数据源。客户端代码可能如下:

ClientDataSource ds = new ClientDataSource();
ds.setServerName("localhost");
ds.setDatabaseName("studentdb");
Connection conn = ds.getConnection();

  每个数据库供应商提供实现数据源的类。由于这些类是特定于供应商的,因此它们可以封装其驱动程序的详细信息,例如驱动程序名称和连接字符串的语法。使用它们的程序只需要指定所需的值。
​  使用数据源的好处是,客户端不再需要知道驱动程序的名称或连接字符串的语法。然而,该类仍然是特定于供应商的,因此客户端代码仍然不是完全独立于供应商的。这个问题可以用各种方式来解决。
  一种解决方案是让数据库管理员(DBA,database administrator)将数据源对象保存在一个文件中。DBA可以创建该对象,并使用Java序列化方法将其写入该文件。然后,客户端可以通过读取该文件并将其反序列化回一个数据源对象来获得数据源。此解决方案类似于使用属性文件。一旦数据源对象保存在文件中,任何JDBC客户端都可以使用它。并且DBA可以通过简单地替换该文件的内容来更改数据源。
  第二种解决方案是使用名称服务器(如JNDI服务器)而不是文件。DBA将数据源对象放置在名称服务器上,然后客户端从服务器请求数据源。鉴于名称服务器是许多计算环境的共同组成部分,这个解决方案通常很容易实现。

显式事务处理

  每个JDBC客户端都以一系列事务的形式运行。从概念上讲,事务是一个“工作单位”,这意味着它的所有数据库交互都被视为一个单元。例如,如果事务中的一个更新失败,数据库引擎将确保该事务所进行的所有更新都将失败。
  事务在其当前工作单元成功完成时提交。数据库引擎通过将所有修改永久化并释放分配给该事务的任何资源(例如,锁)来实现提交。在提交完成后,引擎将启动一个新的事务。
  事务在无法提交时会回滚。数据库引擎通过撤消该事务所做的所有更改、释放锁并启动一个新事务来实现回滚。已提交或回滚的事务被称为已完成。
  事务处理隐含在基础JDBC中。数据库引擎会选择每个事务的边界,从而决定何时应该提交事务以及是否应该回滚该事务。这种情况被称为自动提交(autocommit)。
  在自动提交过程中,数据库引擎在自己的事务中执行每个SQL语句。如果语句成功完成,引擎将提交事务,否则将回滚事务。一旦executeUpdate方法完成,更新命令就会完成,而当查询的结果集被关闭时,查询就会完成。
  事务会获得锁,直到事务提交或回滚后才释放这些锁。因为这些锁可能会导致其他事务等待,因此较短的事务会启用更多的并发性。这一原则意味着,在自动提交模式下运行的客户端应该尽快关闭其结果集。
  自动提交是JDBC客户端的一种合理的默认模式。每个SQL语句有一个事务会导致短事务,这通常是正确的做法。但是,在某些情况下,一个事务应该由多个SQL语句组成(例如转账)。
  不需要自动提交的一种情况是,客户端需要同时执行两个语句。如下代码片段所示,此代码首先执行一个检索所有课程的查询。然后,它循环浏览结果集,询问用户是否应该删除每个课程。如果是这样,它将执行一个SQL删除语句。

DataSource ds = ...
Connection conn = ds.getConnection();
Statement stmt1 = conn.createStatement();
Statement stmt2 = conn.createStatement();
ResultSet rs = stmt1.executeQuery("select * from COURSE");
while (rs.next()) {
String title = rs.getString("Title");
boolean goodCourse = getUserDecision(title);
if (!goodCourse) {
int id = rs.getInt("CId");
stmt2.executeUpdate("delete from COURSE where CId =" + id); } }
rs.close();

  这段代码的问题是,将在记录集仍然打开时执行删除语句。因为一个连接一次只支持一个事务,所以它必须先提交查询的事务,然后才能创建一个新的事务来执行删除。而且由于查询的事务已经提交,所以访问记录集的其余部分真的没有意义。该代码要么抛出异常,要么具有不可预测的行为。
  当需要同时对数据库进行多次修改时,也不需要进行自动提交。r如下代码片段提供了一个示例。该代码的目的是交换教授教学第43和53节。但是,如果数据库引擎在第一次调用executeUpdate方法之后但在第二次调用之前崩溃,那么数据库将变得不正确。此代码需要两个SQL语句在同一事务中发生,以便它们要么一起提交,要么一起回滚。

DataSource ds = ...
Connection conn = ds.getConnection();
Statement stmt = conn.createStatement();
String cmd1 = "update SECTION set Prof= 'brando' where SectId = 43";
String cmd2 = "update SECTION set Prof= 'einstein' where SectId = 53";
stmt.executeUpdate(cmd1);
// suppose that the engine crashes at this point(假设数据库引擎在这个节点发生故障)
stmt.executeUpdate(cmd2);

  自动提交模式也可能很不方便。假设程序正在执行多个插入语句,比如通过从文本文件中加载数据。如果引擎在程序运行时崩溃,那么会插入一些记录,有些不会插入。确定程序失败的位置并重写它以只插入丢失的记录可能是非常乏味和耗时的。一个更好的选择是将所有的插入命令放置在同一事务中。然后,在系统崩溃后,它们都会被回滚,并且可以简单地重新运行客户端。
  连接(Connection)接口包含三种方法,它们允许客户端显式地处理其事务。以下代码块给出了它们的API。客户端通过调用setAutoCommit(false)来关闭自动提交。客户端完成当前事务,并通过调用提交或回滚启动新事务。

public void setAutoCommit(boolean ac) throws SQLException;//设置是否开启自动提交
public void commit() throws SQLException;//提交事务
public void rollback() throws SQLException;//回滚事务

  当客户端关闭自动提交时,它将承担回滚失败的SQL语句的责任。特别是,如果在事务期间抛出异常,那么客户端必须在其异常处理代码中回滚该事务。
  如下代码块,代码在连接创建后立即调用setAutoCommit,并在语句完成后立即调用提交。捕获块包含对回滚的调用。这个调用需要放在它自己的尝试块中,以防它抛出一个异常。

DataSource ds = ...
        try (Connection conn = ds.getConnection()) {
            conn.setAutoCommit(false);
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("select * from COURSE");
            while (rs.next()) {
                String title = rs.getString("Title");
                boolean goodCourse = getUserDecision(title);
                if (!goodCourse) {
                    int id = rs.getInt("CId");
                    stmt.executeUpdate("delete from COURSE where CId =" + id); } }
            rs.close();
            stmt.close();
            conn.commit();
        }
        catch (SQLException e) {
            e.printStackTrace();
            try {
                if (conn != null)
                    conn.rollback();
            }
            catch (SQLException e2) {}
        }

事务隔离级别

  数据库服务器通常同时有多个活动的客户端,每个客户端都运行自己的事务。通过并发地执行这些事务,服务器可以提高它们的吞吐量和响应时间。因此,并发性是一件好事。但是,不受控制的并发性可能会导致问题,因为一个事务可能会通过以意外的方式修改该其他事务所使用的数据来干扰另一个事务。下面有三个例子来说明可能发生的各种问题。

示例1:正在读取未提交的数据

  再次考虑上述教授换课的代码,该代码交换了两位教授,并假设它作为单个事务运行(即,关闭了自动提交)。称此交换为T1。还假设大学决定根据教授课程的数量给教授发放奖金;因此,它执行一个事务T2,计算每个教授教授的课程。此外,假设这两个事务碰巧并发地运行——特别是,假设T2在T1的第一个更新语句之后立即开始并执行到完成。其结果是,白兰度教授和爱因斯坦教授将分别获得比他们应得的额外和少的课程,这将影响他们的奖金。
  哪里出了问题?每一笔事务都是正确的,但它们一起会导致大学发放错误的奖金。问题是T2错误地假设它读取的记录是一致的,也就是说,它们在一起是有意义的。但是,由未提交的事务写入的数据可能并不总是一致的。在T1的情况下,不一致发生在只有两个修改中的一个的地方。当T2此时读取未提交的修改记录时,不一致性导致它做出不正确的计算。

示例2:对现有记录进行的意外更改

  在此例子中,假设学生表包含一个MealPlanBal字段,它表示学生在自助餐厅购买食物的钱。考虑如下代码块中的两个事务。事务T1执行时,当乔买了一个10美元的午餐。事务处理运行一个查询来找出他的当前余额,验证余额是否足够,并适当地减少他的余额。当乔的父母寄来了一张1000美元的支票,作为他的膳食计划余额。该事务只需运行一个SQL更新语句来增加Joe的余额。

DataSource ds = ...
        Connection conn = ds.getConnection();
        conn.setAutoCommit(false);
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("select MealPlanBal from STUDENT "
                + "where SId = 1");
        rs.next();
        int balance = rs.getInt("MealPlanBal");
        rs.close();
        int newbalance = balance 10;
        if (newbalance < 0)
            throw new NoFoodAllowedException("You cannot afford this meal");
        stmt.executeUpdate("update STUDENT "
                + "set MealPlanBal = " + newbalance
                + " where SId = 1");
        conn.commit();


DataSource ds = ...
Connection conn = ds.getConnection();
conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
stmt.executeUpdate("update STUDENT "
                + "set MealPlanBal = MealPlanBal + 1000 "
                + "where SId = 1");
conn.commit();

  现在假设这两笔交易恰好在乔有50美元余额的时候同时运行。特别是,假设T2在T1调用rs.close后立即开始并执行到完成。然后T2将余额修改为1050美元。然而,T1并不知道这一变化,仍然认为余额是50美元。因此,它将余额修改为40美元并承诺。结果是,这1000美元的存款没有记入他的余额,也就是说,更新被“丢失了”。
  这里的问题是,事务T1错误地假设,膳食计划余额的值在T1读取该值的时间和T1修改该值的时间之间不会发生变化。在形式上,这个假设称为可重复读取,因为事务假设从数据库中重复读取一个条目将总是返回相同的值。

示例3:对记录数量的意外更改

  假设大学餐饮服务公司去年盈利了10万美元。大学对学生收费过高感到难过,所以决定给他们平均分配利润。也就是说,如果现在有1000名学生,那么大学将在每一餐计划的余额上增加100美元。该代码如下所示。

DataSource ds = ...
Connection conn = ds.getConnection();
conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
String qry = "select count(SId) as HowMany from STUDENT "
	+ "where GradYear >= extract(year, current_date)"
ResultSet rs = stmt.executeQuery(qry);
rs.next();
int count = rs.getInt("HowMany");
rs.close();
int rebate = 100000 / count;
String cmd = "update STUDENT "
+ "set MealPlanBalance = MealPlanBalance + " + rebate
+ " where GradYear >= extract(year, current_date)";
stmt.executeUpdate(cmd);
conn.commit();

  该事务的问题是,它假设现有学生的数量在计算退税金额和更新学生记录之间不会发生变化。但是,假设在关闭记录集和执行更新语句之间,有几个新的学生记录被插入到数据库中。这些新记录将错误地获得预先计算的退税,而大学最终将在退税上花费超过10万美元。这些新记录被称为幻影记录,因为它们在事务开始后就神秘地出现了。

  这些例子说明了当两个事务交互时可能出现的问题。保证任意事务不会有问题的唯一方法是与其他事务完全隔离地执行它。这种形式的隔离称为可序列化性。
​ 不幸的是,可序列化的事务可能非常缓慢,因为它们需要数据库引擎显著减少所允许的并发性。因此,JDBC定义了四个隔离级别,它们允许客户端指定一个事务应该有多少隔离级别:

  • 读取-未提交的隔离意味着根本没有隔离。这样的交易可能会遇到上述三个例子中的任何问题。
  • 读取-已提交的隔离禁止事务访问未提交的值。与不可重复的读取和幻影相关的问题仍然是可能的。
  • 可重复的读取隔离扩展了读取的提交,因此读取总是可重复的。唯一可能出现的问题是由于幻影。
  • 可序列化的隔离保证了永远不会发生任何问题。

  JDBC客户端通过调用连接方法设置处理隔离(setTransactionIsolation)函数来指定它所需的隔离级别。例如,以下代码片段将隔离级别设置为可序列化:

DataSource ds = ...
Connection conn = ds.getConnection();
conn.setAutoCommit(false);
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);

  这四个隔离级别显示了执行速度和潜在问题之间的权衡。也就是说,您希望事务运行得越快,您必须接受的事务可能运行错误的风险就越大。这种风险可以通过对客户的仔细分析来减轻。
  许多数据库服务器(包括Derby、Oracle和Sybase)的默认隔离级别是读提交的。此级别适用于朴素用户在自动提交模式下提出的简单查询。但是,如果客户端程序执行关键任务,那么同样重要的是要仔细确定最适当的隔离级别。关闭自动提交模式的程序员必须非常小心地为每个事务选择适当的隔离级别。

预处理语句

FindMajors

  许多JDBC客户端程序都是参数化的,因为它们接受来自用户的参值,并基于该参数执行一个SQL语句。这类客户端的一个例子是FindMajors:

import org.apache.derby.jdbc.ClientDataSource;
import org.apache.derby.jdbc.EmbeddedDriver;

import java.sql.Connection;
import java.sql.Driver;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Scanner;

public class FindMajors {
    public static void main(String[] args) {
        System.out.print("Enter a department name: ");
        Scanner sc = new Scanner(System.in);
        String major = sc.next();
        sc.close();
        String qry = "select sname, gradyear from student, dept "
                + "where did = majorid and dname = '" + major + "'";
        /*ClientDataSource ds = new ClientDataSource();
        ds.setServerName("localhost");
        ds.setDatabaseName("studentdb");*/
        String url = "jdbc:derby:d:/test/studentdb";
        Driver d = new EmbeddedDriver();
        try (//Connection conn = ds.getConnection();
             Connection conn = d.connect(url, null);
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(qry)) {
            System.out.println("Here are the " + major + " majors");
            System.out.println("Name\tGradYear");
            while (rs.next()) {
                String sname = rs.getString("sname");
                int gradyear = rs.getInt("gradyear");
                System.out.println(sname + "\t" + gradyear);
            } }
        catch(Exception e) {
            e.printStackTrace();
        }
    }
}

  此客户端首先询问用户的部门名称。然后,它将这个名称合并到它执行的SQL查询中。例如,假设用户输入了值“math”。那么生成的SQL查询将如下所示:select SName, GradYear from STUDENT, DEPT where DId = MajorId and DName = ‘math’。
  代码在生成查询时,如何显式地添加部门名称周围的单引号:客户端可以使用参数化的SQL语句,而不是以这种方式动态地生成SQL语句。一个参数化的语句是一个SQL语句,其中的“?”字符表示缺失的参数值。一个语句可以有几个参数,都用“?”表示。每个参数都有一个对应于其在字符串中的位置的索引值。例如,下面的参数化语句删除所有尚未指定毕业年份和专业的学生。GradYear的值分配索引1,MajorId的值分配索引2。
  JDBC类处理参数化语句。客户端通过三个步骤处理预处理语句:

  • 它为指定的参数化SQL语句创建一个预处理语句(PreparedStatement )对象。
  • 它会为这些参数赋值。
  • 它执行已预处理语句。

PreparedFindMajors

  如下代码块修改了FindMajors客户端以使用预处理语句。首先,客户端通过调用方法准备语句并传递参数化的SQL语句作为参数来创建预处理语句(PreparedStatement)对象。其次,客户端调用setString方法来为第一个(也是唯一的)参数分配一个值。第三,方法调用executeQuery来执行语句。

import org.apache.derby.jdbc.ClientDataSource;
import org.apache.derby.jdbc.EmbeddedDriver;

import java.sql.Connection;
import java.sql.Driver;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Scanner;

public class PreparedFindMajors {
    public static void main(String[] args) {
        System.out.print("Enter a department name: ");
        Scanner sc = new Scanner(System.in);
        String major = sc.next();
        sc.close();
        String qry = "select sname, gradyear from student, dept "
                + "where did = majorid and dname = ?";
        /*ClientDataSource ds = new ClientDataSource();
        ds.setServerName("localhost");
        ds.setDatabaseName("studentdb");*/
        String url = "jdbc:derby:d:/test/studentdb";
        Driver d = new EmbeddedDriver();
        try (//Connection conn = ds.getConnection();
             Connection conn = d.connect(url, null);
             PreparedStatement pstmt = conn.prepareStatement(qry)) {
            pstmt.setString(1, major);
            ResultSet rs = pstmt.executeQuery();
            System.out.println("Here are the " + major + " majors");
            System.out.println("Name\tGradYear");
            while (rs.next()) {
                String sname = rs.getString("sname");
                int gradyear = rs.getInt("gradyear");
                System.out.println(sname + "\t" + gradyear);
            }
            rs.close();
        }
        catch(Exception e) {
            e.printStackTrace();
        }
    }
}

  更改以粗体显示。最后三个粗体语句对应于上述三个要点。

  • String qry = "select sname, gradyear from student, dept "+ “where did = majorid and dname = ?”

  • PreparedStatement pstmt = conn.prepareStatement(qry)

  • *pstmt.setString(1, major);

  • ResultSet rs = pstmt.executeQuery()

  如下代码块给出了最常见的预处理语句(PreparedStatement)方法的API。方法执行查询和执行更新类似于语句中相应的方法;不同之处在于它们不需要任何参数。这些方法可以设置Int和设置String,从而为参数赋值。请注意,setString方法会自动在其值周围插入单引号,因此客户端不必这样做。

public ResultSet executeQuery() throws SQLException;
public int executeUpdate() throws SQLException;
public void setInt(int index, int val) throws SQLException;
public void setString(int index, String val) throws SQLException;

  大多数人发现,使用预处理语句(PreparedStatement)比显式地创建SQL语句更方便。当在循环中生成语句时,预处理语句也是更有效的选项,如下代码块所示。原因是数据库引擎能够在不知道其参数值的情况下编译准备好语句。它编译语句一次,然后在循环中重复执行它,而无需进一步重新编译。

// Prepare the query(准备查询语句)
String qry = "select SName, GradYear from STUDENT, DEPT "
+ "where DId = MajorId and DName = ?";
PreparedStatement pstmt = conn.prepareStatement(qry);
// Repeatedly get parameters and execute the query(循环重复获取参数,并执行查询语句)
String major = getUserInput();
while (major != null) {
pstmt.setString(1, major);
ResultSet rs = pstmt.executeQuery();
displayResultSet(rs);
major = getUserInput();
}

可回滚的和可更新的结果集

  基本JDBC中的结果集是仅正向的和不可更新的。完整的JDBC还允许结果集是可滚动的和可更新的。客户端可以将这些结果集定位在任意的记录上,更新当前的记录,并插入新的记录。如下代码块给出了这些附加方法的API。

Methods used by scrollable result sets(用于结果集回滚操作的方法)
public void beforeFirst() throws SQLException;
public void afterLast() throws SQLException;
public boolean previous() throws SQLException;
public boolean next() throws SQLException;
public boolean absolute(int pos) throws SQLException;
public boolean relative(int offset) throws SQLException;

Methods used by updatable result sets(用于结果集更新操作的方法)
public void updateInt(String fldname, int val) throws SQLException;
public void updateString(String fldname, String val)
 throws SQLException;
public void updateRow() throws SQLException;
public void deleteRow() throws SQLException;
public void moveToInsertRow() throws SQLException;
public void moveToCurrentRow() throws SQLException;

  首先,beforeFirst方法将结果集定位在第一个记录之前,而afterLast方法将结果集定位在最后一个记录之后。absolute方法将结果集绝对定位在指定的记录上,如果没有这样的记录,则返回false。relative方法相对定位结果设置的相对行数。特别是,relative(1)与next相同,relative(-1)与previous相同。
  updateInt和updateString方法修改客户端上当前记录的指定字段。但是,在调用updateRow方法之前,修改不会发送到数据库。需要调用updaterow有点危险(涉及对数据库操作),但是它允许JDBC将一个记录的多个字段批处理更新到对引擎的一次调用中。
  插入操作由插入行(insert row)的概念来处理。它的目的是作为一个新记录的登台区。客户端调用moveToInsertRow方法将结果集定位到插入行,然后调用updateXXX方法设置其字段的值,然后调用updaterow将记方法录插入到数据库中,最后调用moveTocurrentRow方法将设置的记录重新定位到插入之前的位置。
  默认情况下,记录集(结果集)仅可转发且不可更新。如果客户端想要一个更强大的结果集,它会在连接(Connection)的创建语句(createStatement)方法中这样指定。除了基本JDBC的无参创建语句方法之外,还有一个双a参方法,其中客户端指定了可滚动性和可更新性。例如:

Statement stmt =conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_UPDATABLE);

  从此语句生成的所有结果集都是可回滚和更新的。常量TYPE_FORWARD_ONLY指定不可回滚的结果集,而CONCUR_READ_ONLY指定不可更新的结果集。这些常数可以被混合和匹配,以获得所期望的可回滚性和可更新性。
  如下代码块使用可更新的结果集。

DataSource ds = ...
Connection conn = ds.getConnection();
conn.setAutocommit(false);
Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_UPDATABLE);
ResultSet rs = stmt.executeQuery("select * from COURSE");
while (rs.next()) {
String title = rs.getString("Title");
boolean goodCourse = getUserDecision(title);
if (!goodCourse)
rs.deleteRow(); }
rs.close();
stmt.close();
conn.commit();

  一个可回滚的结果集的使用情况都很有限,因为大多数时候客户端都知道它想对输出记录做什么,并且不需要检查它们两次。通常,只有在客户端允许用户与查询的结果进行交互时,客户端才需要一个可回滚的结果集。例如,考虑一个希望将查询的输出显示为SwingJTable对象的客户端。当屏幕上有太多的输出记录时,JTable将显示一个滚动条,并允许用户通过单击滚动条在记录中来回移动。这种情况要求客户端为JTable对象提供一个可滚动的结果集,以便在用户回滚时可以检索以前的记录。

其他数据类型

  除了整数值(int)和字符串值(String)外,JDBC还包含了操作许多其他类型的方法。例如接口结果集。除了getInt方法和getString方法,还有getFloat、getDouble、getShort、getTime、getDate和其他一些方法。这些方法都将从当前记录的指定字段读取值,并将其转换(如果可能的话)转换为指定的Java类型。当然,一般来说,使用数值JDBC方法(如getInt、getFloat等)是最有意义的。在数字SQL字段上,等等。但是JDBC将尝试将任何SQL值转换为该方法所指示的Java类型。特别是,总是可以将任何SQL值转换为Java字符串。

Java(客户端)与SQL(数据库引擎)计算比较

  当程序员编写JDBC客户端时,必须做出一个重要的决定:计算的哪一部分应该由数据库引擎执行,哪些部分应该由Java客户端执行?
  例如[StudentMajor](# StudentMajor)客户端程序,在该程序中,引擎通过执行一个SQL查询来计算学生表和DEPT表的连接,来执行所有的计算。客户端的唯一职责是检索查询输出并打印它。

BadStudentMajor

  或者可以编写客户端,使它完成所有的计算,如下代码块所示。在该代码中,引擎的唯一责任是为学生表和删除表创建结果集。客户机将完成所有剩下的工作,计算连接并打印结果。

import org.apache.derby.jdbc.ClientDataSource;
import org.apache.derby.jdbc.EmbeddedDriver;

import java.sql.*;

public class BadStudentMajor {
    public static void main(String[] args) {
        /*ClientDataSource ds = new ClientDataSource();
        ds.setServerName("localhost");
        ds.setDatabaseName("studentdb");*/
        String url = "jdbc:derby:d:/test/studentdb";
        Driver d = new EmbeddedDriver();
        Connection conn = null;
        try {
            //conn = ds.getConnection();
            conn = d.connect(url, null);
            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) {
            }
        }
    }
}

  上述代码通过使用两个嵌套的循环来计算连接。外环会遍历学生记录。对于每个学生,内环搜索与该学生的专业相匹配的DEPT记录。虽然这是一个合理的连接算法,但它并不是特别有效。

  [StudentMajor](# StudentMajor)和[BadStudentMajor](# BadStudentMajor)说明了非常好和非常坏的JDBC代码的极端情况,因此比较它们非常容易。但有时,进行比较就会更加困难。例如,再次考虑[PreparedFindMajors](# PreparedFindMajors)演示客户端,它返回有一个特定的主要部门的学生。该代码要求引擎执行一个连接学生和专业的SQL查询。假设您知道,执行一个连接可能会很耗时。经过一些认真的思考,您意识到不使用连接就可以获得所需要的数据。其想法是使用两个单表查询。第一个查询扫描DEPT表,查找具有指定主要名称的记录并返回其移除值。然后,第二个查询将使用该值来搜索学生记录的MajorID值。该算法的代码如图下代码块所示。

import org.apache.derby.jdbc.ClientDataSource;
import org.apache.derby.jdbc.EmbeddedDriver;

import java.sql.Connection;
import java.sql.Driver;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Scanner;

public class CleverFindMajors {
    public static void main(String[] args) {
        String major = args[0];//需要配置运行参数,例如在Program Arguments中填入math,或者注释此语句,放开紧接着的注释代码块,通过屏幕输入流输入参数
        /*System.out.print("Enter a department name: ");
        Scanner sc = new Scanner(System.in);
        String major = sc.next();*/
        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");*/
        String url = "jdbc:derby:d:/test/studentdb";
        Driver d = new EmbeddedDriver();
        try (//Connection conn = ds.getConnection()
             Connection conn = d.connect(url, null);
        ) {
            PreparedStatement stmt1 = conn.prepareStatement(qry1);
            stmt1.setString(1, major);
            ResultSet rs1 = stmt1.executeQuery();
            rs1.next();
            int deptid=rs1.getInt("Did");//获得major对应的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();
        }
    }
}

  该算法简单、优雅、高效。它所需要的是对两个表进行顺序扫描,应该比连接快得多。不幸的是,这个努力浪费了。新算法并不是很新,只是一个聪明的连接实现。一个编写得好的数据库引擎会知道这个算法(以及其他一些算法),如果它是最有效的,将使用它来计算连接,让数据库引擎做工作往往是最有效的策略(也是最容易编码的策略)。
  开始JDBC程序员犯的一个错误是他们试图在客户端中做太多事情。程序员可能认为他或她知道一种非常聪明的方法来实现Java查询。或者程序员可能不确定如何用SQL表达查询,并且觉得用Java编码查询更舒服。在这些情况下,用Java编写查询代码的决定几乎总是错误的。程序员必须相信数据库引擎将完成其工作。

小结

  1. JDBC方法可管理Java客户机和数据库引擎之间的数据传输。.
  2. 基础JDBC由五个接口组成:Driver,Connection,Statement,ResultSet,和 ResultSetMetaData。
  3. 驱动程序(Driver)对象封装了用于与引擎连接的低级详细信息。如果客户端想要连接到一个引擎,则必须获得适当的驱动程序类的实例。驱动程序类及其连接字符串是JDBC程序中唯一的特定于供应商的代码。其他的一切都指向与供应商无关的JDBC接口。
  4. 结果集(ResultSet)和连接(Connection)保存了其他客户机可能需要的资源。JDBC客户端应该总是尽快关闭它们。(以节省内存、连接等资源)
  5. 每个JDBC方法都可以抛出一个SQL异常(SQLException)。客户端有义务检查这些异常情况。
  6. ResultSetMetaData的方法提供了有关输出表的模式的信息,即每个字段的名称、类型和显示大小。当客户端直接接受来自用户的查询时,此信息非常有用。
  7. 一个基本的JDBC客户端直接调用驱动程序类。完整JDBC提供了类驱动器管理器(DriverManager)和接口数据源(DataSource),以简化连接过程,并使其更与供应商无关。
  8. 类驱动器管理器(DriverManager)包含一组驱动程序。客户端显式地注册驱动程序管理器,或(优选)通过系统属性文件。当客户端希望连接到数据库时,它会向驱动程序管理器提供一个连接字符串,并为客户端进行连接。
  9. 数据源对象(DataSource)更适合供应商,因为它封装了驱动程序和连接字符串。因此,客户端可以在不知道任何连接细节的情况下连接到数据库引擎。数据库管理员可以创建各种数据源对象,并将它们放在服务器上供客户机使用。
  10. 一个基本的JDBC客户端会忽略事务的存在。数据库引擎以自动提交模式执行这些客户端,这意味着在自动提交模式每条SQL语句都是一个事务。
  11. 事务中的所有数据库交互都被视为一个单元。事务在其当前工作单元成功完成时提交。事务在无法提交时会回滚。数据库引擎通过撤消该事务所做的所有更改来实现回滚。
  12. 对于简单、不重要的JDBC客户机,自动提交是一种合理的默认模式。如果客户机执行关键任务,那么它的程序员应该仔细分析其事务需求。客户端通过调用setAutoCommit(false)来关闭自动提交。此调用会导致引擎启动一个新的事务。然后,当客户端需要完成当前事务并开始一个新的事务时,它会调用提交或回滚。当客户端关闭自动提交时,它必须通过回滚关联的事务来处理失败的SQL语句。
  13. 客户端还可以使用设置事务隔离(setTransactionIsolation )方法来指定其隔离级别。JDBC定义了四个隔离级别(从低到高):
    • 读取-未提交的隔离意味着根本没有隔离。事务可能因读取未提交数据、不可重复读取或幻影记录而产生问题。
    • 读取-已提交的隔离禁止事务访问未提交的值。与不可重复的读取和幻影相关的问题仍然是可能的。
    • 可重复的读取隔离扩展了读取的提交,因此读取总是可重复的。唯一可能出现的问题是由于幻影。
    • 可序列化的隔离保证了永远不会发生任何问题。
  14. 可序列化的隔离显然是首选的,但它的实现往往会导致事务运行缓慢。程序员必须分析客户端可能出现并发错误的风险,只有在风险看起来可以容忍的情况下才选择限制较少的隔离级别。
  15. 预处理语句有一个关联的SQL语句,该语句可以具有参数的占位符。然后,客户端可以在稍后的时间内为这些参数分配值,然后执行该语句。预处理语句是处理动态生成的SQL语句的一种方便的方法。此外,一条预处理语句可以在分配其参数之前进行编译,这意味着多次执行一条预处理语句(例如在一个循环中)将是非常有效的。
  16. 完整的JDBC允许结果集是可滚动的和可更新的。默认情况下,记录集仅可转发且不可更新。如果客户端想要一个更强大的记录集,它会在连接(Connection)的创建语句(createStatement)方法中这样指定。
  17. 在编写JDBC客户端时,经验法则是让引擎做尽可能多的工作。数据库引擎非常复杂,并且通常知道获得所需数据的最有效的方法。对于客户机来说,确定一个精确检索所需数据并提交给引擎的SQL语句几乎总是一个好主意。简而言之,程序员必须相信引擎来完成它的工作。(数据库引擎对多表联合查询可能比自定义客户端进行多个单表操作更加高效)

推荐阅读

   Fisher等人(2003)写了一本关于JDBC的全面而完善的书,其中一部分作为docs.oracle.com/javase/tutorial/jdbc的在线教程。此外,每个数据库供应商都提供了解释其驱动程序的使用以及其他特定于供应商的问题的文档。

参考文献

Fisher M , Ellis J , Bruce J . JDBC™ API Tutorial and Reference, 3rd Edition. 2003.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zszyejing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值