《PostgreSQL 开发指南》第30篇 Java访问PostgreSQL

JDBC(Java Database Connectivity)是 Java 语言中用于访问数据库的应用程序接口(API)。JDBC 提供了操作关系数据库的标准方法,属于 Java Standard Edition 平台的一部分。以下是 Java 应用程序通过 JDBC 访问数据库的示意图:


其中各个模块的作用如下:

  • Java 应用程序由开发人员编码完成,用于实现业务处理的逻辑和流程;

  • JDBC API 提供了统一的接口和驱动管理,实现了应用程序和 JDBC 驱动的隔离。同一套应用代码只需要切换驱动程序就可以支持不同的数据库;

  • JDBC 驱动实现了 JDBC API 中定义的接口,用于与不同的数据库进行交互;

  • 数据库提供数据的存储管理和访问控制。

本篇介绍如何通过 JDBC 连接 PostgreSQL 数据库并执行各种数据操作。

连接数据库

首先,我们需要准备开发环境。开发环境分为三个部分:Java JDK、PostgreSQL 数据库以及 JDBC 驱动。

开发 Java 应用需要使用 JDK(Java Development Kit);使用 java -version 命令检查是否已经安装 JDK,要求至少 JDK 1.8.0 版本以上。

C:\Users\dongx>java -version
java version "13.0.1" 2019-10-15
Java(TM) SE Runtime Environment (build 13.0.1+9)
Java HotSpot(TM) 64-Bit Server VM (build 13.0.1+9, mixed mode, sharing)

如果显示无法识别以上命令,表示没有安装 JDK。可以通过 Oracle 官方 Java SE JDK 地址下载并安装。安装完成之后,还需要设置环境变量(Windows):

  • 右键单击“我的电脑”,然后选择“属性”。在“高级”选项卡上,选择“环境变量”,然后新建环境变量“JAVA_HOME”,变量值为 JDK 的安装目录,例如“C:\Program Files\Java\jdk-13.0.1”。

  • 编辑环境变量“Path”,将以下内容追加到最后:%JAVA_HOME%\bin\。

PostgreSQL 数据库的安装可以参考专栏的第 03 篇 PostgreSQL 安装

最后是安装 PostgreSQL 的 JDBC 驱动。为了方便程序开发,推荐安装一个 IDE(集成开发环境),我们使用 JetBrains 出品的 IntelliJ IDEA 社区版。IntelliJ IDEA 可以使用 Maven 管理包的依赖,我们创建一个新的 Maven 项目(基于项目模板):


后在项目的 pom.xml 文件的 节点中添加以下内容:

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

Maven 会自动下载 PostgreSQL JDBC 驱动并且进行配置。

我们在项目目录中创建一个数据库的连接配置文件 db.properties,内容如下:

url=jdbc:postgresql://192.168.56.104:3306/hrdb
user=tony
password=tony

其中,url 中指定了数据库的 IP 地址、端口以及目标数据库;user 和 password 分别为连接数据库的用户和密码。

然后将项目默认创建的 App.java 文件修改如下:

package org.example;

// 导入 JDBC 和 IO 包
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;

public class App
{
public static void main( String[] args )
{
String url = null;
String user = null;
String password = null;

// 读取数据库连接配置文件
try (FileInputStream file = new FileInputStream("db.properties")) {

Properties pros = new Properties();
pros.load(file);
url = pros.getProperty("url");
user = pros.getProperty("user");
password = pros.getProperty("password");
} catch (IOException e) {
System.out.println(e.getMessage());
}

// 建立数据库连接
try (Connection conn = DriverManager.getConnection(url, user, password)) {
System.out.println("连接 PostgreSQL 数据库成功!");
} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
}

首先,通过一个 FileInputStream 对象读取数据库的连接配置文件;然后使用 try-with-resources 方式建立数据库连接,打印信息。运行该程序的结果如下:

连接 PostgreSQL 数据库成功!

创建和删除表

通过 JDBC 连接数据库并执行 DDL 语句的过程如下:

  1. 利用 DriverManager 类的 getConnection() 方法获取一个 Connection 连接对象;

  2. 使用连接对象的 createStatement() 方法创建一个 Statement 语句对象;

  3. 利用语句对象的 execute() 方法执行 SQL 语句;

