Как я должен использовать 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();
}