Skip to content

Latest commit

 

History

History
834 lines (627 loc) · 38.3 KB

File metadata and controls

834 lines (627 loc) · 38.3 KB

十、管理数据库中的数据

本章解释并演示了如何使用 Java 应用管理(即,插入、读取、更新和删除)数据库中的数据。简要介绍了结构化查询语言SQL)和数据库的基本操作,包括如何连接数据库、如何创建数据库结构、如何用 SQL 编写数据库表达式以及如何执行这些表达式。

本章将讨论以下主题:

  • 创建数据库
  • 创建数据库结构
  • 连接到数据库
  • 释放连接
  • 对数据执行创建、读取、更新和删除(CRUD)操作

创建数据库

Java 数据库连接JDBC)是一种 Java 功能,允许您访问和修改数据库中的数据。它受 JDBC API(包括java.sqljavax.sqljava.transaction.xa包)以及实现数据库访问接口的数据库特定类(称为数据库驱动程序)的支持,该接口由每个数据库供应商提供。

使用 JDBC 意味着编写 Java 代码,使用 JDBC API 的接口和类以及特定于数据库的驱动程序来管理数据库中的数据,该驱动程序知道如何与特定数据库建立连接。使用这个连接,应用就可以发出用 SQL 编写的请求。

当然,我们这里只指理解 SQL 的数据库。它们被称为关系型或表格型数据库管理系统数据库管理系统),构成了目前使用的绝大多数数据库管理系统——尽管也使用了一些替代方法(例如导航数据库和 NoSQL)。

java.sqljavax.sql包包含在 Java 平台标准版Java SE)中。javax.sql包包含支持语句池、分布式事务和行集的DataSource接口

创建数据库包括以下八个步骤:

  1. 按照供应商的说明安装数据库
  2. 创建数据库用户、数据库、模式、表、视图、存储过程以及支持应用的数据模型所必需的任何其他内容
  3. 向该应用添加对具有特定于数据库的驱动程序的.jar文件的依赖关系
  4. 从应用连接到数据库
  5. 构造 SQL 语句
  6. 执行 SQL 语句
  7. 根据应用的需要使用执行结果
  8. 释放(即关闭)数据库连接和在该过程中打开的任何其他资源

步骤 1 到 3 仅在数据库设置期间和运行应用之前执行一次。应用根据需要重复执行步骤 4 到 8。实际上,步骤 5 到 7 可以在同一个数据库连接中重复多次。

对于我们的示例,我们将使用 PostgreSQL 数据库。您首先需要使用特定于数据库的说明自己执行步骤 1 到 3。要为演示创建数据库,我们使用以下命令:

create user student SUPERUSER;
create database learnjava owner student;

这些命令创建一个student用户,可以管理SUPERUSER数据库的所有方面,并使student用户成为learnjava数据库的所有者。我们将使用student用户访问和管理来自 Java 代码的数据。实际上,出于安全考虑,不允许应用创建或更改数据库表和数据库结构的其他方面。

此外,创建另一个名为纲要的逻辑层是一个很好的实践,它可以拥有自己的一组用户和权限。这样,可以隔离同一数据库中的多个模式,并且每个用户(其中一个是您的应用)只能访问某些模式。在企业级,通常的做法是为数据库模式创建同义词,以便任何应用都不能直接访问原始结构。然而,为了简单起见,我们在本书中不这样做。

创建数据库结构

创建数据库后,以下三条 SQL 语句将允许您创建和更改数据库结构。这是通过数据库实体完成的,例如表、函数或约束:

  • CREATE语句创建数据库实体
  • ALTER语句更改数据库实体
  • DROP语句删除数据库实体

还有各种 SQL 语句允许您查询每个数据库实体。这些语句是特定于数据库的,通常只在数据库控制台中使用。例如,在 PostgreSQL 控制台中,\d <table>可以用来描述一个表,而\dt列出了所有的表。有关详细信息,请参阅数据库文档

要创建表,可以执行以下 SQL 语句:

CREATE TABLE tablename ( column1 type1, column2 type2, ... ); 

表名、列名和可使用的值类型的限制取决于特定的数据库。下面是在 PostgreSQL 中创建person表的命令示例:

CREATE table person ( 
   id SERIAL PRIMARY KEY, 
   first_name VARCHAR NOT NULL, 
   last_name VARCHAR NOT NULL, 
   dob DATE NOT NULL );

SERIAL关键字表示该字段是一个连续整数,每次创建新记录时数据库都会生成该整数。生成顺序整数的其他选项有SMALLSERIALBIGSERIAL;它们的大小和可能值的范围不同:

SMALLSERIAL: 2 bytes, range from 1 to 32,767
SERIAL: 4 bytes, range from 1 to 2,147,483,647
BIGSERIAL: 8 bytes, range from 1 to 922,337,2036,854,775,807

PRIMARY_KEY关键字表示这将是记录的唯一标识符,很可能用于搜索。数据库为每个主键创建一个索引,以加快搜索过程。索引是一种数据结构,有助于加速表中的数据搜索,而不必检查每个表记录。索引可以包含一个表的一列或多列。如果您请求表的描述,您将看到所有现有的索引。

或者,我们可以使用first_namelast_namedob的组合来制作复合PRIMARY KEY关键字:

CREATE table person ( 
   first_name VARCHAR NOT NULL, 
   last_name VARCHAR NOT NULL, 
   dob DATE NOT NULL,
   PRIMARY KEY (first_name, last_name, dob) ); 

然而,有可能有两个人将有相同的名字,并在同一天出生。

NOT NULL关键字对字段施加约束:不能为空。每次试图用空字段创建新记录或从现有记录中删除值时,数据库都会引发错误。我们没有设置VARCHAR类型的列的大小,因此允许这些列存储任何长度的字符串值。

与这样一个记录匹配的 Java 对象可以用下面的Person类来表示:

public class Person {
    private int id;
    private LocalDate dob;
    private String firstName, lastName;
    public Person(String firstName, String lastName, LocalDate dob) {
        if (dob == null) {
            throw new RuntimeException("Date of birth cannot be null");
        }
        this.dob = dob;
        this.firstName = firstName == null ? "" : firstName;
        this.lastName = lastName == null ? "" : lastName;
    }
    public Person(int id, String firstName,
                  String lastName, LocalDate dob) {
        this(firstName, lastName, dob);
        this.id = id;
    }
    public int getId() { return id; }
    public LocalDate getDob() { return dob; }
    public String getFirstName() { return firstName;}
    public String getLastName() { return lastName; }
}

您可能已经注意到,Person类中有两个构造器:有和没有id,我们将使用接受id的构造器基于现有记录构造一个对象,而另一个构造器将用于在插入新记录之前创建一个对象。

创建后,可以使用DROP命令删除表:

DROP table person;

也可以使用ALTERSQL 命令更改现有表;例如,我们可以添加列地址:

ALTER table person add column address VARCHAR;

如果您不确定该列是否已经存在,可以添加IF EXISTSIF NOT EXISTS

ALTER table person add column IF NOT EXISTS address VARCHAR;

但是,这种可能性仅在 PostgreSQL 9.6 及更高版本中存在。

在数据库表创建过程中需要注意的另一个重要问题是是否必须添加另一个索引(除了PRIMARY KEY)。例如,我们可以通过添加以下索引来允许对名字和姓氏进行不区分大小写的搜索:

CREATE index idx_names on person ((lower(first_name), lower(last_name));

如果搜索速度提高,我们会保留索引;如果没有,可以按如下方式删除索引:

 DROP index idx_names;

我们删除它是因为索引有额外写入和存储空间的开销。

如果需要,我们还可以从表中删除列,如下所示:

ALTER table person DROP column address;

在我们的示例中,我们遵循 PostgreSQL 的命名约定。如果您使用不同的数据库,我们建议您查找它的命名约定并遵循它,以便您创建的名称与自动创建的名称对齐。

连接到数据库

到目前为止,我们已经使用了一个控制台来执行 SQL 语句。同样的语句也可以使用 JDBC API 从 Java 代码中执行。但是表只创建一次,所以编写一次性执行的程序没有多大意义。

然而,数据管理是另一回事。因此,从现在开始,我们将使用 Java 代码来操作数据库中的数据。为此,我们首先需要将以下依赖项添加到pom.xml文件中:

<dependency> 
    <groupId>org.postgresql</groupId> 
    <artifactId>postgresql</artifactId> 
    <version>42.2.2</version> 
</dependency>

这与我们安装的 PostgreSQL 9.6 版本相匹配。现在我们可以从 Java 代码创建一个数据库连接,如下所示:

String URL = "jdbc:postgresql://localhost/learnjava";
Properties prop = new Properties();
prop.put( "user", "student" );
// prop.put( "password", "secretPass123" );
try {
    Connection conn = DriverManager.getConnection(URL, prop);
} catch (SQLException ex) {
    ex.printStackTrace();
}

前面的代码只是如何使用java.sql.DriverManger类创建连接的示例。prop.put( "password", "secretPass123" )语句演示如何使用java.util.Properties类为连接提供密码。但是,我们在创建student用户时没有设置密码,所以我们不需要它

许多其他值可以传递给配置连接行为的DriverManager。传入属性的键的名称对于所有主要数据库都是相同的,但其中一些是特定于数据库的。因此,请阅读数据库供应商文档以了解更多详细信息。

或者,对于只通过userpassword的情况,我们可以使用重载的DriverManager.getConnection(String url, String user, String password)版本。对密码进行加密是一种很好的做法。我们不打算演示如何做到这一点,但在互联网上有大量的指南,你可以参考。

另一种连接数据库的方法是使用javax.sql.DataSource接口。它的实现包含在与数据库驱动程序相同的.jar文件中。在PostgreSQL的情况下,有两个类实现DataSource接口:

  • org.postgresql.ds.PGSimpleDataSource
  • org.postgresq l.ds.PGConnectionPoolDataSource

我们可以用这些类来代替DriverManager。下面的代码是使用PGSimpleDataSource类创建数据库连接的示例:

PGSimpleDataSource source = new PGSimpleDataSource();
source.setServerName("localhost");
source.setDatabaseName("learnjava");
source.setUser("student");
//source.setPassword("password");
source.setLoginTimeout(10);
try {
    Connection conn = source.getConnection();
} catch (SQLException ex) {
    ex.printStackTrace();
}

使用PGConnectionPoolDataSource类可以在内存中创建Connection对象池,如下所示:

PGConnectionPoolDataSource source = new PGConnectionPoolDataSource();
source.setServerName("localhost");
source.setDatabaseName("learnjava");
source.setUser("student");
//source.setPassword("password");
source.setLoginTimeout(10);
try {
    PooledConnection conn = source.getPooledConnection();
    Set<Connection> pool = new HashSet<>();
    for(int i = 0; i < 10; i++){
        pool.add(conn.getConnection())
    }
} catch (SQLException ex) {
    ex.printStackTrace();
}

这是首选方法,因为创建一个Connection对象需要时间。池允许您提前完成,然后在需要时重用所创建的对象。不再需要连接后,可以将其返回到池中并重新使用。池大小和其他参数可以在配置文件中设置(例如 PostgreSQL 的postgresql.conf)。

但是,您不需要自己管理连接池。有几种成熟的框架可以为您做到这一点,比如 HikariCPVibur 和公共 DBCP——可靠,使用方便。

无论我们选择哪种方法来创建数据库连接,我们都将把它隐藏在getConnection()方法中,并以相同的方式在所有代码示例中使用它。在获取了Connection类的对象之后,我们现在可以访问数据库来添加、读取、删除或修改存储的数据。

释放连接

保持数据库连接处于活动状态需要大量资源(如内存和 CPU),因此,关闭连接并在不再需要时释放分配的资源是一个好主意。在池的情况下,Connection对象在关闭时返回池并消耗更少的资源。

在 Java7 之前,通过调用finally块中的close()方法关闭连接:

try {
    Connection conn = getConnection();
    //use object conn here
} finally { 
    if(conn != null){
        try {
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    } 
}

无论是否抛出try块内的异常,finally块内的代码始终执行。然而,自 Java7 以来,资源尝试构造也在实现java.lang.AutoCloseablejava.io.Closeable接口的任何对象上执行任务。由于java.sql.Connection对象实现了AutoCloseable接口,我们可以将前面的代码段重写如下:

try (Connection conn = getConnection()) {
    //use object conn here
} catch(SQLException ex) {
    ex.printStackTrace();
}    

因为AutoCloseable资源抛出java.sql.SQLException,所以需要使用catch子句。

操作数据

有四种 SQL 语句可以读取或操作数据库中的数据:

  • INSERT语句向数据库添加数据
  • SELECT语句从数据库中读取数据
  • UPDATE语句更改数据库中的数据
  • DELETE语句从数据库中删除数据

可以在前面的语句中添加一个或多个不同的子句,以标识所请求的数据(例如WHERE子句)和返回结果的顺序(例如ORDER子句)。

JDBC 连接由java.sql.Connection表示。除此之外,它还具有创建三种类型的对象所需的方法,这些对象允许您执行为数据库端提供不同功能的 SQL 语句:

  • java.sql.Statement:这只是将语句发送到数据库服务器执行
  • java.sql.PreparedStatement:将具有特定执行路径的语句缓存在数据库服务器上,允许使用不同的参数高效地执行多次
  • java.sql.CallableStatement:执行数据库中的存储过程

在本节中,我们将回顾如何在 Java 代码中实现这一点。最佳实践是在以编程方式使用 SQL 语句之前,在数据库控制台中测试它。

INSERT语句

INSERT语句在数据库中创建(填充)数据,格式如下:

INSERT into table_name (column1, column2, column3,...) 
                values (value1, value2, value3,...); 

或者,当需要添加多个记录时,可以使用以下格式:

INSERT into table_name (column1, column2, column3,...) 
                values (value1, value2, value3,... ), 
                       (value21, value22, value23,...),
                       ...; 

SELECT语句

SELECT语句的格式如下:

SELECT column_name, column_name FROM table_name 
                                WHERE some_column = some_value;

或者,当需要选择所有列时,可以使用以下格式:

SELECT * from table_name WHERE some_column=some_value; 

WHERE条款更一般的定义如下:

WHERE column_name operator value 
Operator: 
= Equal 
<> Not equal. In some versions of SQL, != 
> Greater than 
< Less than 
>= Greater than or equal 
<= Less than or equal IN Specifies multiple possible values for a column 
LIKE Specifies the search pattern
BETWEEN Specifies the inclusive range of values in a column 

构造的column_name运算符值可以使用ANDOR逻辑运算符组合,并用括号( )分组。

例如,下面的方法从person表中获取所有名字值(用空格字符分隔):

String selectAllFirstNames() {
    String result = "";
    Connection conn = getConnection();
    try (conn; Statement st = conn.createStatement()) {
        ResultSet rs = st.executeQuery("select first_name from person");
        while (rs.next()) {
            result += rs.getString(1) + " ";
        }
    } catch (SQLException ex) {
        ex.printStackTrace();
    }
    return result;
}

ResultSet接口的getString(int position)方法从1位置(在SELECT语句的列列表中的第一个)提取String值。所有原始类型都有类似的获取器:getInt(int position)getByte(int position)等等。

也可以使用列名从ResultSet对象中提取值。在我们的例子中,它将是getString("first_name")。当SELECT语句如下时,这种获取值的方法特别有用:

select * from person;

但是,请记住,使用列名从ResultSet对象提取值的效率较低。但性能上的差异非常小,只有在多次操作时才变得重要。只有实际的测量和测试过程才能判断这种差异对您的应用是否重要。按列名提取值特别有吸引力,因为它提供了更好的代码可读性,这在应用维护期间从长远来看是值得的。

ResultSet接口中还有许多其他有用的方法。如果您的应用从数据库读取数据,我们强烈建议您阅读SELECT语句和ResultSet接口的官方文档。

UPDATE语句

数据可以通过UPDATE语句进行更改,如下所示:

UPDATE table_name SET column1=value1,column2=value2,... WHERE clause;

我们可以使用此语句将其中一条记录中的名字从原始值John更改为新值Jim

update person set first_name = 'Jim' where last_name = 'Adams';

没有WHERE子句,表的所有记录都会受到影响。

DELETE语句

要从表中删除记录,请使用DELETE语句,如下所示:

DELETE FROM table_name WHERE clause;

如果没有WHERE子句,则删除表中的所有记录。对于person表,我们可以使用以下 SQL 语句删除所有记录:

delete from person;

此外,此语句仅删除名为Jim的记录:

delete from person where first_name = 'Jim';

使用Statement

java.sql.Statement接口提供了以下执行 SQL 语句的方法:

  • boolean execute(String sql):如果被执行的语句返回可以通过java.sql.Statement接口的ResultSet getResultSet()方法检索的数据(在java.sql.ResultSet对象内部),则返回true。或者,如果执行的语句不返回数据(对于INSERT语句或UPDATE语句),则返回false,随后调用java.sql.Statement接口的int getUpdateCount()方法返回受影响的行数。

  • ResultSet executeQuery(String sql):以java.sql.ResultSet对象的形式返回数据(此方法使用的 SQL 语句通常是SELECT语句)。java.sql.Statement接口的ResultSet getResultSet()方法不返回数据,java.sql.Statement接口的int getUpdateCount()方法返回-1

  • int executeUpdate(String sql):返回受影响的行数(执行的 SQL 语句应该是UPDATE语句或DELETE语句)。相同的号码由java.sql.Statement接口的int getUpdateCount()方法返回;后续调用java.sql.Statement接口的ResultSet getResultSet()方法返回null

我们将演示这三种方法是如何在每个语句上工作的:INSERTSELECTUPDATEDELETE

execute(String sql)方法

让我们尝试执行每个语句;我们将从INSERT语句开始:

String sql = "insert into person (first_name, last_name, dob) " +
                         "values ('Bill', 'Grey', '1980-01-27')";
Connection conn = getConnection();
try (conn; Statement st = conn.createStatement()) {
    System.out.println(st.execute(sql));             //prints: false
    System.out.println(st.getResultSet() == null);   //prints: true
    System.out.println(st.getUpdateCount());         //prints: 1
} catch (SQLException ex) {
    ex.printStackTrace();
}
System.out.println(selectAllFirstNames());           //prints: Bill

前面的代码向person表中添加了一条新记录。返回的false值表示执行语句没有返回数据,这就是getResultSet()方法返回null的原因。但是getUpdateCount()方法返回1,因为一条记录受到影响(添加)。selectAllFirstNames()方法证明插入了预期的记录。

现在执行SELECT语句,如下所示:

String sql = "select first_name from person";
Connection conn = getConnection();
try (conn; Statement st = conn.createStatement()) {
    System.out.println(st.execute(sql));             //prints: true
    ResultSet rs = st.getResultSet();
    System.out.println(rs == null);                  //prints: false
    System.out.println(st.getUpdateCount());         //prints: -1
    while (rs.next()) {
        System.out.println(rs.getString(1) + " ");   //prints: Bill
    }
} catch (SQLException ex) {
    ex.printStackTrace();
}

前面的代码从person表中选择所有的名字。返回的true值表示有被执行语句返回的数据。这就是为什么getResultSet()方法不返回null,而是返回ResultSet对象。getUpdateCount()方法返回-1,因为没有记录受到影响(更改)。由于person表中只有一条记录,ResultSet对象只包含一个结果,rs.getString(1)返回Bill

下面的代码使用UPDATE语句将person表的所有记录中的名字改为Adam

String sql = "update person set first_name = 'Adam'";
Connection conn = getConnection();
try (conn; Statement st = conn.createStatement()) {
    System.out.println(st.execute(sql));             //prints: false
    System.out.println(st.getResultSet() == null);   //prints: true
    System.out.println(st.getUpdateCount());         //prints: 1
} catch (SQLException ex) {
    ex.printStackTrace();
}
System.out.println(selectAllFirstNames());          //prints: Adam

在前面的代码中,返回的false值表示执行语句没有返回数据。这就是getResultSet()方法返回null的原因。但是getUpdateCount()方法返回1,因为person表中只有一条记录,一条记录受到了影响(更改)。selectAllFirstNames()方法证明对该记录进行了预期的更改。

下面的DELETE语句执行从person表中删除所有记录:

String sql = "delete from person";
Connection conn = getConnection();
try (conn; Statement st = conn.createStatement()) {
    System.out.println(st.execute(sql));             //prints: false
    System.out.println(st.getResultSet() == null);   //prints: true
    System.out.println(st.getUpdateCount());         //prints: 1
} catch (SQLException ex) {
    ex.printStackTrace();
}
System.out.println(selectAllFirstNames());           //prints: 

在前面的代码中,返回的false值表示执行语句没有返回数据。这就是为什么getResultSet()方法返回null。但是getUpdateCount()方法返回1,因为person表中只有一条记录,一条记录被影响(删除)。selectAllFirstNames()方法证明person表中没有记录。

executeQuery(String sql)方法

在本节中,我们将尝试执行execute(String sql)方法一节中演示execute()方法时使用的相同语句(作为查询),我们将从INSERT语句开始,如下所示:

String sql = "insert into person (first_name, last_name, dob) " +
                         "values ('Bill', 'Grey', '1980-01-27')";
Connection conn = getConnection();
try (conn; Statement st = conn.createStatement()) {
    st.executeQuery(sql);                  //PSQLException
} catch (SQLException ex) {
    ex.printStackTrace();                  //prints: stack trace 
}
System.out.println(selectAllFirstNames()); //prints: Bill

前面的代码生成了一个关于No results were returned by the query消息的异常,因为executeQuery()方法希望执行SELECT语句。然而,selectAllFirstNames()方法证明插入了预期的记录

现在执行SELECT语句,如下所示:

String sql = "select first_name from person";
Connection conn = getConnection();
try (conn; Statement st = conn.createStatement()) {
    ResultSet rs1 = st.executeQuery(sql);
    System.out.println(rs1 == null);          //prints: false
    ResultSet rs2 = st.getResultSet();
    System.out.println(rs2 == null);          //prints: false
    System.out.println(st.getUpdateCount());  //prints: -1
    while (rs1.next()) {
        System.out.println(rs1.getString(1)); //prints: Bill
    }
    while (rs2.next()) {
        System.out.println(rs2.getString(1)); //prints:
    }
} catch (SQLException ex) {
    ex.printStackTrace();
}

前面的代码从person表中选择所有的名字。返回的false值表示executeQuery()总是返回ResultSet对象,即使person表中没有记录。如您所见,从所执行语句获得结果似乎有两种方法。但是,rs2对象没有数据,因此,在使用executeQuery()方法时,请确保从ResultSet对象获取数据。

现在让我们尝试执行一个UPDATE语句,如下所示:

String sql = "update person set first_name = 'Adam'";
Connection conn = getConnection();
try (conn; Statement st = conn.createStatement()) {
    st.executeQuery(sql);                  //PSQLException
} catch (SQLException ex) {
    ex.printStackTrace();                  //prints: stack trace
}
System.out.println(selectAllFirstNames()); //prints: Adam

前面的代码生成了一个与No results were returned by the query消息相关的异常,因为executeQuery()方法希望执行SELECT语句。然而,selectAllFirstNames()方法证明预期的更改是对记录进行的

在执行DELETE语句时,我们将得到相同的异常:

String sql = "delete from person";
Connection conn = getConnection();
try (conn; Statement st = conn.createStatement()) {
    st.executeQuery(sql);                  //PSQLException
} catch (SQLException ex) {
    ex.printStackTrace();                  //prints: stack trace
}
System.out.println(selectAllFirstNames()); //prints: 

尽管如此,selectAllFirstNames()方法证明了person表的所有记录都被删除了。

我们的演示表明,executeQuery()应该只用于SELECT语句。executeQuery()方法的优点是,当用于SELECT语句时,即使没有选择数据,它也返回一个非空的ResultSet对象,这简化了代码,因为不需要检查null的返回值。

executeUpdate(String sql)方法

我们将用INSERT语句开始演示executeUpdate()方法:

String sql = "insert into person (first_name, last_name, dob) " +
                         "values ('Bill', 'Grey', '1980-01-27')";
Connection conn = getConnection();
try (conn; Statement st = conn.createStatement()) {
    System.out.println(st.executeUpdate(sql));  //prints: 1
    System.out.println(st.getResultSet());      //prints: null
    System.out.println(st.getUpdateCount());    //prints: 1
} catch (SQLException ex) {
    ex.printStackTrace();
}
System.out.println(selectAllFirstNames());      //prints: Bill

如您所见,executeUpdate()方法返回受影响(在本例中是插入的)行数。相同的数字返回int getUpdateCount()方法,ResultSet getResultSet()方法返回nullselectAllFirstNames()方法证明插入了期望的记录。

executeUpdate()方法不能用于执行SELECT语句:

String sql = "select first_name from person";
Connection conn = getConnection();
try (conn; Statement st = conn.createStatement()) {
    st.executeUpdate(sql);    //PSQLException
} catch (SQLException ex) {
    ex.printStackTrace();     //prints: stack trace
}

异常的消息是A result was returned when none was expected

另一方面,UPDATE语句通过executeUpdate()方法执行得很好:

String sql = "update person set first_name = 'Adam'";
Connection conn = getConnection();
try (conn; Statement st = conn.createStatement()) {
    System.out.println(st.executeUpdate(sql));  //prints: 1
    System.out.println(st.getResultSet());      //prints: null
    System.out.println(st.getUpdateCount());    //prints: 1
} catch (SQLException ex) {
    ex.printStackTrace();
}
System.out.println(selectAllFirstNames());      //prints: Adam

executeUpdate()方法返回受影响(在本例中是更新的)行数。相同的数字返回int getUpdateCount()方法,而ResultSet getResultSet()方法返回nullselectAllFirstNames()方法证明预期记录已更新。

DELETE语句产生类似的结果:

String sql = "delete from person";
Connection conn = getConnection();
try (conn; Statement st = conn.createStatement()) {
    System.out.println(st.executeUpdate(sql));  //prints: 1
    System.out.println(st.getResultSet());      //prints: null
    System.out.println(st.getUpdateCount());    //prints: 1
} catch (SQLException ex) {
    ex.printStackTrace();
}
System.out.println(selectAllFirstNames());      //prints:

现在,您可能已经意识到,executeUpdate()方法更适合于INSERTUPDATEDELETE语句。

使用PreparedStatement

PreparedStatementStatement接口的子接口。这意味着它可以在使用Statement接口的任何地方使用。不同之处在于PreparedStatement被缓存在数据库中,而不是每次被调用时都被编译。这样,它就可以针对不同的输入值高效地执行多次。与Statement类似,它可以通过prepareStatement()方法使用相同的Connection对象创建。

由于同一条 SQL 语句可以用于创建StatementPreparedStatement,所以对于任何被多次调用的 SQL 语句,最好使用PreparedStatement,因为它比数据库端的Statement接口性能更好。为此,我们只需更改前面代码示例中的这两行:

try (conn; Statement st = conn.createStatement()) { 
     ResultSet rs = st.executeQuery(sql);

相反,我们可以使用PreparedStatement类,如下所示:

try (conn; PreparedStatement st = conn.prepareStatement(sql)) { 
     ResultSet rs = st.executeQuery();

要创建带参数的PreparedStatement类,可以用问号符号(?替换输入值;例如,我们可以创建以下方法:

List<Person> selectPersonsByFirstName(String searchName) {
    List<Person> list = new ArrayList<>();
    Connection conn = getConnection();
    String sql = "select * from person where first_name = ?";
    try (conn; PreparedStatement st = conn.prepareStatement(sql)) {
        st.setString(1, searchName);
        ResultSet rs = st.executeQuery();
        while (rs.next()) {
            list.add(new Person(rs.getInt("id"),
                    rs.getString("first_name"),
                    rs.getString("last_name"),
                    rs.getDate("dob").toLocalDate()));
        }
    } catch (SQLException ex) {
        ex.printStackTrace();
    }
    return list;
}

数据库将PreparedStatement类编译为模板并存储它而不执行。然后,当应用稍后使用它时,参数值被传递给模板,模板可以立即执行,而无需编译开销,因为它已经完成了。

预备语句的另一个优点是可以更好地防止 SQL 注入攻击,因为值是使用不同的协议传入的,并且模板不基于外部输入。

如果准备好的语句只使用一次,它可能比常规语句慢,但差别可以忽略不计。如果有疑问,请测试性能,看看它是否适合您的应用—提高安全性是值得的

使用CallableStatement

CallableStatement接口(扩展了PreparedStatement接口)可以用来执行存储过程,尽管有些数据库允许您使用StatementPreparedStatement接口调用存储过程。CallableStatement对象是通过prepareCall()方法创建的,可以有三种类型的参数:

  • IN输入值
  • OUT结果
  • IN OUT输入或输出值

IN参数的设置方式与PreparedStatement参数相同,而OUT参数必须通过CallableStatementregisterOutParameter()方法注册

值得注意的是,以编程方式从 Java 执行存储过程是标准化程度最低的领域之一。例如,PostgreSQL 不直接支持存储过程,但它们可以作为函数调用,为此,通过将OUT参数解释为返回值,对其进行了修改。另一方面,Oracle 也允许OUT参数作为函数。

这就是为什么数据库函数和存储过程之间的以下差异只能作为一般准则,而不能作为正式定义:

  • 函数有返回值,但不允许使用OUT参数(某些数据库除外),可以在 SQL 语句中使用。
  • 存储过程没有返回值(某些数据库除外);它允许使用OUT参数(对于大多数数据库),并且可以使用 JDBCCallableStatement接口执行。

您可以参考数据库文档来了解如何执行存储过程

由于存储过程是在数据库服务器上编译和存储的,CallableStatementexecute()方法对同一条 SQL 语句的性能优于StatementPreparedStatement接口的相应方法。这就是为什么很多 Java 代码有时会被一个或多个存储过程(甚至包括业务逻辑)所取代的原因之一。然而,并不是每个案例和问题都有正确的答案,因此我们将避免提出具体的建议,只是重复一个熟悉的咒语,即测试的价值和您正在编写的代码的清晰性。

例如,让我们调用 PostgreSQL 安装附带的replace(string origText, from substr1, to substr2)函数。它搜索第一个参数(string origText),并使用第三个参数(string substr2提供的字符串替换其中与第二个参数(from substr1)匹配的所有子字符串。以下 Java 方法使用CallableStatement执行此函数:

String replace(String origText, String substr1, String substr2) {
    String result = "";
    String sql = "{ ? = call replace(?, ?, ? ) }";
    Connection conn = getConnection();
    try (conn; CallableStatement st = conn.prepareCall(sql)) {
        st.registerOutParameter(1, Types.VARCHAR);
        st.setString(2, origText);
        st.setString(3, substr1);
        st.setString(4, substr2);
        st.execute();
        result = st.getString(1);
    } catch (Exception ex){
        ex.printStackTrace();
    }
    return result;
}

现在我们可以如下调用此方法:

String result = replace("That is original text",
                                "original text", "the result");
System.out.println(result);  //prints: That is the result

一个存储过程可以完全没有任何参数,可以只使用IN参数,也可以只使用OUT参数,或者两者都使用。结果可以是一个或多个值,也可以是一个ResultSet对象。您可以在数据库文档中找到用于创建函数的 SQL 语法

总结

在本章中,我们讨论并演示了如何在 Java 应用中填充、读取、更新和删除数据库中的数据。对 SQL 语言的简短介绍描述了如何使用StatementPreparedStatementCallableStatement创建数据库及其结构、如何修改数据库以及如何执行 SQL 语句。

在下一章中,我们将描述和讨论最流行的网络协议,演示如何使用它们,以及如何使用最新的 Java HTTP 客户端 API 实现客户端-服务器通信。所回顾的协议包括基于 TCP、UDP 和 URL 的通信协议的 Java 实现

测验

  1. 选择所有正确的语句:

    1. JDBC 代表 Java 数据库通信。
    2. JDBC API 包括java.db包。
    3. JDBC API 随 Java 安装而来。
    4. JDBC API 包括所有主要 DBMSE 的驱动程序。
  2. 选择所有正确的语句:

    1. 可以使用CREATE语句创建数据库表。
    2. 可以使用UPDATE语句更改数据库表。
    3. 可以使用DELETE语句删除数据库表。
    4. 每个数据库列都可以有一个索引。
  3. 选择所有正确的语句:

    1. 要连接到数据库,可以使用Connect类。
    2. 必须关闭每个数据库连接。
    3. 同一数据库连接可用于许多操作。
    4. 可以合并数据库连接。
  4. 选择所有正确的语句:

    1. 可以使用资源尝试结构自动关闭数据库连接。
    2. 可以使用finally块构造关闭数据库连接。
    3. 可以使用catch块关闭数据库连接。
    4. 一个数据库连接可以在没有try块的情况下关闭。
  5. 选择所有正确的语句:

    1. INSERT语句包含一个表名。
    2. INSERT语句包括列名。
    3. INSERT语句包含值。
    4. INSERT语句包含约束。
  6. 选择所有正确的语句:

    1. SELECT语句必须包含表名。
    2. SELECT语句必须包含列名。
    3. SELECT语句必须包含WHERE子句。
    4. SELECT语句可以包括ORDER子句。
  7. 选择所有正确的语句:

    1. UPDATE语句必须包含表名。
    2. UPDATE语句必须包含列名。
    3. UPDATE语句可以包括WHERE子句。
    4. UPDATE语句可以包括ORDER子句。
  8. 选择所有正确的语句:

    1. DELETE语句必须包含表名。
    2. DELETE语句必须包含列名。
    3. DELETE语句可以包括WHERE子句。
    4. DELETE语句可以包括ORDER子句。
  9. 选择Statement接口的execute()方法的所有正确语句:

    1. 它接收 SQL 语句。
    2. 它返回一个ResultSet对象。
    3. 调用execute()后,Statement对象可能返回数据。
    4. 调用execute()后,Statement对象可能返回受影响的记录数
  10. 选择Statement接口的executeQuery()方法的所有正确语句: 1. 它接收 SQL 语句。 2. 它返回一个ResultSet对象。 3. 调用executeQuery()后,Statement对象可能返回数据。 4. 调用executeQuery()后,Statement对象可能返回受影响的记录数。

  11. 选择接口StatementexecuteUpdate()方法的所有正确语句: 1. 它接收 SQL 语句。 2. 它返回一个ResultSet对象。 3. 调用executeUpdate()后,Statement对象可能返回数据。 4. Statement对象返回调用executeUpdate()后受影响的记录数。

  12. 选择所有关于PreparedStatement接口的正确语句: 1. 它扩展自Statement。 2. 类型为PreparedStatement的对象是通过prepareStatement()方法创建的。 3. 它总是比Statement更有效。 4. 它导致数据库中的模板只创建一次。

  13. 选择所有关于CallableStatement接口的正确语句: 1. 它扩展自PreparedStatement。 2. 类型为CallableStatement的对象是通过prepareCall()方法创建的。 3. 它总是比PreparedStatement更有效。 4. 它导致数据库中的模板只创建一次。