vlambda博客
学习文章列表

JDK17 |java17学习 第 9 章 JVM 结构和垃圾回收

Chapter 10: Managing Data in a Database

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

本章将涵盖以下主题:

  • 创建数据库
  • 创建数据库结构
  • 连接到数据库
  • 释放连接
  • 对数据的创建、读取、更新和删除(CRUD)操作
  • 使用共享库 JAR 文件访问数据库

在本章结束时,您将能够创建和使用数据库来存储、更新和检索数据,以及创建和使用共享库。

Technical requirements

为了能够执行本章提供的代码示例,您将需要以下内容:

  • Java SE version 17 or later
  • An IDE or code editor you prefer

第 1 章 Java 17 入门。本章的代码示例文件可在 GitHub (https://github .com/PacktPublishing/Learn-Java-17-Programming.git) 在 examples/src/main/java/com/packt/learnjava/ch10_database 文件夹中,并在 database 文件夹中,作为共享库的一个单独项目。

Creating a database

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

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

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

java.sqljavax.sql 包含在 < strong class="bold">Java 平台标准版 (Java SE)。 javax.sql 包包含支持语句池、分布式事务和行集的 DataSource 接口。

创建数据库涉及以下八个步骤:

  1. Install the database by following the vendor instructions.
  2. Open the PL/SQL terminal and create a database user, a database, a schema, tables, views, stored procedures, and anything else that is necessary to support the data model of the application.
  3. Add to this application the dependency on a .jar file with the database-specific driver.
  4. Connect to the database from the application.
  5. Construct the SQL statement.
  6. Execute the SQL statement.
  7. Use the result of the execution as your application requires.
  8. Release (that is, close) the database connection and any other resources that were opened in the process.

步骤 13 在数据库设置期间和之前只执行一次应用程序正在运行。 步骤 48 由应用程序根据需要重复执行。事实上,Steps 57 可以在同一个数据库连接上重复多次。

对于我们的示例,我们将使用 PostgreSQL 数据库。您首先需要使用特定于数据库的说明自行执行 Steps 13。要为我们的演示创建数据库,我们使用以下 PL/SQL 命令:

create user student SUPERUSER;
create database learnjava owner student;

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

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

Creating a database structure

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

  • The CREATE statement creates the database entity.
  • The ALTER statement changes the database entity.
  • The DROP statement deletes the database entity.

还有各种 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_name< 的组合来制作复合 PRIMARY KEY 关键字/code> 和 dob

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

但是,有一个可能会有两个人同名并在同一天出生,所以这样的复合 prim 并不是一个好主意。

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;

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

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;

In our examples, we follow the naming convention of PostgreSQL. If you use a different database, we suggest that you look up its naming convention and follow it, so that the names you create align with those that are created automatically.

Connecting to a database

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

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

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

example 项目也可以访问此依赖项,因为在 pom.xml 文件中>example项目,我们对数据库.jar文件有如下依赖:

<dependency> 
    <groupId>com.packt.learnjava</groupId>
    <artifactId>database</artifactId>
    <version>1.0-SNAPSHOT</version> 
</dependency>

确保通过执行"mvn clean install"database项目> 在运行任何示例之前在 database 文件夹中执行命令。

现在,我们可以从 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) 版本。保持密码加密是一个很好的做法。我们不会演示如何执行此操作,但您可以参考 Internet 上的大量指南。

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

  • org.postgresql.ds.PGSimpleDataSource
  • org.postgresql.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.conf 用于 PostgreSQL)。

