本章解释并演示了如何使用 Java 应用管理(即,插入、读取、更新和删除)数据库中的数据。简要介绍了结构化查询语言(SQL)和数据库的基本操作,包括如何连接数据库、如何创建数据库结构、如何用 SQL 编写数据库表达式以及如何执行这些表达式。
本章将讨论以下主题:
- 创建数据库
- 创建数据库结构
- 连接到数据库
- 释放连接
- 对数据执行创建、读取、更新和删除(CRUD)操作
Java 数据库连接(JDBC)是一种 Java 功能,允许您访问和修改数据库中的数据。它受 JDBC API(包括java.sql
、javax.sql
和java.transaction.xa
包)以及实现数据库访问接口的数据库特定类(称为数据库驱动程序)的支持,该接口由每个数据库供应商提供。
使用 JDBC 意味着编写 Java 代码,使用 JDBC API 的接口和类以及特定于数据库的驱动程序来管理数据库中的数据,该驱动程序知道如何与特定数据库建立连接。使用这个连接,应用就可以发出用 SQL 编写的请求。
当然,我们这里只指理解 SQL 的数据库。它们被称为关系型或表格型数据库管理系统(数据库管理系统),构成了目前使用的绝大多数数据库管理系统——尽管也使用了一些替代方法(例如导航数据库和 NoSQL)。
java.sql
和javax.sql
包包含在 Java 平台标准版(Java SE)中。javax.sql
包包含支持语句池、分布式事务和行集的DataSource
接口
创建数据库包括以下八个步骤:
- 按照供应商的说明安装数据库
- 创建数据库用户、数据库、模式、表、视图、存储过程以及支持应用的数据模型所必需的任何其他内容
- 向该应用添加对具有特定于数据库的驱动程序的
.jar
文件的依赖关系 - 从应用连接到数据库
- 构造 SQL 语句
- 执行 SQL 语句
- 根据应用的需要使用执行结果
- 释放(即关闭)数据库连接和在该过程中打开的任何其他资源
步骤 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
关键字表示该字段是一个连续整数,每次创建新记录时数据库都会生成该整数。生成顺序整数的其他选项有SMALLSERIAL
和BIGSERIAL
;它们的大小和可能值的范围不同:
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_name
、last_name
和dob
的组合来制作复合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;
也可以使用ALTER
SQL 命令更改现有表;例如,我们可以添加列地址:
ALTER table person add column address VARCHAR;
如果您不确定该列是否已经存在,可以添加IF EXISTS
或IF 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
。传入属性的键的名称对于所有主要数据库都是相同的,但其中一些是特定于数据库的。因此,请阅读数据库供应商文档以了解更多详细信息。
或者,对于只通过user
和password
的情况,我们可以使用重载的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
)。
但是,您不需要自己管理连接池。有几种成熟的框架可以为您做到这一点,比如 HikariCP、Vibur 和公共 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.AutoCloseable
或java.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 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 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
运算符值可以使用AND
和OR
逻辑运算符组合,并用括号( )
分组。
例如,下面的方法从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 table_name SET column1=value1,column2=value2,... WHERE clause;
我们可以使用此语句将其中一条记录中的名字从原始值John
更改为新值Jim
:
update person set first_name = 'Jim' where last_name = 'Adams';
没有WHERE
子句,表的所有记录都会受到影响。
要从表中删除记录,请使用DELETE
语句,如下所示:
DELETE FROM table_name WHERE clause;
如果没有WHERE
子句,则删除表中的所有记录。对于person
表,我们可以使用以下 SQL 语句删除所有记录:
delete from person;
此外,此语句仅删除名为Jim
的记录:
delete from person where first_name = 'Jim';
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
。
我们将演示这三种方法是如何在每个语句上工作的:INSERT
、SELECT
、UPDATE
和DELETE
。
让我们尝试执行每个语句;我们将从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
表中没有记录。
在本节中,我们将尝试执行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
的返回值。
我们将用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()
方法返回null
,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 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()
方法返回null
。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()
方法更适合于INSERT
、UPDATE
和DELETE
语句。
PreparedStatement
是Statement
接口的子接口。这意味着它可以在使用Statement
接口的任何地方使用。不同之处在于PreparedStatement
被缓存在数据库中,而不是每次被调用时都被编译。这样,它就可以针对不同的输入值高效地执行多次。与Statement
类似,它可以通过prepareStatement()
方法使用相同的Connection
对象创建。
由于同一条 SQL 语句可以用于创建Statement
和PreparedStatement
,所以对于任何被多次调用的 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
接口(扩展了PreparedStatement
接口)可以用来执行存储过程,尽管有些数据库允许您使用Statement
或PreparedStatement
接口调用存储过程。CallableStatement
对象是通过prepareCall()
方法创建的,可以有三种类型的参数:
IN
输入值OUT
结果IN OUT
输入或输出值
IN
参数的设置方式与PreparedStatement
参数相同,而OUT
参数必须通过CallableStatement
的registerOutParameter()
方法注册
值得注意的是,以编程方式从 Java 执行存储过程是标准化程度最低的领域之一。例如,PostgreSQL 不直接支持存储过程,但它们可以作为函数调用,为此,通过将OUT
参数解释为返回值,对其进行了修改。另一方面,Oracle 也允许OUT
参数作为函数。
这就是为什么数据库函数和存储过程之间的以下差异只能作为一般准则,而不能作为正式定义:
- 函数有返回值,但不允许使用
OUT
参数(某些数据库除外),可以在 SQL 语句中使用。 - 存储过程没有返回值(某些数据库除外);它允许使用
OUT
参数(对于大多数数据库),并且可以使用 JDBCCallableStatement
接口执行。
您可以参考数据库文档来了解如何执行存储过程
由于存储过程是在数据库服务器上编译和存储的,CallableStatement
的execute()
方法对同一条 SQL 语句的性能优于Statement
或PreparedStatement
接口的相应方法。这就是为什么很多 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 语言的简短介绍描述了如何使用Statement
、PreparedStatement
和CallableStatement
创建数据库及其结构、如何修改数据库以及如何执行 SQL 语句。
在下一章中,我们将描述和讨论最流行的网络协议,演示如何使用它们,以及如何使用最新的 Java HTTP 客户端 API 实现客户端-服务器通信。所回顾的协议包括基于 TCP、UDP 和 URL 的通信协议的 Java 实现
-
选择所有正确的语句:
- JDBC 代表 Java 数据库通信。
- JDBC API 包括
java.db
包。 - JDBC API 随 Java 安装而来。
- JDBC API 包括所有主要 DBMSE 的驱动程序。
-
选择所有正确的语句:
- 可以使用
CREATE
语句创建数据库表。 - 可以使用
UPDATE
语句更改数据库表。 - 可以使用
DELETE
语句删除数据库表。 - 每个数据库列都可以有一个索引。
- 可以使用
-
选择所有正确的语句:
- 要连接到数据库,可以使用
Connect
类。 - 必须关闭每个数据库连接。
- 同一数据库连接可用于许多操作。
- 可以合并数据库连接。
- 要连接到数据库,可以使用
-
选择所有正确的语句:
- 可以使用资源尝试结构自动关闭数据库连接。
- 可以使用
finally
块构造关闭数据库连接。 - 可以使用
catch
块关闭数据库连接。 - 一个数据库连接可以在没有
try
块的情况下关闭。
-
选择所有正确的语句:
INSERT
语句包含一个表名。INSERT
语句包括列名。INSERT
语句包含值。INSERT
语句包含约束。
-
选择所有正确的语句:
SELECT
语句必须包含表名。SELECT
语句必须包含列名。SELECT
语句必须包含WHERE
子句。SELECT
语句可以包括ORDER
子句。
-
选择所有正确的语句:
UPDATE
语句必须包含表名。UPDATE
语句必须包含列名。UPDATE
语句可以包括WHERE
子句。UPDATE
语句可以包括ORDER
子句。
-
选择所有正确的语句:
DELETE
语句必须包含表名。DELETE
语句必须包含列名。DELETE
语句可以包括WHERE
子句。DELETE
语句可以包括ORDER
子句。
-
选择
Statement
接口的execute()
方法的所有正确语句:- 它接收 SQL 语句。
- 它返回一个
ResultSet
对象。 - 调用
execute()
后,Statement
对象可能返回数据。 - 调用
execute()
后,Statement
对象可能返回受影响的记录数
-
选择
Statement
接口的executeQuery()
方法的所有正确语句: 1. 它接收 SQL 语句。 2. 它返回一个ResultSet
对象。 3. 调用executeQuery()
后,Statement
对象可能返回数据。 4. 调用executeQuery()
后,Statement
对象可能返回受影响的记录数。 -
选择接口
Statement
的executeUpdate()
方法的所有正确语句: 1. 它接收 SQL 语句。 2. 它返回一个ResultSet
对象。 3. 调用executeUpdate()
后,Statement
对象可能返回数据。 4.Statement
对象返回调用executeUpdate()
后受影响的记录数。 -
选择所有关于
PreparedStatement
接口的正确语句: 1. 它扩展自Statement
。 2. 类型为PreparedStatement
的对象是通过prepareStatement()
方法创建的。 3. 它总是比Statement
更有效。 4. 它导致数据库中的模板只创建一次。 -
选择所有关于
CallableStatement
接口的正确语句: 1. 它扩展自PreparedStatement
。 2. 类型为CallableStatement
的对象是通过prepareCall()
方法创建的。 3. 它总是比PreparedStatement
更有效。 4. 它导致数据库中的模板只创建一次。