如何在JDBC中使用try-with-resources?


148

我有一种使用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;
}

5
在第二个示例中,您不需要内部对象,try (ResultSet rs = ps.executeQuery()) {因为ResultSet对象会自动由生成该对象的Statement对象关闭
Alexander Farber

2
@AlexanderFarber不幸的是,驱动程序出现了臭名昭著的问题,无法自行关闭资源。磨难学校教导我们要始终关闭所有的JDBC资源明确,更容易使用try-与资源身边ConnectionPreparedStatementResultSet也。没有理由不这样做,因为try-with-resources使它变得如此简单,并使我们的代码更符合我们的意图。
罗勒·布尔克

Answers:


85

在您的示例中,无需进行外部尝试,因此您至少可以从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;
}

5
你怎么称呼Connection::setAutoCommit?和try之间的不允许进行此类调用。当从可能由连接池支持的数据源获取连接时,我们无法假定如何设置autoCommit。con = ps =
罗勒·布尔克

1
您通常会将连接注入到方法中(与OP问题中显示的即席方法不同),可以使用连接管理类,该类将被调用以提供或关闭连接(无论是否池化)。在该经理中,您可以指定您的连接行为
svarog

@BasilBourque,您可以DriverManager.getConnection(myConnectionURL)进入一个方法,该方法还设置autoCommit标志并返回连接(或将其设置为createPreparedStatement与前面示例中的方法等效的方法……)
rogerdpack

@rogerdpack是的,这很有意义。DataSourcegetConnection方法按照您说的做自己的实现,获取连接并根据需要对其进行配置,然后继续进行连接。
罗勒·布尔克

1
@rogerdpack感谢您的答复。我已将其更新为所选答案。
乔纳斯(Jonas)

187

我意识到这早就得到了回答,但想提出一种避免嵌套的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;
}

24
不,它被覆盖了,问题是上面的代码正在从没有声明抛出SQLException的方法内部调用prepareStatement。另外,上面的代码具有至少一个路径,其中它可以在不关闭准备语句失败(如果在主叫SETINT发生一个SQLException。)
Trejkaz

1
@Trejkaz关于不关闭PreparedStatement的可能性的要点。我没想到,但是你是对的!
珍妮·博雅斯基

2
@ArturoTena是的-订单得到保证
Jeanne Boyarsky

2
@JeanneBoyarsky还有另一种方法吗?如果不是我需要为每一个SQL语句特定createPreparedStatement方法
约翰·亚历山大·贝茨

1
关于Trejkaz的评论,createPreparedStatement无论您如何使用都是不安全的。要修复它,您必须在setInt(...)周围添加一个try-catch,捕获任何SQLException,然后在发生这种情况时调用ps.close()并重新抛出异常。但这将导致代码几乎与OP想要改进的代码一样冗长和晦涩。
Florian F

4

这是一种使用lambda和JDK 8 Supplier的简洁方法,可以将所有内容放入外部尝试中:

try (Connection con = DriverManager.getConnection(JDBC_URL, prop);
    PreparedStatement stmt = ((Supplier<PreparedStatement>)() -> {
    try {
        PreparedStatement s = con.prepareStatement("SELECT userid, name, features FROM users WHERE userid = ?");
        s.setInt(1, userid);
        return s;
    } catch (SQLException e) { throw new RuntimeException(e); }
    }).get();
    ResultSet resultSet = stmt.executeQuery()) {
}

5
这比@bpgergo描述的“经典方法”更为简洁。我不这样认为,代码更难理解。因此,请解释这种方法的优势。
rmuller

在这种情况下,我认为不需要您显式捕获SQLException。在try-with-resources上,它实际上是“可选的”。没有其他答案提及此。因此,您可能可以进一步简化。
djangofan

如果DriverManager.getConnection(JDBC_URL,prop);该怎么办?返回null?
gaurav '18

2

如何创建一个额外的包装器类?

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();
}


2
上面的评论中没有任何内容表明没有。
Trejkaz

2

正如其他人所述,尽管try不需要外部代码,但是您的代码基本上是正确的。这里还有一些想法。

DataSource

此处的其他答案是正确且良好的,例如bpgergo 接受的答案。但是没有一个显示DataSource使用,DriverManager在现代Java中通常不推荐使用。

因此,为了完整起见,下面是一个完整的示例,该示例从数据库服务器获取当前日期。这里使用的数据库是Postgres。任何其他数据库都将类似地工作。您可以使用适合您的数据库org.postgresql.ds.PGSimpleDataSource的实现来代替使用DataSource。如果您选择该驱动程序,则可能由您的特定驱动程序或连接池提供实现。

DataSource,在实现中被关闭,因为它永远不会“打开”。A DataSource不是资源,没有连接到数据库,因此它既没有网络连接也没有数据库服务器上的资源。A DataSource是与数据库建立连接时所需的简单信息,其中包含数据库服务器的网络名称或地址,用户名,用户密码以及最终建立连接时要指定的各种选项。所以你DataSource执行对象不走你的尝试,与资源括号内。

嵌套的尝试资源

您的代码正确使用了嵌套的try-with-resources语句。

请注意,在下面的示例代码中,我们还两次使用了try-with-resources语法,一个嵌套在另一个内。外部try定义了两个资源:ConnectionPreparedStatement。内部try定义ResultSet资源。这是一种常见的代码结构。

如果从内部抛出一个异常,并且没有捕获到该异常,则ResultSet资源将自动关闭(如果存在,则不为null)。之后,PreparedStatement将关闭,最后Connection关闭。在try-with-resource语句中声明资源时,资源以相反的顺序自动关闭。

此处的示例代码过于简单。如所写,可以使用单个try-with-resources语句执行。但是在实际工作中,您可能会在嵌套try调用对之间进行更多工作。例如,您可能正在从用户界面或POJO中提取值,然后传递这些值以实现?PreparedStatement::set…方法的调用 SQL中的占位符。

语法注释

尾随分号

请注意,在try-with-resources括号内的最后一个resource语句后面的分号是可选的。我将其包含在自己的工作中有两个原因:一致性并且看起来很完整,并且它使复制粘贴混合行变得更加容易,而不必担心行尾分号。您的IDE可能会将最后一个分号标记为多余,但是保留它不会有任何危害。

Java 9 –在try-with-resources中使用现有的vars

Java 9中的新增功能是对try-with-resources语法的增强。现在,我们可以在try语句括号之外声明并填充资源。我还没有发现它对JDBC资源有用,但是在您自己的工作中要牢记这一点。

ResultSet 应该自行关闭,但可能不会关闭

在理想的ResultSet情况下,文档将承诺自己会关闭:

当关闭,重新执行或用于生成多个结果序列中的下一个结果的Statement对象时,将自动关闭ResultSet对象。

不幸的是,过去某些JDBC驱动程序臭名昭著地未能实现这一承诺。其结果是,许多JDBC程序员学会了明确关闭所有的JDBC资源,包括ConnectionPreparedStatement,和ResultSet也。现代的try-with-resources语法使此操作变得更容易,并且代码更紧凑。请注意,Java团队将标记ResultSetAutoCloseable,我建议我们利用它。在所有JDBC资源周围使用try-with-resources可以使您的代码根据自己的意图进行更多的自我记录。

代码示例

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." );
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.