但是,您不需要自己管理连接池。 有几个成熟的框架可以为你做这件事,例如 HikariCP (https://github.com/brettwooldridge/HikariCP),Vibur(http://www.vibur.org )和 Commons DBCP(https://commons.apache.org/proper/commons -dbcp) – 这些 可靠且易于使用。

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

Releasing the connection

保持数据库连接处于活动状态需要大量资源,例如内存和 CPU,因此最好关闭连接并尽快释放分配的资源不再需要它们。在池化的情况下,Connection 对象在关闭时会返回到池中并消耗更少的资源。

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

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

finally 块内的代码总是被执行,无论 try 块内的异常是否被抛出。但是,从 Java 7 开始,try-with-resources 构造也 对任何实现 java.lang.AutoCloseablejava.io.Closeable 接口。由于java.sql.Connection对象确实实现了AutoCloseable接口,我们可以重写之前的代码片段,如下:

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

catch 子句是必需的,因为 AutoCloseable 资源会抛出 java.sql.SQLException

CRUD data

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

  • The INSERT statement adds data to a database.
  • The SELECT statement reads data from a database.
  • The UPDATE statement changes data in a database.
  • The DELETE statement deletes data from a database.

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

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

  • java.sql.Statement: This simply sends the statement to the database server for execution.
  • java.sql.PreparedStatement: This caches the statement with a certain execution path on the database server by allowing it to be executed multiple times with different parameters in an efficient manner.
  • java.sql.CallableStatement: This executes the stored procedure in the database.

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

The INSERT statement

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,...),
                       ...; 

The SELECT statement

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)方法提取String位置 1 的值(SELECT 语句中列列表中的第一个)。所有原始类型都有类似的 getter:getInt(int position)getByte(int position) 等等。

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

select * from person;

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

ResultSet 接口中还有许多其他有用的方法。如果您的应用程序从数据库中读取数据,我们强烈建议您阅读官方文档(www.postgresql.org/docs< /a>) 的 SELECT 语句和您正在使用的数据库版本的 ResultSet 接口。

The UPDATE statement

数据可以通过UPDATE语句改变,如下:

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

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

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

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

The DELETE statement

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

DELETE FROM table_name WHERE clause;

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

delete from person;

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

delete from person where first_name = 'Jim';

Using statements

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

  • boolean execute(String sql): This returns true if the executed statement returns data (inside the java.sql.ResultSet object) that can be retrieved using the ResultSet getResultSet() method of the java.sql.Statement interface. Alternatively, it returns false if the executed statement does not return data (for the INSERT statement or the UPDATE statement) and the subsequent call to the int getUpdateCount() method of the java.sql.Statement interface returns the number of affected rows.
  • ResultSet executeQuery(String sql): This returns data as a java.sql.ResultSet object (the SQL statement used with this method is usually a SELECT statement). The ResultSet getResultSet() method of the java.sql.Statement interface does not return data, while the int getUpdateCount() method of the java.sql.Statement interface returns -1.
  • int executeUpdate(String sql): This returns the number of affected rows (the executed SQL statement is expected to be the UPDATE statement or the DELETE statement). The same number is returned by the int getUpdateCount() method of the java.sql.Statement interface; the subsequent call to the ResultSet getResultSet() method of the java.sql.Statement interface returns null.

我们将演示这三种方法如何作用于每个语句:INSERTSELECT UPDATEDELETE

The execute(String sql) method

让我们尝试执行每个语句;我们将 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 表的所有记录中的名字更改为 亚当

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 因为一条记录受到影响(更改),因为 getUpdateCount() 中只有一条记录code class="literal">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 因为一条记录受到影响(删除),因为 < code class="literal">person 表。 selectAllFirstNames() 方法证明 person 表中没有记录。

The executeQuery(String sql) method

在这个 部分中,我们将尝试执行我们在演示 相同的语句(作为查询) execute(String sql) 方法 部分中的="literal">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

The executeUpdate(String sql) method

我们将开始演示带有 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() 方法返回 selectAllFirstNames() 方法证明插入了预期的记录。

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 is 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() 方法返回 selectAllFirstNames() 方法证明预期的记录已更新。

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 语句。

Using PreparedStatement

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

由于可以使用相同的 SQL 语句来创建 StatementPreparedStatement,因此最好使用 PreparedStatement 用于多次调用的任何 SQL 语句,因为它的性能优于数据库端的 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 对象,您可以将输入值 替换为问号符号(?);例如,我们可以创建以下方法(参见 database 项目中的 Person 类):

