20230628 5. 数据库编程

数据库编程

Java 数据库连接( JDBC )API 使编程人员可以通过这个 API 接口连接到数据库,并使用结构化查询语 (即 SQL )完成对数据库的查找与更新

根据 Oracle 的声明,JDBC 是一个注册了商标的术语,而并非 Java Database Connectivity 的首字母缩写。对它的命名体现了对 ODBC 的致敬,后者是微软开创的标准数据库 API ,并因此而并入了 SQL 标准中

JDBC 规范下载地址

JDBC 的设计

业界存在许多不同的数据库,且它们所使用的协议也各不相同。尽管很多数据库供应商都表示支持 Java 提供一套数据库访问的标准网络协议,但是每家企业都希望 Java 能采用自己的网络协议

所有的数据库供应商和工具开发商都认为,如果 Java 能够为 SQL 访问提供一套 “纯” JavaAPI ,同时提供一个驱动管理器,以允许第三方驱动程序可以连接到特定的数据库,那它就会显得非常有用。这样,数据库供应商就可以提供自己的驱动程序,将其插入到驱动管理器中。这将成为一种向驱动管理器注册第三方驱动程序的简单机制

这种接口组织方式遵循了微软公司非常成功的 ODBC 模式, ODBC 为 C 语言访问数据库提供了一套编程接口。 JDBC 和 ODBC 都基于同一个思想:根据 API 编写的程序都可以与驱动管理器进行通信,而驱动管理器则通过驱动程序与实际的数据库进行通信

所有这些都意味着 JDBC API 是大部分程序员不得不使用的接口

JDBC 驱动程序类型

JDBC 规范将驱动程序归结为以下几类:

  • 第一类驱动程序将 JDBC 翻译成 ODBC ,然后使用一个 ODBC 驱动程序与数据库进行通信。较早版本的 Java 包含了一个这样的驱动程序: JDBC/ODBC 桥,不过在使用这个桥接器之前需要对 ODBC 进行相应的部署和正确的设置。 JDBC 面世之初,桥接器可以方便地用于测试,却不太适用于产品的开发。 Java 8 已经不再提供 JDBC/ODBC 桥了
  • 第二类驱动程序是由部分 Java 程序和部分本地代码组成的,用于与数据库的客户端 API 进行通信。在使用这种驱动程序之前,客户端不仅需要安装 Java 类库,还需要安装一些与平台相关的代码
  • 第三类驱动程序是纯 Java 客户端类库,它使用一种与具体数据库无关的协议将数据库请求发送给服务器构件,然后该构件再将数据库请求翻译成数据库相关的协议。这简化了部署,因为平台相关的代码只位于服务器端
  • 第四类驱动程序是纯 Java 类库,它将 JDBC 请求直接翻译成数据库相关的协议

大部分数据库供应商都为他们的产品提供第三类或第四类驱动程序

JDBC 最终是为了实现以下目标:

  • 通过使用标准的 SQL 语句,甚至是专门的 SQL 扩展,程序员就可以利用 Java 语言开发访问数据库的应用,同时还依旧遵守 Java 语言的相关约定
  • 数据库供应商和数据库工具开发商可以提供底层的驱动程序。因此,他们可以优化各自数据库产品的驱动程序

JDBC 的典型用法

JDBC 驱动程序应该部署在客户端

img

结构化查询语言( SQL )

JDBC 使得我们可以通过 SQL 与数据库进行通信

我们可以将 JDBC 包看作是一个用于将 SQL 语句传递给数据库的应用编程接口( API )

JDBC 配置

数据库 URL

JDBC 使用了一种与普通 URL 相类似的语法来描述数据源 下面是这种语法的两个实例:

jdbc:derby://localhost:1527/COREJAVA;create=true
jdbc:postgresql:COREJAVA

JDBC URL 般语法为:

jdbc:subprotocol:other stuff

其中, subprotocol 用于选择连接到数据库的具体驱动程序

other stuff 参数的格式随所使用的 subprotocol 不同而不同

驱动程序 JAR 文件

在运行访问数据库的程序时,需要将驱动程序的 JAR 文件包括到类路径中(编译时并不需要这个 JAR 文件)

注册驱动器类

许多 JDBC JAR 文件会自动注册驱动器类,在这种情况下,可以跳过手动注册步骤。包含 META-INF/services/java.sql.Driver 文件的 JAR 文件可以自动注册驱动器类,解压缩驱动程序 JAR 文件就可以检查其是否包含该文件

自动注册对于遵循 JDBC4 的驱动程序是必须具备的特性,参看 官方文档

如果驱动程序 JAR 文件不支持自动注册,那就需要找出数据库提供商使用的 JDBC 驱动器类的名字。典型的驱动器名字如下:

com.mysql.cj.jdbc.Driver

调试与 JDBC 相关的问题时,有种方法是启用 JDBC 的跟踪特性。调用 DriverManager.setLogWriter 方法可以将跟踪信息发送给 PrintWriter ,而 PrintWriter 将输出 JDBC 活动的详细列表

PrintWriter printWriter = new PrintWriter(System.out);
DriverManager.setLogWriter(printWriter);

Connection connection = DriverManager.getConnection(url, username, password);

示例程序:

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

public class TestDB {
    public static void main(String args[]) throws IOException {
        try {
            runTest();
        } catch (SQLException ex) {
            for (Throwable t : ex) {
                t.printStackTrace();
            }
        }
    }


    public static void runTest() throws SQLException, IOException {

        try (Connection conn = getConnection();
             Statement stat = conn.createStatement()) {
            stat.executeUpdate("CREATE TABLE Greetings (Message CHAR(20))");
            stat.executeUpdate("INSERT INTO Greetings VALUES ('Hello, World!')");

            try (ResultSet result = stat.executeQuery("SELECT * FROM Greetings")) {
                if (result.next()) {
                    System.out.println(result.getString(1));
                }
            }
            stat.executeUpdate("DROP TABLE Greetings");
        }
    }

    public static Connection getConnection() throws SQLException, IOException {
        Properties props = new Properties();
        try (InputStream in = Files.newInputStream(Paths.get("database.properties"))) {
            props.load(in);
        }
        String drivers = props.getProperty("jdbc.drivers");
        if (drivers != null) {
            System.setProperty("jdbc.drivers", drivers);
        }
        String url = props.getProperty("jdbc.url");
        String username = props.getProperty("jdbc.username");
        String password = props.getProperty("jdbc.password");

        PrintWriter printWriter = new PrintWriter(System.out);
        DriverManager.setLogWriter(printWriter);

        Connection connection = DriverManager.getConnection(url, username, password);

        return connection;
    }
}
java.sql.DriverManager 方法名称 方法声明 描述
getConnection public static Connection getConnection(String url, String user, String password) throws SQLException 建立一个到指定数据库的连接,并返回一个 Connection 对象

使用 JDBC 语句

执行 SQL 语句