  4. 释放 Statement 以及 Connection 对象资源。

创建一个新的 Java 文件 PostgreSQLDDL.java,内容如下:
package org.example;

// 导入 JDBC 和 IO 包
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;

public class PostgreSQLDDL {
public static void main(String[] args )
{
String url = null;
String user = null;
String password = null;
String sql_str = "create table users (" +
" id serial primary key," +
" name character varying(10) not null unique," +
" created_at timestamp not null" +
")";

// 读取数据库连接配置文件
try (FileInputStream file = new FileInputStream("db.properties")) {

Properties p = new Properties();
p.load(file);
url = p.getProperty("url");
user = p.getProperty("user");
password = p.getProperty("password");
} catch (IOException e) {
System.out.println(e.getMessage());
}

// 建立数据库连接,创建查询语句,并且执行语句
try (Connection conn = DriverManager.getConnection(url, user, password);
Statement ps = conn.createStatement()) {

// 执行 SQL 语句
ps.execute(sql_str);
System.out.println("成功创建 users 表!");

} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
}

首先,通过一个 FileInputStream 对象读取数据库的连接配置文件;然后使用 try-with-resources 方式建立数据库连接,创建查询语句,并且执行语句创建 users 表。运行该程序的结果如下:

成功创建 users 表!

如果 users 表已经存在,将会提示以下错误:

ERROR: relation "users" already exists

此时可以将 sql_str 的内容修改为drop table users删除并重新创建 users 表。

插入数据

通过 JDBC 连接数据库并执行插入操作的过程如下:

  1. 利用 DriverManager 类的 getConnection() 方法获取一个 Connection 连接对象;

  2. 使用连接对象的 createStatement() 方法创建一个 Statement 或者 PreparedStatement 语句对象;

  3. 利用语句对象的 execute() 或者 executeBatch() 方法执行 INSERT 语句;

  4. 释放 Statement 以及 Connection 对象资源。

创建一个新的 Java 文件 PostgreSQLInsert.java,内容如下:
package org.example;

// 导入 JDBC 和 IO 包
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
import java.sql.Timestamp;

public class PostgreSQLInsert {
public static void main(String[] args )
{
String url = null;
String user = null;
String password = null;
String sql_str = "insert into users(name, created_at) values(?, ?)";

// 读取数据库连接配置文件
try (FileInputStream file = new FileInputStream("db.properties")) {

Properties p = new Properties();
p.load(file);
url = p.getProperty("url");
user = p.getProperty("user");
password = p.getProperty("password");
} catch (IOException e) {
System.out.println(e.getMessage());
}

// 建立数据库连接,创建预编译语句,并且执行语句
try (Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement ps = conn.prepareStatement(sql_str)) {

// 设置输入参数
ps.setString(1, "tony");
ps.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
ps.addBatch();

ps.setString(1, "david");
ps.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
ps.addBatch();

// 执行批量插入操作
ps.executeBatch();
System.out.println("插入数据成功!");

} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
}

其中,sql_str 中的两个问号(?)表示两个占位符,它们的值会在运行时进行替换;然后使用 prepareStatement() 方法创建了一个 预编译的 SQL 语句,在执行该语句之前使用 setString() 和 setTimestamp() 方法替换占位符的值,并且使用 addBatch() 添加批量操作;最后执行 executeBatch() 方法进行批量插入操作。

?预编译语句可以避免 SQL 语句的重复编译,使用不同的参数多次运行语句可以提高效率,并且能够预防 SQL 注入。

该程序的执行结果如下:

插入数据成功!

如果想要查看被插入的数据,需要执行查询语句。

查询数据

通过 JDBC 连接数据库并执行查询语句的过程如下:

  1. 利用 DriverManager 类的 getConnection() 方法获取一个 Connection 连接对象;

  2. 使用连接对象的 createStatement() 方法创建一个 Statement 或者 PreparedStatement 语句对象;

  3. 利用语句对象的 executeQuery() 方法执行 SQL 语句或者存储过程,返回一个 ResultSet 结果集对象;

  4. 遍历结果集,获取并处理查询结果;