private static final String SELECT_BY_FIRST_NAME = 
            "select * from person where first_name = ?";
static List<Person> selectByFirstName(Connection conn, 
                                     String searchName) {
    List<Person> list = new ArrayList<>();
    try (PreparedStatement st = 
         conn.prepareStatement(SELECT_BY_FIRST_NAME)) {
       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 注入攻击,因为值是使用不同的协议传入的,并且模板不基于外部输入。

如果一个prepared statement 只使用一次,它可能会比一个普通的statement 慢,但差异可以忽略不计。如果有疑问,请测试性能并查看它是否适合您的应用程序 - 提高安全性可能是值得的。

Using CallableStatement

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

  • IN for an input value
  • OUT for the result
  • IN OUT for either an input or an output value

IN参数可以和PreparedStatement的参数一样设置,而OUT参数必须通过registerOutParameter()方法注册CallableStatement

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

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

  • A function has a return value, but it does not allow OUT parameters (except for some databases) and can be used in a SQL statement.
  • A stored procedure does not have a return value (except for some databases); it allows OUT parameters (for most databases) and can be executed using the JDBC CallableStatement interface.

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

由于存储过程是编译存储在数据库服务器上的,所以CallableStatementexecute()方法对于同样的SQL语句表现更好比 StatementPreparedStatement 接口的对应方法。这就是为什么很多 Java 代码有时会被一个或多个甚至包含业务逻辑的存储过程所取代的原因之一。但是,对于每个案例和问题,没有一个正确的答案,因此我们将避免提出具体的建议,除了重复熟悉的关于测试的价值和您正在编写的代码的清晰性的口头禅:

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 语法。

Using a shared library JAR file to access a database

其实我们已经开始使用database项目的JAR文件来访问数据库驱动程序,在 database 项目的 pom.xml 文件中设置为依赖项。现在,我们将演示如何使用 database 项目 JAR 文件的 JAR 文件来操作数据库中的数据。 UseDatabaseJar 类中提供了这种用法的示例。

为了支持 CRUD 操作,数据库表通常表示一类对象。这种表的每一行都包含一个类的一个对象的属性。在 创建数据库结构 部分,我们演示了 Personperson 表。为了说明如何使用 JAR 文件进行数据操作,我们创建了一个 单独的 database 项目,该项目只有一个 Person 类。除了 创建数据库结构 部分中显示的属性之外,它还具有用于所有 CRUD 操作的静态方法。以下是 insert() 方法:

static final String INSERT = "insert into person " +
  "(first_name, last_name, dob) values (?, ?, ?::date)";
static void insert(Connection conn, Person person) {
   try (PreparedStatement st = 
                       conn.prepareStatement(INSERT)) {
            st.setString(1, person.getFirstName());
            st.setString(2, person.getLastName());
            st.setString(3, person.getDob().toString());
            st.execute();
   } catch (SQLException ex) {
            ex.printStackTrace();
   }
}

以下是 selectByFirstName() 方法:

private static final String SELECT = 
          "select * from person where first_name = ?";
static List<Person> selectByFirstName(Connection conn, 
                                    String firstName) {
   List<Person> list = new ArrayList<>();
   try (PreparedStatement st = conn.prepareStatement(SELECT)) {
        st.setString(1, firstName);
        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;
}

跟在 之后的是 updateFirstNameById() 方法:

private static final String UPDATE = 
      "update person set first_name = ? where id = ?";
public static void updateFirstNameById(Connection conn, 
                           int id, String newFirstName) {
   try (PreparedStatement st = conn.prepareStatement(UPDATE)) {
            st.setString(1, newFirstName);
            st.setInt(2, id);
            st.execute();
   } catch (SQLException ex) {
            ex.printStackTrace();
   }
}

跟在 之后的是 deleteById() 方法:

private static final String DELETE = 
                       "delete from person where id = ?";
public static void deleteById(Connection conn, int id) {
   try (PreparedStatement st = conn.prepareStatement(DELETE)) {
            st.setInt(1, id);
            st.execute();
   } catch (SQLException ex) {
            ex.printStackTrace();
   }
}

如您所见,上述所有方法都接受 Connection 对象作为参数,而不是在每个方法中创建和销毁它。我们决定这样做是因为它允许多个操作与每个 Connection 对象相关联,以防我们希望它们一起提交到数据库,或者如果有一个则回滚其中一个失败(在您选择的数据库的文档中阅读有关事务管理的信息)。此外,JAR 文件(由 database 项目生成)可以被不同的应用程序使用,所以database 连接参数将是特定于应用程序的,这就是必须在使用 JAR 文件的应用程序中创建 Connection 对象的原因。下面的代码演示了这种用法(参见 UseDatabaseJar 类)。

在运行以下示例之前,请确保您已在 database 文件夹中执行了 mvn clean install 命令:

1 try(Connection conn = getConnection()){
2    cleanTablePerson(conn);
3    Person mike = new Person("Mike", "Brown", 
                             LocalDate.of(2002, 8, 14));
4    Person jane = new Person("Jane", "McDonald", 
                             LocalDate.of(2000, 3, 21));
5    Person jill = new Person("Jill", "Grey", 
                             LocalDate.of(2001, 4, 1));
6    Person.insert(conn, mike);
7    Person.insert(conn, jane);
8    Person.insert(conn, jane);
9    List<Person> persons = 
           Person.selectByFirstName(conn, jill.getFirstName());
10   System.out.println(persons.size());      //prints: 0
11   persons = Person.selectByFirstName(conn, 
                                          jane.getFirstName());
12   System.out.println(persons.size());      //prints: 2
13   Person person = persons.get(0);
14   Person.updateFirstNameById(conn, person.getId(),
                                          jill.getFirstName());
15   persons = Person.selectByFirstName(conn, 
                                          jane.getFirstName());
16   System.out.println(persons.size());      //prints: 1 
17   persons = Person.selectByFirstName(conn, 
                                          jill.getFirstName());
18   System.out.println(persons.size());      //prints: 1
19   persons = Person.selectByFirstName(conn, 
                                          mike.getFirstName());
20   System.out.println(persons.size());      //prints: 1
21   for(Person p: persons){
22      Person.deleteById(conn, p.getId());
23   }
24   persons = Person.selectByFirstName(conn, 
                                          mike.getFirstName());
25   System.out.println(persons.size());      //prints: 0
26 } catch (SQLException ex){
27       ex.printStackTrace();
28 }

让我们看看前面的代码片段12628 行组成 try–catch 块,它处理 Connection 对象并捕获在其执行期间该块内可能发生的所有异常。

包含行 2 只是为了清理 person 表中的数据在运行演示代码之前。以下是 cleanTablePerson() 方法的实现:

void cleanTablePerson(Connection conn) {
   try (Statement st = conn.createStatement()) {
       st.execute("delete from person");
   } catch (SQLException ex) {
       ex.printStackTrace();
   }
}

345 行中,我们创建了三个对象Person 类,然后在 67 行中class="literal">8,我们使用它们在 person 表中插入记录。

9 行中,我们在数据库中查询一条记录,该记录的名字取自 jill 对象,在 jill 对象中code class="literal">10,我们打印出结果计数,即0(因为我们没有插入这样的记录)。

在第 11 行中,我们在数据库中查询名字设置为 Jane 的记录,并且在第 12,我们打印出结果计数,即2(因为我们确实插入了两条具有这样一个值的记录)。

在行 13 中,我们提取了前一个查询返回的两个对象中的第一个,在行 14 中,我们更新了对应的记录具有不同的名字值(取自 jill 对象)。

在行 15 中,我们重复查询第一个名称设置为 Jane 的记录,并在行 16,我们打印出结果计数,这次是1(和预期的一样,因为我们把名字改成了Jill 在两条记录之一上)。

在第 17 行中,我们选择所有 具有 名字集的记录到 Jill,在 18 行,我们打印出结果计数,即 1这次是 (正如预期的那样,因为我们已将名字更改为 Jill 在曾经具有名字值 )。

19 行,我们选择名称设置为 Mike 的所有记录,在 20,我们打印出结果计数,即1(正如预期的那样,因为我们只创建了一个这样的记录)。

2123 行中,我们循环删除所有检索到的记录。

这就是为什么当我们再次在 24 行中选择所有名字为 Mike 的记录时,我们得到的结果计数等于025 行(正如预期的那样,因为没有这样的记录了)。

至此,当这段代码片段执行完毕,UseDatabseJar类的main()方法完成后,所有的变化在数据库中自动保存。

这就是任何将此文件作为依赖项的应用程序可以使用 JAR 文件(允许修改数据库中的数据)的方式。

Summary

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

现在,您可以创建和使用数据库来存储、更新和检索数据,并创建和使用共享库。

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

Quiz

  1. Select all the correct statements:
    1. JDBC stands for Java Database Communication.
    2. The JDBC API includes the java.db package.
    3. The JDBC API comes with Java installation.
    4. The JDBC API includes the drivers for all major DBMSs.
  2. Select all the correct statements:
    1. A database table can be created using the CREATE statement.
    2. A database table can be changed using the UPDATE statement.
    3. A database table can be removed using the DELETE statement.
    4. Each database column can have an index.
  3. Select all the correct statements:
    1. To connect to a database, you can use the Connect class.
    2. Every database connection must be closed.
    3. The same database connection may be used for many operations.
    4. Database connections can be pooled.
  4. Select all the correct statements:
    1. A database connection can be closed automatically using the try-with-resources construct.
    2. A database connection can be closed using the finally block construct.
    3. A database connection can be closed using the catch block.
    4. A database connection can be closed without a try block.
  5. Select all the correct statements:
    1. The INSERT statement includes a table name.
    2. The INSERT statement includes column names.
    3. The INSERT statement includes values.
    4. The INSERT statement includes constraints.
  6. Select all the correct statements:
    1. The SELECT statement must include a table name.
    2. The SELECT statement must include a column name.
    3. The SELECT statement must include the WHERE clause.
    4. The SELECT statement may include the ORDER clause.
  7. Select all the correct statements:
    1. The UPDATE statement must include a table name.
    2. The UPDATE statement must include a column name.
    3. The UPDATE statement may include the WHERE clause.
    4. The UPDATE statement may include the ORDER clause.
  8. Select all the correct statements:
    1. The DELETE statement must include a table name.
    2. The DELETE statement must include a column name.
    3. The DELETE statement may include the WHERE clause.
    4. The DELETE statement may include the ORDER clause.
  9. Select all the correct statements about the execute() method of the Statement interface:
    1. It receives a SQL statement.
    2. It returns a ResultSet object.
    3. The Statement object may return data after execute() is called.
    4. The Statement object may return the number of affected records after execute() is called.
  10. Select all the correct statements about the executeQuery() method of the Statement interface:
    1. It receives a SQL statement.
    2. It returns a ResultSet object.
    3. The Statement object may return data after executeQuery() is called.
    4. The Statement object may return the number of affected records after executeQuery() is called.
  11. Select all the correct statements about the executeUpdate() method of the Statement interface:
    1. It receives a SQL statement.
    2. It returns a ResultSet object.
    3. The Statement object may return data after executeUpdate() is called.
    4. The Statement object returns the number of affected records after executeUpdate() is called.
  12. Select all the correct statements about the PreparedStatement interface:
    1. It extends Statement.
    2. An object of type PreparedStatement is created by the prepareStatement() method.
    3. It is always more efficient than Statement.
    4. It results in a template in the database being created only once.
  13. Select all the correct statements about the CallableStatement interface:
    1. It extends PreparedStatement.
    2. An object of type CallableStatement is created by the prepareCall() method.
    3. It is always more efficient than PreparedStatement.
    4. It results in a template in the database being created only once.