在执行 SQL 语句之前,首先需要创建一个 Statement 对象。要创建 Statement 对象,需要使用调用 DriverManager.getConnection 方法所获得的 Connection 对象

Statement stat = conn.createStatement()

Statement 对象的 executeUpdate 方法将返回受 SQL 语句影响的行数,或者对不返回行数的语句返回 0

executeUpdate 法既可以执行诸如 INSERTUPDATEDELETE 类的操作,也可以执行诸如 CREATE TABLEDROP TABLE 类的数据定义语句 但是,执行 SELECT 查询时必须使用 executeQuery 方法.另外还有一个 execute 语句可以执行任意的 SQL 语句,此方法通常只用于由用户提供的交互式查询

当我们执行查询操作时,通常感兴趣的是查询结果。 executeQuery 方法会返回 ResultSet 类型的对象,可以通过它来每次一行地迭代遍历所有查询结果

try (ResultSet result = stat.executeQuery("SELECT * FROM Greetings")) {
    while (result.next()) {
        System.out.println(result.getString(1));
    }
}

ResultSet 接口的迭代协议与 java.util.Iterator 接口稍有不同。对于 ResultSet 接口,迭代器初始化时被设定在第一行之前的位置,必须调用 next 方法将它移动到第一行。另外,它没有 hasNext 方法,我们需要不断地调用 next 直至该方法返回 false

结果集中行的顺序是任意排列的。除非使用 ORDER BY 子句指定行的顺序,否则不能为行序强加任何意义

查看每一行时,可能希望知道其中每一列的 容,有许多访问器( accessor )方法( getXxx )可以用于获取这些信息。不同的数据类型有不同的访问器,比如 getStringgetDouble 。每个访问器都有两种形式,一种接受数字型参数,另一种接受字符串参数。当使用数字型参数时,我们指的是该数字所对应的列。例如, rs.getString(1) 返回的是当前行中第一列的值

与数组的索引不同,数据库的列序号是从 1 开始计算的

当使用字符串参数时,指的是结果集中以该字符串为列名的列。例如, rs.getDouble("Price") 返回列名为 Price 的列所对应的值。使用数字型参数效率更高一些,但是使用字符串参数可以使代码易于阅读和维护

get 方法的类型和列的数据类型不一致时,每个 get 方法都会进行合理的类型转换。例如,调用 rs.getString("Price") 时,该方法会将 Price 列的浮点值转换成字符串

java.sql.Connection 方法名称 方法声明 描述
createStatement Statement createStatement() throws SQLException; 创建一个 Statement 对象,用以执行不带参数的 SQL 查询和更新
close void close() throws SQLException; 立即关闭当前的连接,并释放由它所创建的 JDBC 资源
java.sql.Statement 方法名称 方法声明 描述
executeQuery ResultSet executeQuery(String sql) throws SQLException; 执行给定字符串中的 SQL 语句,并返回一个用于查看查询结果的 ResultSet 对象
executeUpdate
executeLargeUpdate
int executeUpdate(String sql) throws SQLException;
default long executeLargeUpdate(String sql) throws SQLException
执行字符串中指定的 INSERTUPDATEDELETE 等 SQL 语句。还可以执行数据定义语言( Data Definition Language, DDL )的语句,如 CREATE TABLE 。返回受影响的行数,如果是没有更新计数的语句,则返回 0
execute boolean execute(String sql) throws SQLException; 执行字符串中指定的 SQL 语句。可能会产生多个结果集和更新计数。如果第一个执行结果是结果集,则返回 true ;反之,返回 false 。调用 getResultSetgetUpdateCount 方法可以得到第一个执行结果
getResultSet ResultSet getResultSet() throws SQLException; 返回前一条查询语句的结果集。如果前一条语句未产生结果集,则返回 null 。对于每一条执行过的语句,该方法只能被调用一次
getUpdateCount
getLargeUpdateCount
int getUpdateCount() throws SQLException;
default long getLargeUpdateCount() throws SQLException
返回受前一条更新语句影响的行数 如果前一条语句未更新数据库,则返回 -1 。对于每一条执行过的语句,该方法只能被调用一次
close void close() throws SQLException; 关闭 Statement 对象以及它所对应的结果集
isClosed boolean isClosed() throws SQLException; 如果语句被关闭, 返回 true
closeOnCompletion public void closeOnCompletion() throws SQLException; 使得一旦该语句的所有结果集都被关闭,关闭该语句
java.sql.ResultSet 方法名称 方法声明 描述
next boolean next() throws SQLException; 将结果集中的当前行向前移动一行。如果已经到达最后一行的后面, 返回 false 。注意,初始情况下必须调用该方法才能转到第一行
getXxx
getObjec
updateObject
Xxx getXxx(int columnNumber)
Xxx getXxx(String columnLabel)
public <T> T getObject(int columnIndex, Class<T> type) throws SQLException;
public <T> T getObject(String columnLabel, Class<T> type) throws SQLException;
default void updateObject(int columnIndex, Object x, SQLType targetSqlType) throws SQLException
default void updateObject(String columnLabel, Object x, SQLType targetSqlType) throws SQLException
Xxx 指数据类型,例如 intdoubleStringDate 。用给定的列序号或列标签返回或更新该列的值,并将值转换成指定的类型。列标签是 SQL 的 AS 子句中指定的标签,在没有使用 AS 时,它就是列名
findColumn int findColumn(String columnLabel) throws SQLException; 根据给定的列名,返回该列的序号
close void close() throws SQLException; 立即关闭当前的结果集
isClosed boolean isClosed() throws SQLException; 如果该语句被关闭, 返回 true

管理连接、语句和结果集

每个 Connection 对象都可以创建一个或多个 Statement 对象。同一个 Statement 对象可以用于多个不相关的命令和查询。但是,一个 Statement 对象最多只能有一个打开的结果集。如果需要执行多个查询操作,且需要同时分析查询结果,那么必须创建多个 Statement 对象

需要说明的是,至少有一种常用的数据库( Microsoft SQL Server )的 JDBC 驱动程序只允许同时存在一个活动的 Statement 对象。使用 DatabaseMetaData 接口中的 getMaxStatements 方法可以获取 JDBC 驱动程序支持的同时活动的语句对象的总数

实际上,我们通常并不需要同时处理多个结果集。如果结果集相互关联,我们可以使用组合查询,这样就只需要分析一个结果。对数据库进行组合查询比使用 Java 程序遍历多个结果集要高效得多

使用完 ResultSetStatementConnection 对象后,应立即调用 close 方法。这些对象都使用了规模较大的数据结构,它们会占用数据库存服务器上的有限资源

如果 Statement 对象上有一个打开的结果集,那么调用 close 方法将自动关闭该结果集。同样地,调用 Connection 类的 close 方法将关闭该连接上的所有语句。

反过来的情况是,在使用 Java 7 时,可以 Statement 上调用 closeOnCompletion 方法,在其所有结果集都被关闭后,该语句会立即被自动关闭