  5. 释放 ResultSet、Statement 以及 Connection 对象资源。

新建一个 Java 文件 PostgreSQLQuery.java,内容如下:
package org.example;

// 导入 JDBC 和 IO 包
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;

public class PostgreSQLQuery {
public static void main( String[] args )
{
String url = null;
String user = null;
String password = null;
String sql_str = "SELECT id, name, created_at FROM users";

// 读取数据库连接配置文件
try (FileInputStream file = new FileInputStream("db.properties")) {

Properties pros = new Properties();
pros.load(file);
url = pros.getProperty("url");
user = pros.getProperty("user");
password = pros.getProperty("password");
} catch (IOException e) {
System.out.println(e.getMessage());
}

// 建立数据库连接,创建查询语句,并且执行语句
try (Connection conn = DriverManager.getConnection(url, user, password);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql_str)) {

// 处理查询结果集
while (rs.next()) {
System.out.println(rs.getInt("id") + "\t" +
rs.getString("name") + "\t" +
rs.getTimestamp("created_at"));
}

} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
}

首先,通过一个 FileInputStream 对象读取数据库的连接配置文件;然后使用 try-with-resources 方式建立数据库连接,创建查询语句,并且执行该语句;最后使用一个 while 循环获取查询结果集,rs.next() 获取结果中的下一条记录;rs.getInt()rs.getString() 和 rs.getTimestamp() 分别用于获取记录中的整数、字符串以及时间戳字段。

该程序的输出结果如下:

1	tony	2020-06-15 15:36:13.562
2 david 2020-06-15 15:36:13.563

修改数据

我们可以通过 PreparedStatement 语句对象 executeUpdate() 方法执行更新语句。创建一个新的源文件 PostgreSQLUpdate.java:

package org.example;

// 导入 JDBC 和 IO 包
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;

public class PostgreSQLUpdate {
public static void main(String[] args )
{
String url = null;
String user = null;
String password = null;
int affectedrows = 0;
String sql_str = "update users " +
"set name = ? " +
"where id = ?";

// 读取数据库连接配置文件
try (FileInputStream file = new FileInputStream("db.properties")) {

Properties p = new Properties();
p.load(file);
url = p.getProperty("url");
user = p.getProperty("user");
password = p.getProperty("password");
} catch (IOException e) {
System.out.println(e.getMessage());
}

// 建立数据库连接,创建更新语句,并且执行语句
try (Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement ps = conn.prepareStatement(sql_str)) {

// 设置输入参数
ps.setString(1, "tom");
ps.setInt(2, 1);

// 执行更新操作
affectedrows = ps.executeUpdate();
System.out.println(String.format("更新行数: %d", affectedrows));

} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
}

其中,sql_str 中的两个问号(?)表示两个占位符,它们的值会在运行时进行替换;然后使用 prepareStatement() 方法创建了一个 PreparedStatement 预编译的 SQL 语句,在执行该语句之前使用 setInt() 和 setString() 方法替换占位符的值;最后执行 executeBatch() 方法进行更新操作,返回被更新的行数。

运行以上程序的输出结果如下:

更新行数: 1

我们再次运行上文中的查询示例,确认修改后的结果:

2	david	2020-06-15 15:36:13.563
1 tom 2020-06-15 15:36:13.562

删除数据

删除操作和插入、更新操作类似,只需要将 SQL 语句替换成 DELETE 即可。我们创建一个新的 Java 文件 PostgreSQLDelete.java:

package org.example;

// 导入 JDBC 和 IO 包
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;

public class PostgreSQLDelete {
public static void main(String[] args )
{
String url = null;
String user = null;
String password = null;
int affectedrows = 0;
String sql_str = "delete from users where id = ?";

// 读取数据库连接配置文件
try (FileInputStream file = new FileInputStream("db.properties")) {

Properties p = new Properties();
p.load(file);
url = p.getProperty("url");
user = p.getProperty("user");
password = p.getProperty("password");
} catch (IOException e) {
System.out.println(e.getMessage());
}

// 建立数据库连接,创建查询语句,并且执行语句
try (Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement ps = conn.prepareStatement(sql_str)) {

// 设置输入参数
ps.setInt(1, 1);

// 执行更新操作
affectedrows = ps.executeUpdate();
System.out.println(String.format("删除行数: %d", affectedrows));

} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
}

执行该程序输出的结果如下:

删除行数: 1

处理事务

默认情况下,JDBC 连接 PostgreSQL 时使用自动提交模式,意味着每个 SQL 语句都会自动执行事务的提交操作。如果我们想要在一个事务中执行多条 SQL 语句,需要禁用连接对象的自动提交属性,并且手动执行 COMMIT 或者 ROLLBACK 操作。

我们创建一个新的 Java 文件 PostgreSQLTransaction.java:

package org.example;

// 导入 JDBC 和 IO 包
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
import java.sql.Timestamp;

public class PostgreSQLTransaction {
public static void main(String[] args )
{
String url = null;
String user = null;
String password = null;
String sql_str = "insert into users(name, created_at) values(?, ?)";
String sql_str2 = "update users set name = ? where id = ?";

// 读取数据库连接配置文件
try (FileInputStream file = new FileInputStream("db.properties")) {

Properties p = new Properties();
p.load(file);
url = p.getProperty("url");
user = p.getProperty("user");
password = p.getProperty("password");
} catch (IOException e) {
System.out.println(e.getMessage());
}

// 建立数据库连接,创建查询语句,并且执行语句
try (Connection conn = DriverManager.getConnection(url, user, password)) {

//设置手动提交
conn.setAutoCommit(false);

try (PreparedStatement ps = conn.prepareStatement(sql_str);
PreparedStatement ps2 = conn.prepareStatement(sql_str2)) {

// 设置输入参数并执行语句
ps.setString(1, "anne");
ps.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
ps.executeUpdate();

ps2.setString(1, "anne");
ps2.setInt(2, 2);
ps2.executeUpdate();

// 提交事务
conn.commit();
System.out.println("事务提交成功!");
} catch (SQLException e) {
// 回滚事务
conn.rollback();
System.out.println(e.getMessage());
System.out.println("回滚事务!");
}
} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
}

创建连接之后,使用 setAutoCommit() 方法禁用自动提交;然后分别执行插入语句和更新语句,并提交事务;在异常处理中回滚事务并打印错误消息。

由于更新语句将 name 设置为重复值,因此该程序返回以下错误:

ERROR: duplicate key value violates unique constraint "users_name_key"
Detail: Key (name)=(tony) already exists.
回滚事务!

此时,插入语句也会被回滚;所以并不会创建 anne。

调用存储过程

利用 JDBC 中的 CallableStatement 对象可以调用 PostgreSQL 存储过程和函数。首先创建一个存储过程 add_user:

CREATE OR REPLACE PROCEDURE add_user(pv_name varchar, pd_created_at timestamp)
AS $$
BEGIN
insert into users(name, created_at)
values (pv_name, pd_created_at);
END; $$
LANGUAGE plpgsql;

CallableStatement 对象默认只支持存储函数调用;为了支持存储过程,需要将连接属性 escapeSyntaxCallMode 设置为 callIfNoReturn。我们将 db.properties 文件修改如下:

url=jdbc:postgresql://192.168.56.104:5432/hrdb?escapeSyntaxCallMode=callIfNoReturn
user=tony
password=tony

然后创建一个新的文件 PostgreSQLSP.java:

package org.example;

// 导入 JDBC 和 IO 包
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;

public class PostgreSQLSP {
public static void main( String[] args )
{
String url = null;
String user = null;
String password = null;

// 读取数据库连接配置文件
try (FileInputStream file = new FileInputStream("db.properties")) {

Properties pros = new Properties();
pros.load(file);
url = pros.getProperty("url");
user = pros.getProperty("user");
password = pros.getProperty("password");
} catch (IOException e) {
System.out.println(e.getMessage());
}

// 建立数据库连接,调用存储过程
try (Connection conn = DriverManager.getConnection(url, user, password);
CallableStatement stmt = conn.prepareCall("{call add_user( ?, ? )}");
) {
stmt.setString(1, "anne");
stmt.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
stmt.execute();
System.out.println("调用存储过程成功!");
} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
}

其中, **{ call add_user( ?, ? ) }**中的问号是占位符,表示一个输入参数;prepareCall() 方法返回一个 CallableStatement 对象,代表调用存储过程的语句;设置输入参数后执行存储过程。运行该程序输出以下内容:

调用存储过程成功!

?除了以上内容之外,JDBC 还提供了许多功能,例如连接池、事务管理以及负载均衡等,具体可以参考 PostgreSQL JDBC 文档。

在实际的应用开发中,我们不需要直接调用这些底层 JDBC 接口,而是可以利用成熟的框架,例如 Mybatis、Hibernate 和 Spring JDBC。这些框架为我们处理了所有的低层细节,包括连接管理、事务控制以及异常处理等;当然,这些框架最后还是调用了 JDBC 接口。

请使用浏览器的分享功能分享到微信等