除了Answer by rzwitserloot中的优点外,我还可以提供一些说明并给出示例代码。
JDBC 驱动程序自动加载
使用Class.forName 加载JDBC driver 是老派,从最早的Java 版本开始。
在现代 Java 中,没有必要这样做。 JDBC 使用 Java 中的 Service Provider Interfaces (SPI) 工具 (Wikipedia) 自动定位和加载可用的 JDBC 驱动程序。
DataSource
使用DataSource 是获得数据库连接的首选方法。见Tutorial by Oracle。 DataSource 对象包含您的登录信息以及新连接所需的设置。使用DataSource 可以让您在部署时使用externalize this information,而不是在您的代码库中使用hard-coding 此信息。
您的 JDBC 驱动程序可能为直接连接提供了 DataSource 的实现。您还可以使用其他实现,例如用于连接池的实现。
private DataSource configureDataSource ( )
{
System.out.println( "INFO - `configureDataSource` method. " + Instant.now() );
com.mysql.cj.jdbc.MysqlDataSource dataSource = Objects.requireNonNull( new com.mysql.cj.jdbc.MysqlDataSource() ); // Implementation of `DataSource` for this specific database engine.
dataSource.setServerName( "your_server_address" );
dataSource.setPortNumber( some_port_number );
dataSource.setDatabaseName( "your_database_name" );
dataSource.setUser( "scott" );
dataSource.setPassword( "tiger" );
return dataSource;
}
实例化一个DataSource 对象,并保留它。根据需要将其传递给您的各种数据库方法。
DataSource dataSource = this.configureDataSource();
删除表
首先,在试验时,您可能想要删除任何现有的表,然后重新构建它。
在此示例中,我们需要一个名为 event_ 的表。
请注意我们如何使用 try-with-resources 语法,正如另一个答案中所建议的那样。当控制流离开try 块时,括号内列出的资源会自动关闭。无论是成功离开块,还是因为抛出异常(或错误)而离开,都会发生这种关闭。使用 try-with-resources 可以使您的代码更整洁、更易于遵循,并且样板代码更少。
对于 SQL 字符串,我们使用 Java 15 中的新功能text blocks。三重引号字符标记每个文本块。对于格式化 SQL、XML、JSON 等的 sn-ps 非常方便。
请注意,我们使用 Statement 而不是 PreparedStatement,因为我们的 SQL 没有不受信任的文本。如果在 SQL 中使用来自用户或其他不受信任来源的文本,请始终使用 PreparedStatement。
private void dropTable ( DataSource dataSource )
{
System.out.println( "INFO - `dropTable` method. " + Instant.now() );
try ( Connection conn = dataSource.getConnection() )
{
String sql = """
DROP TABLE IF EXISTS event_
;
""";
System.out.println( "sql: \n" + sql );
try ( Statement stmt = conn.createStatement() )
{
stmt.execute( sql );
}
}
catch ( SQLException e )
{
e.printStackTrace();
}
}
创建表
让我们创建用于此示例的表。
private void createTable ( DataSource dataSource )
{
System.out.println( "INFO - `createTable` method. " + Instant.now() );
try ( Connection conn = dataSource.getConnection() )
{
String sql = """
CREATE TABLE IF NOT EXISTS event_
(
id_ INT NOT NULL AUTO_INCREMENT PRIMARY KEY , -- ⬅ `identity` = auto-incrementing integer number.
title_ VARCHAR ( 30 ) NOT NULL ,
date_ DATE NOT NULL
)
;
""";
System.out.println( "sql: \n" + sql );
try ( Statement stmt = conn.createStatement() ; )
{
stmt.execute( sql );
}
}
catch ( SQLException e )
{
e.printStackTrace();
}
}
注意我们如何使用两个 try-with-resources,一个嵌套在另一个中。如果我们将 SQL 字符串创建代码上移,我们可以声明和实例化 conn 和 stmt 资源是单个 try-with-resources。
否则,此代码与上面的代码非常相似。我们信任 SQL 文本,所以我们使用Statement。
private void createTable ( DataSource dataSource )
{
System.out.println( "INFO - `createTable` method. " + Instant.now() );
String sql = """
CREATE TABLE IF NOT EXISTS event_
(
id_ INT NOT NULL AUTO_INCREMENT PRIMARY KEY , -- ⬅ `identity` = auto-incrementing integer number.
title_ VARCHAR ( 30 ) NOT NULL ,
date_ DATE NOT NULL
)
;
""";
System.out.println( "sql: \n" + sql );
try (
Connection conn = dataSource.getConnection() ;
Statement stmt = conn.createStatement() ;
)
{
stmt.execute( sql );
}
catch ( SQLException e )
{
e.printStackTrace();
}
}
插入行
接下来我们在表格中插入一些行。
在实际应用中,我们可能会使用用户输入的数据或从其他不受信任的来源获得的数据。所以这里我们使用PreparedStatement。我们碰巧使用了诸如"Dog Show" 之类的硬编码字符串,但这些值很可能是实际应用中的变量。
JDBC 4.2 及更高版本支持 java.time 类型。但是,没有添加特定类型的方法,例如 setLocalDate(莫名其妙)。所以我们使用setObject/getObject。
此代码将三行添加到表中。
private void insertRows ( DataSource dataSource )
{
System.out.println( "INFO - `insertRows` method. " + Instant.now() );
String sql = """
INSERT INTO event_ ( title_ , date_ )
VALUES ( ? , ? )
;
""";
try (
Connection conn = dataSource.getConnection() ;
PreparedStatement pstmt = conn.prepareStatement( sql ) ;
)
{
pstmt.setString( 1 , "Dog Show" );
pstmt.setObject( 2 , LocalDate.of( 2022 , Month.JANUARY , 21 ) );
pstmt.executeUpdate();
pstmt.setString( 1 , "Cat Show" );
pstmt.setObject( 2 , LocalDate.of( 2022 , Month.FEBRUARY , 22 ) );
pstmt.executeUpdate();
pstmt.setString( 1 , "Bird Show" );
pstmt.setObject( 2 , LocalDate.of( 2022 , Month.MARCH , 23 ) );
pstmt.executeUpdate();
}
catch ( SQLException e )
{
e.printStackTrace();
}
}
转储表
为了验证我们的行是否成功插入,让我们将此表的内容转储到控制台。
现在我们有一个ResultSet 来检查。
问题是您的问题代码是您试图在 JDBC 代码之外访问 ResultSet。但是 JDBC 代码需要关闭它的资源。这些资源中包括任何ResultSet。您需要在 JDBC 代码块中访问您的 ResultSet 对象。这里我们简单地从结果集中的每一行中获取值,然后将该值写入System.out。
private void dumpTable ( DataSource dataSource )
{
System.out.println( "INFO - `dumpTable` method. " + Instant.now() );
String sql = "SELECT * FROM event_ ;";
try (
Connection conn = dataSource.getConnection() ;
Statement stmt = conn.createStatement() ;
ResultSet rs = stmt.executeQuery( sql ) ;
)
{
System.out.println( "-------| event_ table |--------------------" );
while ( rs.next() )
{
//Retrieve by column name
int id = rs.getInt( "id_" );
String title = rs.getString( "title_" );
LocalDate date = rs.getObject( "date_" , LocalDate.class );
System.out.println( "id_=" + id + " | title_=" + title + " | date_=" + date );
}
}
catch ( SQLException e )
{
e.printStackTrace();
}
}
运行时,我们得到以下信息。
INFO - `dumpTable` method. 2021-03-11T02:31:22.769442Z
-------| event_ table |--------------------
id_=1 | title_=Dog Show | date_=2022-01-21
id_=2 | title_=Cat Show | date_=2022-02-22
id_=3 | title_=Bird Show | date_=2022-03-23
循环结果集
您问题的主要部分是询问如何循环结果集。上面的代码就是这样做的。
首先,我们获得了一个ResultSet 作为调用Statement#executeQuery 返回的值。然后我们处理该结果集。结果集一次只能处理一行。因此,“光标”会跟踪当前在哪一行。
处理结果集的关键部分是在您的ResultSet 对象上调用next。
next 方法做了三件事:
- 在第一次使用时将光标移动到第一行。
- 在连续使用时将光标移至下一行。
- 如果行已排队等待访问,则返回
true。如果光标已移过结果集中的最后一行,则返回 false。
因为该方法返回一个布尔值,我们可以连续循环,直到遇到false。
共享数据
当然,在实际应用中,我们所做的不仅仅是将数据库值转储到控制台。我们希望与应用的其他部分共享这些数据。
如上所述,在另一个答案中,您不能通过共享 ResultSet 对象本身来与应用程序的其他部分共享数据。相反,您必须将数据从ResultSet 复制到其他对象中。正如另一个答案所暗示的那样,您可以选择框架来协助完成这项工作。或者,您可以自己复制数据。
在 Java 16 中,透明且不可变地共享此类数据的明显方法是使用新的 records 功能。记录是声明类的一种简单方式。您只需在一对括号内声明每个成员字段的类型和名称。编译器隐式创建构造函数、getter、equals & hashCode 和 toString。
public record Event(Integer id , String title , LocalDate happening) {}
以下代码类似于dumpTable 方法。此代码将从数据库获取的数据传递给Event 记录的构造函数以实例化Event 对象。每个Event 对象都被添加到一个集合中。集合返回给调用方法。
通常最好从不知道调用者的方法返回不可变数据。作为记录,Event 上的每个字段都是只读的(final),immutable 也是浅的。所有三种字段类型(Integer、String 和 LocalDate)都设计为不可变的,因此每个 Event 都是不可变的。我们获取行的唯一可变部分是它们的容器,ArrayList。因此,我们通过传递给List.copyOf 从该列表中创建一个unmodifiable list。现在我们返回的是完全不可变的数据。
private List < Event > fetchEvents ( DataSource dataSource )
{
System.out.println( "INFO - `fetchEvents` method. " + Instant.now() );
List < Event > events = new ArrayList <>();
String sql = "SELECT * FROM event_ ;";
try (
Connection conn = dataSource.getConnection() ;
Statement stmt = conn.createStatement() ;
ResultSet rs = stmt.executeQuery( sql ) ;
)
{
while ( rs.next() )
{
//Retrieve by column name
int id = rs.getInt( "id_" );
String title = rs.getString( "title_" );
LocalDate date = rs.getObject( "date_" , LocalDate.class );
Event event = new Event( id , title , date ); // Instantiate a record, an `Event` object.
events.add( event );
System.out.println( event );
}
}
catch ( SQLException e )
{
e.printStackTrace();
}
return List.copyOf( events ); // Return an unmodifiable list produced by `List.copyOf`.
}
结果是Event 对象的列表。调用方法可以对这些对象做任何事情。
运行示例
您可以像这样运行这些方法:
DataSource dataSource = this.configureDataSource();
this.dropTable( dataSource );
this.createTable( dataSource );
this.insertRows( dataSource );
this.dumpTable( dataSource );
List < Event > events = this.fetchEvents(dataSource);
System.out.println( "events = " + events );
运行时:
events = [Event[id=1, title=Dog Show, happening=2022-01-21], Event[id=2, title=Cat Show, happening=2022-02-22], Event[id=3, title=Bird Show, happening=2022-03-23]]
技术细节
上面的代码在 IntelliJ 2021.1 beta 中运行,在 Java 16 早期访问(候选版本)上运行,在 macOS Mojave 上,作为 DigitalOcean.com 托管的托管数据库服务连接到 MySQL 8,与此 JDBC 驱动程序连接MySQL 通过 Maven POM 依赖:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
RowSet
从数据库共享数据的另一种方法是通过RowSet 接口和实现。见tutorial by Oracle。
虽然我一直不明白为什么,但我很少看到这些被提及。这似乎是JDBC 的“休眠”功能。
RowSet 接口扩展了ResultSet 接口。与ResultSet 不同,RowSet 设计用于在 JDBC 代码之外使用。 RowSet 可以重新连接到数据库以检索新数据或写入更改。
JdbcRowSet 维护与数据库的连接。
无需维护与数据库的连接即可使用CachedRowSet。