如果所用连接都是短时的,那么无需考虑关闭语句和结果集。只需将 close 语句放在带资源的 try 语句中,以便确保最终连接对象不可能继续保持打开状态

分析 SQL 异常( SQLException

每个 SQLException 都有一个由多个 SQLException 对象构成的链,这些对象可以通过 getNextException 方法获取。这个异常链是每个异常都具有的由 Throwable 对象构成的“成因”链之外的异常链,因此,我们要用两个嵌套的循环来完整枚举所有的异常。幸运的是, Java 6 改进了 SQLException ,让其实现了 Iterable<Throwable> 接口,其 iterator() 方法可以产生一个 Iterator<Throwable> ,这个迭代器可以迭代这两个链,首先迭代第一个 SQLException 的成因链,然后迭代下一个 SQLException ,以此类推

for (Throwable t : sqlException) {
    // do something
}

可以在 SQLException 上调用 getSQLStategetErrorCode 方法来进一步分析它,其中第一个方法将产生符合 X/Open 或 SQL:2003 标准的字符串(调用 DatabaseMetaData 接口的 getSQLStateType 方法可以查出驱动程序所使用的标准)。 而错误代码是与具体的提供商相关的

SQL 异常按照层次结构树的方式组织到了一起,这使得我们可以按照与提供商无关的方式来捕获具体的错误类型

img

另外,数据库驱动程序可以将非致命问题作为警告报告,我们可以从连接、语句和结果中获取这些警告。SQLWarning 类是 SQLException 的子类(尽管 SQLWarning 不会被当作异常抛出),可以调用 getSQLStategetErrorCode 来获取有关警告的更多信息

与 SQL 异常类似,警告也是串成链的 要获得所有的警告,可以使用下面的循环:

SQLWarning w = stat.getWarning();
while (w != null) {
    // do something with w
    w = w.getNextWarning();
}

当数据从数据库中读出并意外被截断时, SQLWarningDataTruncation 子类就派上用场了。如果数据截断发生在更新语句中,那么 DataTruncation 将会被当作异常抛出

java.sql.SQLException 方法名称 方法声明 描述
getNextException public SQLException getNextException() 返回链接到该 SQL 异常的下一个 SQL 异常,或者在到达链尾时返回 null
iterator public Iterator<Throwable> iterator() 获取迭代器,可以迭代链接的 SQL 异常和它们的成因
getSQLState public String getSQLState() 获取 “SQL 状态”,即标准化的错误代码
getErrorCode public int getErrorCode() 获取提供商相关的错误代码
java.sql.SQLWarning 方法名称 方法声明 描述
getNextWarning public SQLWarning getNextWarning() 返回链接到该警告的下一个警告,或者在到达链尾时返回 null
java.sql.Connection 方法名称 方法声明 描述
getWarnings SQLWarning getWarnings() throws SQLException; 返回未处理警告中的第一个,或者在没有未处理警告时返回 null

java.sql.Statementjava.sql.ResultSet 具有相同的 getWarnings 方法

java.sql.DataTruncation 方法名称 方法声明 描述
getParameter public boolean getParameter() 如果在参数上进行了数据截断,则返回 true ;如果在列上进行了数据截断,则返回 false
getIndex public int getIndex() 返回被截断的参数或列的索引
getDataSize public int getDataSize() 返回应该被传输的字节数量,或者在该值未知的情况下返回 -1
getTransferSize public int getTransferSize() 返回应该被传输的字节数量,或者在该值未知的情况下返回 -1

执行查询操作

预备语句( PreparedStatement

预备语句( prepared statement )

准备一个带有宿主变量的查询语句,每次查询时只需为该变量填入不同的字符串就可以反复多次使用该语句。这一技术改进了查询性能,每当数据库执行一个查询时,它总是首先通过计算来确定查询策略,以便高效地执行查询操作。通过事先准备好查询并多次重用它,我们就可以确保查询所需的准备步骤只被执行一次

在预备查询语句中,每个宿主变量都用 ? 来表示。如果存在一个以上的变 ,那么在设置变量值时必须注意 ? 的位置

在执行预备语句之前,必须使用 set 方法将变量绑定到实际的值上。和 ResultSet 接口中的 get 方法类似 ,针对不同的数据类型也有不同的 set 方法,位置 1 表示第一个 ?

stat.setDouble(1, priceChange);

如果想要重用已经执行过的预备查询语句,那么除非使用 set 方法或调用 clearParameters 方法,否则所有宿主变量的绑定都不会改变。这就意味着,在从一个查询到另一个查询的过程中,只需使用 setXxx 方法重新绑定那些需要改变的变量即可

提示:通过连接字符串来手动构建查询显得非常枯燥乏味,而且存在潜在的危险。你必须注意像引号这样的特殊字符,而且如果查询中涉及用户的输入,那就还需要警惕注入攻击。因此,只有查询涉及变量时,才应该使用预备语句

注意:在相关的 Connection 对象关闭之后, PreparedStatement 对象也就变得无效了。不过,许多数据库通常都会自动缓存预备语句。如果相同的查询被预备两次,数据库通常会直接重用查询策略。因此,无需过多考虑调用 prepareStatement 的开销

提示:许多程序员都不喜欢使用如此复杂的 SQL 语句。比较常见的方法是使用大量的 Java 代码来迭代多个结果集,但是这种方法效率非常低。通常,使用数据库的查询代码要比使用 Java 程序好得多一一这是数据库的一个重要优点。一般而言,可以使用 SQL 解决的问题,就不要使用 Java 程序

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Scanner;


public class QueryTest {
    private static final String allQuery = "SELECT Books.Price, Books.Title FROM Books";

    private static final String authorPublisherQuery = "SELECT Books.Price, Books.Title"
            + " FROM Books, BooksAuthors, Authors, Publishers"
            + " WHERE Authors.Author_Id = BooksAuthors.Author_Id AND BooksAuthors.ISBN = Books.ISBN"
            + " AND Books.Publisher_Id = Publishers.Publisher_Id AND Authors.Name = ?"
            + " AND Publishers.Name = ?";

    private static final String authorQuery
            = "SELECT Books.Price, Books.Title FROM Books, BooksAuthors, Authors"
            + " WHERE Authors.Author_Id = BooksAuthors.Author_Id AND BooksAuthors.ISBN = Books.ISBN"
            + " AND Authors.Name = ?";

    private static final String publisherQuery
            = "SELECT Books.Price, Books.Title FROM Books, Publishers"
            + " WHERE Books.Publisher_Id = Publishers.Publisher_Id AND Publishers.Name = ?";


    private static final String priceUpdate = "UPDATE Books " + "SET Price = Price + ? "
            + " WHERE Books.Publisher_Id = (SELECT Publisher_Id FROM Publishers WHERE Name = ?)";

    private static Scanner in;
    private static ArrayList<String> authors = new ArrayList<>();
    private static ArrayList<String> publishers = new ArrayList<>();

    public static void main(String[] args) throws IOException {
        try (Connection conn = getConnection()) {
            in = new Scanner(System.in);
            authors.add("Any");
            publishers.add("Any");
            try (Statement stat = conn.createStatement()) {
                // Fill the authors array list
                String query = "SELECT Name FROM Authors";
                try (ResultSet rs = stat.executeQuery(query)) {
                    while (rs.next()) {
                        authors.add(rs.getString(1));
                    }
                }

                // Fill the publishers array list
                query = "SELECT Name FROM Publishers";
                try (ResultSet rs = stat.executeQuery(query)) {
                    while (rs.next()) {
                        publishers.add(rs.getString(1));
                    }
                }
            }
            boolean done = false;
            while (!done) {
                System.out.print("Q)uery C)hange prices E)xit: ");
                String input = in.next().toUpperCase();
                if (input.equals("Q")) {
                    executeQuery(conn);
                } else if (input.equals("C")) {
                    changePrices(conn);
                } else {
                    done = true;
                }
            }
        } catch (SQLException e) {
            for (Throwable t : e) {
                System.out.println(t.getMessage());
            }
        }
    }

    private static void executeQuery(Connection conn) throws SQLException {
        String author = select("Authors:", authors);
        String publisher = select("Publishers:", publishers);
        PreparedStatement stat;
        if (!author.equals("Any") && !publisher.equals("Any")) {
            stat = conn.prepareStatement(authorPublisherQuery);
            stat.setString(1, author);
            stat.setString(2, publisher);
        } else if (!author.equals("Any") && publisher.equals("Any")) {
            stat = conn.prepareStatement(authorQuery);
            stat.setString(1, author);
        } else if (author.equals("Any") && !publisher.equals("Any")) {
            stat = conn.prepareStatement(publisherQuery);
            stat.setString(1, publisher);
        } else {
            stat = conn.prepareStatement(allQuery);
        }

        try (ResultSet rs = stat.executeQuery()) {
            while (rs.next()) {
                System.out.println(rs.getString(1) + ", " + rs.getString(2));
            }
        }
    }


    public static void changePrices(Connection conn) throws SQLException {
        String publisher = select("Publishers:", publishers.subList(1, publishers.size()));
        System.out.print("Change prices by: ");
        double priceChange = in.nextDouble();
        PreparedStatement stat = conn.prepareStatement(priceUpdate);
        stat.setDouble(1, priceChange);
        stat.setString(2, publisher);
        int r = stat.executeUpdate();
        System.out.println(r + " records updated.");
    }


    public static String select(String prompt, List<String> options) {
        while (true) {
            System.out.println(prompt);
            for (int i = 0; i < options.size(); i++) {
                System.out.printf("%2d) %s%n", i + 1, options.get(i));
            }
            int sel = in.nextInt();
            if (sel > 0 && sel <= options.size()) {
                return options.get(sel - 1);
            }
        }
    }


    public static Connection getConnection() throws SQLException, IOException {
        Properties props = new Properties();
        try (InputStream in = Files.newInputStream(Paths.get("database.properties"))) {
            props.load(in);
        }

        String drivers = props.getProperty("jdbc.drivers");
        if (drivers != null) {
            System.setProperty("jdbc.drivers", drivers);
        }
        String url = props.getProperty("jdbc.url");
        String username = props.getProperty("jdbc.username");
        String password = props.getProperty("jdbc.password");

        return DriverManager.getConnection(url, username, password);
    }
}

读写 LOB

在 SQL 中,二进制大对象称为 BLOB ,字符型大对象称为 CLOB

要读 LOB ,需要执行 SELECT 语句,然后在 ResultSet 上调 getBlobgetClob 方法,这样就可以获得 BlobClob 类型的对象。要从 Blob 中获取二进制数据,可以调用 getBytesgetBinaryStream 。如果获取了 Clob 对象,那么就可以通过调用 getSubStringgetCharacterStream 方法来获取其中的字符数据

要将 LOB 置于数据库中,需要在 Connection 对象上调用 createBlobcreateClob ,然后获取一个用于该 LOB 的输出流或写出器,写出数据,并将该对象存储到数据库中

Blob coverBlob = connection.createBlob();
int offset = 0;
OutputStream out = coverBlob.setBinaryStream(offset);
ImageIO.write(coverImage, "PNG", out);
PreparedStatement stat = connection.prepareStatement("INSERT INTO Cover VALUES (?, ?)");
stat.setInt(1, isbn);
stat.setBlob(2, coverBlob);
java.sql.ResultSet 方法名称 方法声明 描述
getBlob
getClob
Blob getBlob(int columnIndex) throws SQLException;
Blob getBlob(String columnLabel) throws SQLException;
Clob getClob(int columnIndex) throws SQLException;
Clob getClob(String columnLabel) throws SQLException;
获取给定列的 BLOBCLOB
java.sql.Blob 方法名称 方法声明 描述
length long length() throws SQLException; 获取该 BLOB 的长度
getBytes byte[] getBytes(long pos, int length) throws SQLException; 获取该 BLOB 中给定范围的数据
getBinaryStream InputStream getBinaryStream () throws SQLException;
InputStream getBinaryStream(long pos, long length) throws SQLException;
返回一个输入流,用于读取该 BLOB 中全部或给定范围的数据
setBinaryStream OutputStream setBinaryStream(long pos) throws SQLException; 返回一个输出流,用于从给定位置开始写入该 BLOB
java.sql.Clob 方法名称 方法声明 描述
length long length() throws SQLException; 获取该 CLOB 中的字符总数
getSubString String getSubString(long pos, int length) throws SQLException; 获取该 CLOB 中给定范围的字符
getCharacterStream Reader getCharacterStream() throws SQLException;
Reader getCharacterStream(long pos, long length) throws SQLException;
返回一个读入器(而不是流),用于读取 CLOB 中全部或给定范围的数据
setCharacterStream Writer setCharacterStream(long pos) throws SQLException; 返回一个写出器(而不是流),用于从给定位置开始写入该 CLOB
java.sql.Connection 方法名称 方法声明 描述
createBlob
createClob
Blob createBlob() throws SQLException;
Clob createClob() throws SQLException;
创建一个空的 BLOBCLOB

SQL 转义

“转义”语法是各种数据库普遍支持的特性,但是数据库使用的是与数据库相关的语法变体,因此,将转义语法转译为特定数据库的语法是 JDBC 驱动程序的任务之一

转义主要用于下列场景:

  • 日期和时间字面常量
  • 调用标量函数
  • 调用存储过程
  • 外连接
  • LIKE 子句中的转义字符

日期和时间字面常量随数据库的不同而变化很大。要嵌入日期或时间字面常量,需要按照 ISO 8601 格式 指定它的值,之后驱动程序会将其转译为本地格式。应该使用 dtts 来表示 DATETIMETIMESTAMP 值:

{d '2008-01-24'} 
{t '23:59:59’}
{ts '2008-01-24 23:59:59.999'}

标量函数( scalar function ) 是指仅返回单个值的函数。在数据库中包含大量的函数,但是不同的数据库中这些函数名存在着差异。JDBC 规范提供了标准的名字,并将其转译为数据库相关的名字。要调用函数,需要像下面这样嵌入标准的函数名和参数:

{fn left(?, 20)} 
{fn user()}

在 JDBC 规范中可以找到它支持的函数名的完整列表

存储过程(stored procedure ) 是在数据库中执行的用数据库相关的语言编写的过程。要调用存储过程,需要使用 call 转义命令,在存储过程没有任何参数时,可以不用加上括号外,应该用 = 来捕获存储过程的返回值:

{call PROC1(?, ?)} 
{call PROC2} 
{call ?= PROC3(?)}

两个表的 外连接( outer join ) 并不要求每个表的所有行都要根据连接条件进行匹配。LEFT OUTER JOINRIGHT OUTER JOINFULL OUTER JOIN ,由于并非所有的数据库对于这些连接都使用标准的写法,因此需要使用转义语法

_% 字符在 LIKE 子句中具有特殊含义,用来匹配一个字符或一个字符序列。目前并不存在任何在宇面上使用它们的标准方式,所以如果想要匹配所有包含 _ 字符的字符串,就必须使用下面的结构:

... WHERE ? LIKE %!_% {escape '!'}

这里我们将 ! 定义为转义字符,而 !_ 组合表示字面常量下划线

多结果集

在执行存储过程,或者在使用允许在单个查询中提交多个 SELECT 语句的数据库时,一个查询有可能会返回多个结果集。下面是获取所有结果集的步骤:

  1. 使用 execute 方法来执行 SQL 语句
  2. 获取第一个结果集或更新计数
  3. 重复调用 getMoreResults 方法以移动到下一个结果集
  4. 当不存在更多的结果集或更新计数时,完成操作

如果由多结果集构成的链中的下一项是结果集, executegetMoreResults 方法将返回 true ,而如果在链中的下一项不是更新计数, getUpdateCount 方法将返回 -1

boolean isResult = stat.execute(command);
boolean done = false;
while (!done) {
    if (isResult) {
        ResultSet resultSet = stat.getResultSet();
        // do something
    } else {
        int updateCount = stat.getUpdateCount();
        if (updateCount >= 0) {
            // do something
        } else {
            done = true;
        }
    }
    if (!done) {
        isResult = stat.getMoreResults();
    }
}
java.sql.Statement 方法名称 方法声明 描述
getMoreResults boolean getMoreResults() throws SQLException;
boolean getMoreResults(int current) throws SQLException;
获取该语句的下一个结果集, current 参数是 Statement.CLOSE_CURRENT_RESULT (默认值),KEEP_CURRENT_RESULTCLOSE_ALL_RESULTS 之一。如果存在下一个结果集,并且它确实是一个结果集 ,则返回 true

获取自动生成的键

大多数数据库都支持某种在数据库中对行自动编号的机制。但是,不同的提供商所提供的机制之间存在着很大的差异, 这些自动编号的值经常用作主键。尽管 JDBC 没有提供独立于提供商的自动生成键的解决方案,但是它提供了获取自动生成键的有效途径。当我们向数据表中插入一个新行,且其键自动生成时 ,可以用下面的代码来获取这个键

stat.executeUpdate(insertStatment, Statement.RETURN_GENERATED_KEYS);
ResultSet rs = stat.getGeneratedKeys();
if (rs.next()) {
    int key = rs.getInt(1);
    // do something
}
java.sql.Statement 方法名称 方法声明 描述
execute
executeUpdate
boolean execute(String sql, int autoGeneratedKeys) throws SQLException;
int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException;
像前面描述的那 行给定 SQL 语句,如果 autoGeneratedKeys 被设置为 Statement.RETURN GENERATED_KEYS ,并且该语句是一条 INSERT 语句, 那么第一列中就是自动生成的键

可滚动和可更新的结果集

使用 ResultSet 接口中 next 方法可以迭代遍历结果集中的所有行

对于可滚动结果集而言,可以在其中向前或向后移动,甚至可以跳到任意位置

在可更新的结果集中,可以以编程方式来更新其中的项,使得数据库可以自动更新数据

可滚动的结果集

默认情况下,结果集是不可滚动和不可更新的 。为了从查询中获取可滚动的结果集,必须使用下面的方法得到一个不同的 Statement 对象:

Statement stat = conn.createStatement(resultSetType, resultSetConcurrency);
PreparedStatement preparedStatement = conn.prepareStatement(sql, resultSetType, resultSetConcurrency);

resultSetTyperesultSetConcurrency 取值来自 ResultSet 定义的常量

resultSetType 描述
TYPE_FORWARD_ONLY 结果集不能滚动(默认值)
TYPE_SCROLL_INSENSITIVE 结果集可以滚动,但对数据库变化不敏感
TYPE_SCROLL_SENSITIVE 结果集可以滚动,且对数据库变化敏感
resultSetConcurrency 描述
CONCUR_READ_ONLY 结果集不能用于更新数据库 (默认值)
CONCUR_UPDATABLE 结果集可以用于更新数据库

如果只想滚动遍历结果集,而不想编辑它的数据,那么可以使用以下语句:

Statement stat = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
ResultSet rs = stat.executeQuery(query);

现在,获得的所有结果集都将是可滚动的。可滚动的结果集有一个游标,用以指示当前位置。

注意: 并非所有的数据库驱动程序都支持可滚动和可更新的结果集。(使用 DatabaseMetaData 接口中 supportsResultSetTypesupportsResultSetConcurrency 方法,我们可以获知在使用特定的驱动程序时,某个数据库究竟支持哪些结采集类型以及哪些并发模式 )。即使是数据库支持所有的结果集模式,某个特定的查询也可能无法产生带有所请求的所有属性的结果集。(例如,一个复杂查询的结果集就有可能是不可更新的结果集 )在这种情况下,executeQuery 方法将返回一个功能较少的 ResultSet 对象,并添加 SQLWarning 到连接对象中。或者,也可以使用 ResultSet 中的 getTypegetConcurrency 方法查看结果集实际支持的模式。如果不检查结果集的功能就发起一个不支持的操作, 如对不可滚动的结果集调用 previous 方法,那么程序将抛出一个 SQLException 异常

DatabaseMetaData metaData = connection.getMetaData();

boolean b1 = metaData.supportsResultSetType(ResultSet.TYPE_FORWARD_ONLY);
boolean b2 = metaData.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);
boolean b3 = metaData.supportsResultSetType(ResultSet.TYPE_SCROLL_SENSITIVE);
System.out.println("supportsResultSetType :: " + b1 + " , " + b2 + " , " + b3);

boolean b11 = metaData.supportsResultSetConcurrency(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
boolean b12 = metaData.supportsResultSetConcurrency(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
boolean b21 =
        metaData.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
boolean b22 =
        metaData.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
boolean b31 =
        metaData.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
boolean b32 =
        metaData.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
System.out.println(
        "supportsResultSetConcurrency :: " + b11 + " , " + b12 + " , " + b21 + " , " + b22 + " , " + b31 + " , "
                + b32);

结果集向后滚动:

if (rs.previous()) {
    // do something
}

如果游标位于一个实际的行上,那么该方法将返回 true ;如果游标位于第一行之前,那么返回 false

将游标向后或向前移动多行:

rs.relative(n);

如果 n 为正数,游标将向前移动。如果 n 为负数,游标将向后移动。如果 n 为 0 ,那么调用该方法将不起任何作用。如果试图将游标移动到当前行集的范围之外, 根据 n 值的正负号,游标需要被设置在最后一行之后或第一行之前,那么,该方法将返回 false ,且不移动游标。如果游标位于一个实际的行上,那么该方法将返回 true

将游标设置到指定的行号上:

rs.absolute(n);

返回当前行的行号:

int currentRow = rs.getRow();

结果集中第一行的行号为 1 。如果返回值为 0 ,那么当前游标不在任何行上,它要么位于第一行之前,要么位于最后一行之后

firstlastbeforeFirstafterLast 这些简便方法用于将游标移动到第一行、最后一行、第一行之前或最后一行之后

isFirstisLastisBeforeFirstisAfterLast 用于测试游标是否位于这些特殊位置上

可更新的结果集

如果希望编辑结果集中的数据,并且将结果集上的数据变更自动反映到数据库中,那么就必须使用可更新的结果集 可更新的结果集并非必须是可滚动的,但如果将数据提供给用户去编辑,那么通常也会希望结果集是可滚动的

获得可更新的结果集:

Statement stat = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
ResultSet rs = stat.executeQuery(sql);

并非所有的查询都会返回可更新的结果集。 如果查询涉及多个表的连接操作,那么它所产生的结果集将是不可更新的。如果查询只涉及一个表,或者在查询时是使用主键连接多个表的,那么它所产生的结果集将是可更新的结果集。可以调用 ResultSet 接口中的 getConcurrency 方法来确定结果集是否是可更新的

假设想提高某些图书的价格,但是在执行 UPDATE 语句时又没有一个简单而统一的提价标准。此时,就可以根据任意设定的条件,迭代遍历所有的图书并更新它们的价格

String query = "SELECT * FROM Books";
ResultSet rs = stat.executeQuery(query);
while (rs.next()) {
    if (...){
        double increase = ...
        double price = rs.getDouble("Price");
        rs.updateDouble("Price", price + increase);
        rs.updateRow(); // make su to call updateRow afte updating fie 1 ds
    }
}

所有对应于 SQL 类型的数据类型都配有 updateXxx 方法,比如 updateDoubleupdateString 等。与 getXxx 方法相同,在使用 updateXxx 方法时必须指定列的名称或序号。然后,你可以给该字段设置新的值

在使用第一个参数为列序号的 updateXxx 方法时,请注意这里的列序号指的是该列在结果集中的序号。它的值可以与数据库中的列序号不同

updateXxx 方法改变的只是结果集中的行值,而非数据库中的值。当更新完行中的字段值后,必须调用 updateRow 方法,这个方法将当前行中的所有更新信息发送给数据库。如果没有调用 updateRow 方法就将游标移动到其他行上,那么对此行所做的所有更新都将被丢弃,而且永远也不会被传递给数据库。还可以调用 cancelRowUpdates 方法来取消对当前行的更新

如果想在数据库中添加一条新的记录,首先需要使用 moveToInsertRow 方法将游标移动到特定的位置,我们称之为 插入行( insert row )。然后,调用 updateXxx 方法在插入行的位置上创建一个新的行。在上述操作全部完成之后,还需要调用 insertRow 方法将新建的行发送给数据库。完成插入操作后,再调用 moveToCurrentRow 方法将游标移回到调用 moveToInsertRow 方法之前的位置

rs.moveToInsertRow();
rs.updateString("Title", title);
rs.updateString("ISBN", isbn);
rs.updateString("Publisher_Id", pubid);
rs.updateDouble("Price", price);
rs.insertRow();
rs.moveToCurrentRow();

请注意,你无法控制在结果集或数据库中添加新数据的位置

对于在插入行中没有指定值的列,将被设置为 SQL 的 NULL 。但是,如果这个列有 NOT NULL 约束 ,那么将会抛出异常,而这一行也无法插入

删除游标所指的行:

rs.deleteRow();

deleteRow 方法会立即将该行从结果集和数据库中删除

ResultSet 接口中的 updateRowinsertRowdeleteRow 方法的执行效果等同于 SQL 命令中的 UPDATEINSERTDELETE

对大多数程序性的修改而言,使用 SQL 的 UPDATE 语句更合适一些

java.sql.Connection 方法名称 方法声明 描述
createStatement
prepareStatement
Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException;
PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException;
创建一个语句或预备语句,且该语句可以产生指定类型和并发模式的结果集
java.sql.ResultSet 方法名称 方法声明 描述
getType int getType() throws SQLException; 返回结果集的类型
getConcurrency int getConcurrency() throws SQLException; 返回结果集的并发设置
previous boolean previous() throws SQLException; 将游标移动到前一行。如果游标位于某一行上, 返回 true ;如果游标位于第一行之前的位置, 返回 false
getRow int getRow() throws SQLException; 得到当前行的序号 所有行从 1 开始编号
absolute boolean absolute( int row ) throws SQLException; 移动游标到第 row 行。如果游标位于某一行上, 返回 true
relative boolean relative( int rows ) throws SQLException; 将游标移动 row 行。如果 row 为负数,则游标向后移动。如果游标位于某一行上, 返回 true
first
last
boolean first() throws SQLException;
boolean last() throws SQLException;
移动游标到第一行或最后一行。如果游标位于某一行上, 返回 true
beforeFirst
afterLast
void beforeFirst() throws SQLException;
void afterLast() throws SQLException;
移动游标到第一行之前或最后一行之后的位置
isFirst
isLast
boolean isFirst() throws SQLException;
boolean isLast() throws SQLException;
测试游标是否在第一行或最后一行
isBeforeFirst
isAfterLast
boolean isBeforeFirst() throws SQLException;
boolean isAfterLast() throws SQLException;
测试游标是否在第一行之前或最后一行之后的位置
moveToInsertRow void moveToInsertRow() throws SQLException; 移动游标到插入行。插入行是一个特殊的行,可以在该行上使用 updateXxxinsertRow 方法来插入新数据
moveToCurrentRow void moveToCurrentRow() throws SQLException; 将游标从插入行移回到调用 moveToInsertRow 方法之前它所在的那一行
insertRow void insertRow() throws SQLException; 将插入行上的内容插入到数据库和结果集中
deleteRow void deleteRow() throws SQLException; 从数据库和结果集中删除当前行
updateXxx void updateXxx(int column, Xxx data)
void updateXxx(String columnName, Xxx data)
(Xxx 指数据类型,比如 intdoubleStringDate 等)更新结果中当前行上的某个字段值
updateRow void updateRow() throws SQLException; 将当前行的更新信息发送到数据库
cancelRowUpdates void cancelRowUpdates() throws SQLException; 撤销对当前行的更新
java.sql.DatabaseMetaData 方法名称 方法声明 描述
supportsResultSetType boolean supportsResultSetType(int type) throws SQLException; 如果数据库支持给定类型的结果集, 返回 true
supportsResultSetConcurrency boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException; 如果数据库支持给定类型和并发模式的结果集, 返回 true

行集( Rowset

可滚动的结果集虽然功能强大,却有一个重要的缺陷:在与用户的整个交互过程中,必须始终与数据库保持连接。这种方式存在很大的问题,因为数据库连接属于稀有资源。在这种情况下,我们可以使用行集 Rowset 接口扩展自 ResultSet 接口,却无需始终保持与数据库的连接

行集还适用于将查询结果移动到复杂应用的其他层,或者是诸如手机之类的其他设备中。你可能从未考虑过移动一个结果集,因为它的数据结构非常庞大,且依赖于数据连接

构建行集

javax.sql.rowset 提供的接口,它们都扩展了 RowSet 接口:

  • CachedRowSet 允许在断开连接的状态下执行相关操作
  • WebRowSet 对象代表了一个被缓存的行集,该行集可以保存为 XML 文件。该文件可以移动到 Web 应用的其他层中,只要在该层中使用另一个 WebRowSet 对象重新打开该文件即可
  • FilteredRowSetJoinRowSet 接口支持对行集的轻量级操作,它们等同于 SQL 中的 SELECTJOIN 操作。这两个接口的操作对象是存储在行集中的数据,因此运行时无需建立数据库连接
  • JdbcRowSetResultSet 接口的一个瘦包装器。它在 RowSet 接口中添加了有用的方法

一种获取行集的标准方式:

RowSetFactory factory = RowSetProvider.newFactory();
CachedRowSet crs = factory.createCachedRowSet();

获取其他行集类型的对象也有类似的方法

另外,JDK 在 com.sun.rowset 中还提供了参考实现 ,这些实现类的名字以 Impl 结尾,例如 CachedRowSetImpl 如果你无法使用 RowSetProvider ,那么可以使用下面的类取而代之:

CachedRowSet crs = new com.sun.rowset.CachedRowSetImpl();

被缓存的行集 ( CachedRowSet )

一个被缓存的行集中包含了一个结果集中所有的数据。CachedRowSetResultSet 接口的子接口,所以你完全可以像使用结果集一样来使用被缓存的行集。被缓存的行集有一个非常重要的优点:断开数据库连接后仍然可以使用行集。在执行每个用户命令时,我们只需打开数据库连接、执行查询操作、将查询结果放入被缓存的行集,然后关闭数据库连接即可。

我们甚至可以修改被缓存的行集的数据。当然,这些修改不会立刻反馈到数据库中。相反,必须发起一个显式的请求,以便让数据库真正接受所有修改。CachedRowSet 会重新连接到数据库,并通过执行 SQL 语句向数据库中写入所有修改后的数据

javax.sql.RowSet 方法名称 方法声明 描述
getUrl
setUrl
String getUrl() throws SQLException;
void setUrl(String url) throws SQLException;
获取或设置数据库的 URL
getUsername
setUsername
String getUsername();
void setUsername(String name) throws SQLException;
获取或设置连接数据库所需的用户名
getPassword
setPassword
String getPassword();
void setPassword(String password) throws SQLException;
获取或设置连接数据库所需的密码
getCommand
setCommand
String getCommand();
void setCommand(String cmd) throws SQLException;
获取或设置向行集中填充数据时需要执行的命令
execute void execute() throws SQLException; 通过执行使用 setCommand 方法设置的语句集来填充行集。为了使驱动管理器可以获得连接, 须事先设定 URL 用户名和密码

元数据

JDBC 还可以提供关于数据库及其表结构的详细信息

在 SQL 中,描述数据库或其组成部分的数据称为元数据(区别于那些存在数据库中的实际数据)。

我们可以获得三类元数据:

  • 关于数据库的元数据
  • 关于结果集的元数据
  • 关于预备语句参数的元数据

DatabaseMetaData 接口中有上百个方法可以用于查询数据库的相关信息,包括一些使用奇特的名字进行调用的方法

DatabaseMetaData 接口用于提供有关数据库的数据,第二个元数据接口 ResultSetMetaData 则用于提供结果集的相关信息。

java.sql.Connection 方法名称 方法声明 描述
getMetaData DatabaseMetaData getMetaData() throws SQLException; 返回 DatabaseMetaData 对象,该对象封装了有关数据库连接的元数据
java.sql.DatabaseMetaData 方法名称 方法声明 描述
getTables ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String types[]) throws SQLException; 返回某个目录( catalog )中所有表的描述,该目录必须匹配给定的模式( schema )、表名字模式以及类型标准 (模式用于描述一组相关的表和访问权限,而目录描述的是组相关的模式,这些概念对组织大型数据库非常重要)
getJDBCMajorVersion
getJDBCMinorVersion
int getJDBCMajorVersion() throws SQLException;
int getJDBCMinorVersion() throws SQLException;
返回建立数据库连接的 JDBC 驱动程序的主版本号和次版本号。例如, JDBC 4.2 的驱动程序有一个主版本号 4 和一个次版本号 2
getMaxConnections int getMaxConnections() throws SQLException; 返回可同时连接到数据库的最大并发连接数
getMaxStatements getMaxStatements 返回单个数据库连接允许同时打开的最大并发语句数 如果对允许打开的语句数目没有限制或者不可知, 返回 0
java.sql.ResultSet 方法名称 方法声明 描述
getMetaData ResultSetMetaData getMetaData() throws SQLException; 返回与当前 ResultSet 对象中的列相关的元数据
java.sql.ResultSetMetaData 方法名称 方法声明 描述
getColumnCount int getColumnCount() throws SQLException; 返回当前 ResultSet 对象中的列数
getColumnDisplaySize int getColumnDisplaySize(int column) throws SQLException; 返回给定列序号的列的最大宽度
getColumnLabel String getColumnLabel(int column) throws SQLException; 返回该列所建议的名称
getColumnName String getColumnName(int column) throws SQLException; 返回指定的列序号所对应的列名

事务

可以将一组语句构建成一个 事务( transaction )。当所有语句都顺利执行之后,事务可以被 提交( commit )。否则,如果其中某个语句遇到错误,那么事务将被回滚,就好像没有任何语句被执行过一样

将多个语句组合成事务的主要原因是为了确保 数据库完整性( database integrity )

对 JDBC 对事务编程

默认情况下,数据库连接处于 自动提交模式( autocommit mode )。每个 SQL 语句一旦被执行便被提交给数据库。一旦命令被提交,就无法对它进行回滚操作。在使用事务时, 需要关闭这个默认值:

conn.setAutoCommit(false);
// 创建一个语句对象:
Statement stat = conn.createStatement();

// 任意多次地调用 executeUpdate 方法:
stat.executeUpdate(command1);
stat.executeUpdate(command2);
stat.executeUpdate(command3);

// 如果执行了所有命令之后没有出错,则调用 commit 方法
conn.commit();

// 如果出现错误,则调用:
conn.rollback();

保存点

在使用某些驱动程序时,使用 保存点( save point ) 可以更细粒度地控制回滚操作。创建一个保存点意味着稍后只需返回到这个点,而非事务的开头

Statement stat = conn.createStatement();    // start transaction; rollback() goes here
stat.executeUpdate(command1);
Savepoint svpt = conn.setSavepoint();    // set savepoint; rollback(svpt) goes here
stat.executeUpdate(command2);

if (condition) {
    conn.rollback(svpt);
}

conn.commit();

当不再需要保存点时,必须释放它:

conn.releaseSavepoint(svpt);

批量更新

假设有一个程序需要执行许多 INSERT 语句,以便将数据填入数据库表中,此时可以使用批量更新的方法来提高程序性能。在使用 批量更新( batch update ) 时,一个语句序列作为一批操作将同时被收集和提交

使用 DatabaseMetaData 接口中的 supportsBatchUpdates 方法可以获知数据库是否支持这种特性

处于向一批中的语句可以是 INSERTUPDATEDELETE 等操作,也可以是数据库定义语句,如 CREATE TABLEDROP TABLE 。但是,在批量处理中添加 SELECT 语句会抛出异常

boolean autoCommit = conn.getAutoCommit();
conn.setAutoCommit(false);
Statement stat = conn.createStatement();

stat.addBatch(command);

while (condition) {
    // do something
    stat.addBatch(command);
}

// 为所有已提交的语句返回一个记录数的数组
int[] counts = stat.executeBatch();

conn.commit();
conn.setAutoCommit(autoCommit);
java.sql.Connection 方法名称 方法声明 描述
getAutoCommit
setAutoCommit
boolean getAutoCommit() throws SQLException;
void setAutoCommit(boolean autoCommit) throws SQLException;
获取该连接中的自动提交模式,或将其设置为 b 。如果自动更新为 true ,那么所有语句将在执行结束后立刻被提交
commit void commit() throws SQLException; 提交自上次提交以来所有执行过的语句
rollback void rollback() throws SQLException; 撤销自上次提交以来所有执行过的语句所产生的影响
setSavepoint Savepoint setSavepoint() throws SQLException;
Savepoint setSavepoint(String name) throws SQLException;
设置一个匿名或具名的保存点
rollback void rollback(Savepoint savepoint) throws SQLException; 回滚到给定保存点
releaseSavepoint void releaseSavepoint(Savepoint savepoint) throws SQLException; 释放给定的保存点
java.sql.Savepoint 方法名称 方法声明 描述
getSavepointId int getSavepointId() throws SQLException; 获取该匿名保存点的 ID 。如果该保存点具有名字, 抛出一个 SQLException 异常
getSavepointName String getSavepointName() throws SQLException; 获取该保存点的名称。如果该对象为匿名保存点,则抛出一个 SQLException 异常
java.sql.Statement 方法名称 方法声明 描述
addBatch void addBatch( String sql ) throws SQLException; 添加命令到该语句当前的批量命令中
executeBatch
executeLargeBatch
int[] executeBatch() throws SQLException;
default long[] executeLargeBatch() throws SQLException
执行当前批量更新中的所有命令。返回一个记录数的数组,其中每一个元素都对应一条语句,如果其值非负, 表示受该语句影响的记录总数;如果其值为 SUCCESS_NO_INFO 。表示该语句成功执行了,但没有记录数可用;如果其值为 EXECUTE_FAILED ,则表示该语句执行失败了
java.sql.DatabaseMetaData 方法名称 方法声明 描述
supportsBatchUpdates boolean supportsBatchUpdates() throws SQLException; 如果驱动程序支持批量更新,则返回 true

高级 SQL 类型

JDBC 支持的 SQL 数据类型以及它们在 Java 语言中对应的数据类型:

SQL 数据类型 Java 数据类型
INTEGERINT int
SMALLINT short
NUMERIC(m ,n) , DECIMAL(m ,n)DEC(m ,n) java.math.BigDecimal
FLOAT(n) double
REAL float
DOUBLE double
CHARACTER(n)CHAR(n) String
VARCHAR(n) , LONG VARCHAR String
BOOLEAN boolean
DATE java.sql.Date
TIME java.sql.Time
TIMESTAMP java.sql.Timestamp
BLOB java.sql.Blob
CLOB java.sql.Clob
ARRAY java.sql.Array
ROWID java.sql.RowId
NCHAR(n) , NVARCHAR(n) , LONG NVARCHAR String
NCLOB java.sql.NClob
SQLXML java.sql.SQLXML

Web 与企业应用中的连接管理

在 Web 或企业环境中部署 JDBC 应用时,数据库连接管理与 Java 名字和目录接口( JNDI )是集成在一起的。遍布企业的数据源的属性可以存储在一个目录中,采用这种方式使得可以集中管理用户名、密码、数据库名和 JDBC URL

使用 JNDI 服务来定位数据源,创建数据库连接:

Context jndiContext = new InitialContext();
DataSource source = (DataSource) jndiContext.lookup("java:comp/env/jdbc/corejava");
Connection conn = source.getConnection();

数据源就是一个能够提供简单的 JDBC 连接和更多高级服务的接口,比如执行涉及多个数据库的分布式事务。 javax.sql 标准扩展包定义了 DataSource 接口

在 Java EE 容器 ,甚至不必编程进行 JNDI 查找,只需在 DataSource 域上使用 Resource 注解,当加载应用时 ,这个数据源引用将被设置:

@Resource(name = "jdbc/corejava")
private DataSource source;

数据库连接是有限的资源,如果用户要离开应用一段时间,那么他占用的连接就不应该保持打开状态;另一方面,每次查询都获取连接并在随后关闭它的代价也是相当高的。

解决问题的方法是建立数据库 连接池( pool )。这意味着数据库连接在物理上并未被关闭,而是保留在一个队列中并被反复重用。连接池是一种非常重要的服务, JDBC 规范为实现者提供了用以实现连接池服务的手段。不过,JDK 本身并未实现这项服务,数据库供应商提供的 JDBC 驱动程序中通常也不包含这项服务。相反, Web 容器和应用服务器的开发商通常会提供连接池服务的实现

连接池的使用对程序员来说是完全透明的,可以通过获取数据源并调用 getConnection 方法来得到连接池中的连接。使用完连接后,需要调用 close 方法。该方法并不在物理上关闭连接,而只是告诉连接池已经使用完该连接。连接池通常还会将池机制作用于预备语句上

posted @ 2023-09-05 09:26  流星<。)#)))≦  阅读(23)  评论(0编辑  收藏  举报