Вопрос-ответ

How should I use try-with-resources with JDBC?

Как я должен использовать try-with-resources с JDBC?

У меня есть метод получения пользователей из базы данных с помощью JDBC:

public List<User> getUser(int userId) {
String sql = "SELECT id, name FROM users WHERE id = ?";
List<User> users = new ArrayList<User>();
try {
Connection con = DriverManager.getConnection(myConnectionURL);
PreparedStatement ps = con.prepareStatement(sql);
ps.setInt(1, userId);
ResultSet rs = ps.executeQuery();
while(rs.next()) {
users.add(new User(rs.getInt("id"), rs.getString("name")));
}
rs.close();
ps.close();
con.close();
} catch (SQLException e) {
e.printStackTrace();
}
return users;
}

Как я должен использовать Java 7 try-with-resources для улучшения этого кода?

Я пробовал использовать приведенный ниже код, но он использует много try блоков и не сильно улучшает читаемость. Должен ли я использовать try-with-resources по-другому?

public List<User> getUser(int userId) {
String sql = "SELECT id, name FROM users WHERE id = ?";
List<User> users = new ArrayList<>();
try {
try (Connection con = DriverManager.getConnection(myConnectionURL);
PreparedStatement ps = con.prepareStatement(sql);) {
ps.setInt(1, userId);
try (ResultSet rs = ps.executeQuery();) {
while(rs.next()) {
users.add(new User(rs.getInt("id"), rs.getString("name")));
}
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return users;
}
Переведено автоматически
Ответ 1

Я понимаю, что на этот вопрос уже давно был дан ответ, но хочу предложить дополнительный подход, который позволяет избежать вложенного двойного блока try-with-resources.

public List<User> getUser(int userId) {
try (Connection con = DriverManager.getConnection(myConnectionURL);
PreparedStatement ps = createPreparedStatement(con, userId);
ResultSet rs = ps.executeQuery()) {

// process the resultset here, all resources will be cleaned up

} catch (SQLException e) {
e.printStackTrace();
}
}

private PreparedStatement createPreparedStatement(Connection con, int userId) throws SQLException {
String sql = "SELECT id, username FROM users WHERE id = ?";
PreparedStatement ps = con.prepareStatement(sql);
ps.setInt(1, userId);
return ps;
}
Ответ 2

В вашем примере нет необходимости во внешней попытке, поэтому вы можете, по крайней мере, перейти с 3 на 2, а также вам не нужно закрывать ; в конце списка ресурсов. Преимущество использования двух блоков try заключается в том, что весь ваш код присутствует заранее, поэтому вам не нужно обращаться к отдельному методу:

public List<User> getUser(int userId) {
String sql = "SELECT id, username FROM users WHERE id = ?";
List<User> users = new ArrayList<>();
try (Connection con = DriverManager.getConnection(myConnectionURL);
PreparedStatement ps = con.prepareStatement(sql)) {
ps.setInt(1, userId);
try (ResultSet rs = ps.executeQuery()) {
while(rs.next()) {
users.add(new User(rs.getInt("id"), rs.getString("name")));
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return users;
}
Ответ 3

Как заявляли другие, ваш код в основном правильный, хотя внешний try не нужен. Вот еще несколько мыслей.

DataSource

Другие ответы здесь правильные и хорошие, такие как принятый ответ от bpgergo. Но ни в одном из них не показано использование DataSource, обычно рекомендуемое вместо использования DriverManager в современной Java.

Итак, для полноты картины, вот полный пример, который извлекает текущую дату с сервера базы данных. Здесь используется база данных Postgres. Любая другая база данных работала бы аналогично. Вы бы заменили использование org.postgresql.ds.PGSimpleDataSource реализацией DataSource, подходящей для вашей базы данных. Реализация, вероятно, предоставляется вашим конкретным драйвером или пулом подключений, если вы пойдете по этому пути.

DataSourceРеализацию не нужно закрывать, потому что она никогда не “открывается”. A DataSource не является ресурсом, не подключен к базе данных, поэтому он не поддерживает сетевые подключения и ресурсы на сервере базы данных. A DataSource - это просто информация, необходимая при установлении соединения с базой данных, с сетевым именем или адресом сервера базы данных, именем пользователя, паролем пользователя и различными параметрами, которые вы хотите указать при конечном установлении соединения. Таким образом, ваш DataSource объект реализации не помещается в круглые скобки try-with-resources.

Цель DataSource заключается в экстернализации информации о вашем подключении к базе данных. Если вы жестко прописали имя пользователя, пароль и тому подобное в своем исходном коде, то изменение конфигурации вашего сервера баз данных означает необходимость перекомпиляции и повторного развертывания вашего кода — неинтересно. Вместо этого такие сведения о конфигурации базы данных должны храниться вне вашего исходного кода, а затем извлекаться во время выполнения. Вы можете получить сведения о конфигурации через JNDI с сервера именования и каталогов, такого как LDAP. Или вы можете получить данные из контейнера сервлетов или с сервера Jakarta EE, на котором запущено ваше приложение.

Вложенный try-with-resources

В вашем коде правильно используются вложенные инструкции try-with-resources.

Обратите внимание, что в приведенном ниже примере кода мы также используем синтаксис try-with-resources дважды, один из которых вложен в другой. Внешний try определяет два ресурса: Connection и PreparedStatement. Внутренний try определяет ResultSet ресурс. Это обычная структура кода.

If an exception is thrown from the inner one, and not caught there, the ResultSet resource will automatically be closed (if it exists, is not null). Following that, the PreparedStatement will be closed, and lastly the Connection is closed. Resources are automatically closed in reverse order in which they were declared within the try-with-resource statements.

The example code here is overly simplistic. As written, it could be executed with a single try-with-resources statement. But in a real work you will likely be doing more work between the nested pair of try calls. For example, you may be extracting values from your user-interface or a POJO, and then passing those to fulfill ? placeholders within your SQL via calls to PreparedStatement::set… methods.

Syntax notes

Trailing semicolon

Notice that the semicolon trailing the last resource statement within the parentheses of the try-with-resources is optional. I include it in my own work for two reasons: Consistency and it looks complete, and it makes copy-pasting a mix of lines easier without having to worry about end-of-line semicolons. Your IDE may flag the last semicolon as superfluous, but there is no harm in leaving it.

Java 9 – Use existing vars in try-with-resources

New in Java 9 is an enhancement to try-with-resources syntax. We can now declare and populate the resources outside the parentheses of the try statement. I have not yet found this useful for JDBC resources, but keep it in mind in your own work.

ResultSet should close itself, but may not

In an ideal world the ResultSet would close itself as the documentation promises:


A ResultSet object is automatically closed when the Statement object that generated it is closed, re-executed, or used to retrieve the next result from a sequence of multiple results.


Unfortunately, in the past some JDBC drivers infamously failed to fulfill this promise. As a result, many JDBC programmers learned to explicitly close all their JDBC resources including Connection, PreparedStatement, and ResultSet too. The modern try-with-resources syntax has made doing so easier, and with more compact code. Notice that the Java team went to the bother of marking ResultSet as AutoCloseable, and I suggest we make use of that. Using a try-with-resources around all your JDBC resources makes your code more self-documenting as to your intentions.

Code example

package work.basil.example;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.Objects;

public class App
{
public static void main ( String[] args )
{
App app = new App();
app.doIt();
}

private void doIt ( )
{
System.out.println( "Hello World!" );

org.postgresql.ds.PGSimpleDataSource dataSource = new org.postgresql.ds.PGSimpleDataSource();

dataSource.setServerName( "1.2.3.4" );
dataSource.setPortNumber( 5432 );

dataSource.setDatabaseName( "example_db_" );
dataSource.setUser( "scott" );
dataSource.setPassword( "tiger" );

dataSource.setApplicationName( "ExampleApp" );

System.out.println( "INFO - Attempting to connect to database: " );
if ( Objects.nonNull( dataSource ) )
{
String sql = "SELECT CURRENT_DATE ;";
try (
Connection conn = dataSource.getConnection() ;
PreparedStatement ps = conn.prepareStatement( sql ) ;
)
{
… make `PreparedStatement::set…` calls here.
try (
ResultSet rs = ps.executeQuery() ;
)
{
if ( rs.next() )
{
LocalDate ld = rs.getObject( 1 , LocalDate.class );
System.out.println( "INFO - date is " + ld );
}
}
}
catch ( SQLException e )
{
e.printStackTrace();
}
}

System.out.println( "INFO - all done." );
}
}
Ответ 4

Как насчет создания дополнительного класса-оболочки?

package com.naveen.research.sql;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public abstract class PreparedStatementWrapper implements AutoCloseable {

protected PreparedStatement stat;

public PreparedStatementWrapper(Connection con, String query, Object ... params) throws SQLException {
this.stat = con.prepareStatement(query);
this.prepareStatement(params);
}

protected abstract void prepareStatement(Object ... params) throws SQLException;

public ResultSet executeQuery() throws SQLException {
return this.stat.executeQuery();
}

public int executeUpdate() throws SQLException {
return this.stat.executeUpdate();
}

@Override
public void close() {
try {
this.stat.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}



Затем в вызывающем классе вы можете реализовать метод prepareStatement как:

try (Connection con = DriverManager.getConnection(JDBC_URL, prop);
PreparedStatementWrapper stat = new PreparedStatementWrapper(con, query,
new Object[] { 123L, "TEST" }) {
@Override
protected void prepareStatement(Object... params) throws SQLException {
stat.setLong(1, Long.class.cast(params[0]));
stat.setString(2, String.valueOf(params[1]));
}
};
ResultSet rs = stat.executeQuery();) {
while (rs.next())
System.out.println(String.format("%s, %s", rs.getString(2), rs.getString(1)));
} catch (SQLException e) {
e.printStackTrace();
}

java jdbc