【问题标题】:Java database interaction patternJava数据库交互模式
【发布时间】:2014-07-17 22:51:53
【问题描述】:

我实际上是在构建一个 Java 应用程序来处理和响应 RPC 事件。我发现自己一直在做以下事情,而我的 Java 知识正在碰壁。

        PreparedStatement preparedStatement = null;

        try {
            preparedStatement = conn.prepareStatement(removeFollowersStmt);
            preparedStatement.setLong(1, Long.parseLong(conversation) );
            preparedStatement.setLong(2, Long.parseLong(userId) );
            preparedStatement.executeUpdate();

            return true;
        } catch (SQLException e) {
            e.printStackTrace();
            return false;
        }
        finally
        {
            try {
                assert preparedStatement != null;
                preparedStatement.clearParameters();
                preparedStatement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

理想情况下,我只执行一次 try/catch/finally 并且能够从 try 中调用各种数据库交互。

我对 java 不够熟悉,无法做到这一点,但我的想法可能是我可以创建一个接受闭包的函数,该闭包将在 try 中调用?

【问题讨论】:

  • 你的问题到底是什么?
  • 我在每个函数中重复 try/catch/finally,如果可能的话,我只需要这样做一次。
  • 因为除了 finally 块之外,在代码中的其他任何地方关闭连接都不好,并且由于连接关闭会引发您想要处理的异常,所以您别无选择,只能有 2 个 try/catch 块。
  • 在关闭之前调用clearParameters 没有意义,你的assert 也没有意义:preparedStatement 在这里可能为空!请改用try-with-resources!

标签: java jdbc lambda java-8 try-with-resources


【解决方案1】:

看起来您已经发现对需要关闭的事物(例如 JDBC Statement)进行适当的异常处理是一件非常痛苦的事情。 Java 8 lambdas 在这里可能会有所帮助,但这是 Java 7 中引入的 try-with-resources statement 的教科书示例。事实上,该教程有一个 JDBC 示例,尽管与您尝试的有点不同在这里做。

在我们可以应用 try-with-resources 之前,我们需要仔细查看您的 finally 块。首先,preparedStatement != null 的断言在这一点上实际上是不正确的。 try 块顶部的conn.prepareStatement 语句可以在分配preparedStatement 之前抛出SQLException,因此在finally 块执行时它实际上可能仍然为null。 (大多数人在这里添加一个空检查,并且只有在它不为空时才关闭该语句。) try-with-resources 语句通过在 try-finally 语句的 finally- block 负责关闭资源。

其次,在断言之后调用clearParameters。我不认为这是必要的。该语句即将关闭,preparedStatement 变量即将超出范围,因此它将变得无法访问,因此被垃圾回收。清除参数应该不会有任何效果。

考虑到这几点,很明显finally块的主要职责是关闭语句,处理关闭操作中的任何SQLException,允许封闭方法正常返回。这几乎就是 try-with-resources 所做的。

重写代码以使用 try-with-resources 得到以下结果:

    try (PreparedStatement preparedStatement = conn.prepareStatement(removeFollowersStmt)) {
        preparedStatement.setLong(1, Long.parseLong(conversation) );
        preparedStatement.setLong(2, Long.parseLong(userId) );
        preparedStatement.executeUpdate();
        return true;
    } catch (SQLException e) {
        e.printStackTrace();
        return false;
    }

没错,整个finally-block都可以丢掉!但是,确切的行为与您的原始代码有些不同。不同之处在于如果通过executeUpdate 的所有操作都成功,但关闭语句会抛出SQLException。在原始代码中,将打印堆栈跟踪,并且该方法将返回 true

在修改后的代码中,close 调用的异常将被此处的单个 catch 子句捕获,该子句将打印堆栈跟踪并返回 false。我不知道这是否正确。我的印象是,如果关闭语句引发异常,则可能意味着先前执行的更新实际上并未成功。如果是这样,那么在这里返回false 是正确的。 (但我不是 JDBC 专家。)

这比以前更好,但仍然需要在每个语句执行周围添加 try-catch 样板。你能重用这个结构并传入一个lambda吗?我想是的,但我们必须先做一些准备。该方法将需要一个用于创建PreparedStatement 的SQL 字符串和一个负责将参数设置到语句中的lambda。这里的问题是PreparedStatement 的设置器都可以抛出SQLException。 Java 8 的 java.util.function 包中的内置函数式接口都没有处理这个问题,所以我们必须创建自己的函数式接口:

interface StatementPreparer {
    void prepare(PreparedStatement ps) throws SQLException;
}

现在我们有了这个,让我们编写一个方法来准备一个语句并执行它,处理异常,并返回一个布尔状态:

boolean update(String sql, StatementPreparer sp) {
    try (PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
        sp.prepare(preparedStatement);
        preparedStatement.executeUpdate();
        return true;
    } catch (SQLException e) {
        e.printStackTrace();
        return false;
    }
}

现在在想要执行实际工作的代码中,它可以发出如下调用:

boolean result = update("delete from followers where conv = ? and userid = ?",
    preparedStatement -> {
        preparedStatement.setLong(1, Long.parseLong(conversation) );
        preparedStatement.setLong(2, Long.parseLong(userId) );
    });

【讨论】:

  • 我正在与一个 postgres 数据库进行交互,并且我正在尝试保持一个连接。这可能是我在其他地方做错了,但我发现如果我没有调用 clearParameters,那么下次我会调用 conn.prepareStatement(sql) 即使在设置新参数时它也会使用设置的参数执行第一次。我将尝试这两种解决方案。我的 Java 排骨相当陈旧,所以感谢您耐心解释。
  • @Vinnyt 嗯,这是奇怪的行为,JDBC 允许驱动程序有很多奇怪的行为。如果这仍然是个问题,您可以在我的示例中的 try 块的前两行(对 sp.prepareexecuteUpdate 的调用)在嵌套的 try 语句中使用 finally 子句清除参数。有点丑,但至少你只需要做一次。
【解决方案2】:

我猜这是一个网络应用程序,因为“Followers”这个词。以下内容也适用于独立应用程序,但会有所不同。

您的代码存在两个概念性问题,这让您的生活变得比实际需要的更加艰难:

1.您试图在错误的地方处理异常。

如果你发现自己在写这样的代码:

try {
    doSomething()
} catch( SomeException e ){
    e.printStackTrace()
}

那么您的程序的“布局”有问题。如果将异常处理移到调用链的某个位置会更好。

如果这是一个 Web 应用程序,请在 servlet 中执行。所以在你的情况下,这将是:

void executeMyStatements throws SQLException {
    try (PreparedStatement preparedStatement = conn.prepareStatement(removeFollowersStmt))
        preparedStatement.setLong(1, Long.parseLong(conversation) );
        preparedStatement.setLong(2, Long.parseLong(userId) );
        preparedStatement.executeUpdate();
    }
    return true;
}

在某处

MyServlet extends HttpServlet {

    void doGet( ... ){

        try {
             executeMyStatements()
             doSomeMoreStuff()
             executeMyOtherStatements()

        } catch( Throwable t ){

             doSomethingMeaningfullWithException( t );
             // e.g. t.printStackTrace( response.getOutputStream() );
             // logger.error( t ); ...
        }
    }
}

2。使用事务

(也许你仍然在这样做,但以防万一)

交易会帮助你。如果前一个语句因您捕获的异常而失败,您真的确定继续执行下一个语句是有意义的吗?或者您的数据库是否处于您最好从您想做的事情重新开始的状态?

这非常适合在其他地方处理异常的方法。您基本上想要做的是:(伪代码

try{ 
    Connection con = createConnectionSomehow();
    Transaction transaction = con.startTransactionSomehow();

    executeSomeStatements();
    doStuff()
    doDeeperStuffWithMoreStatementsWhichCallOtherStatementsDeepBelowInMoria();

} catch( Exception t ){

    transaction.rollback();
    doSomethingUsefullWith( t );

} finally {

    transactions.commit();
    connection.close()
}

您必须在finally 中用if != null 包装一些语句,以便更清楚地显示主要概念,这应该不难。

哦,顺便说一句:您在创建语句时使用了parseLong。这将失败有一天或另一个NumberFormatException这是一个RuntimeException,不需要被抓住。在您的方法中,您只处理 SQLException ,因此另一个将向上传播并且(在最坏的情况下,在独立应用程序中)将使应用程序崩溃。如果你使用我的方法,这个异常会被catch( Throwable t ) 捕获,并且会被优雅地处理。

以建议的方式组织您的代码将使您的生活更轻松,并避免您不喜欢的代码重复,同时提高总体代码质量和稳定性。如果出现问题,它将为您提供更好的错误处理。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-21
    相关资源
    最近更新 更多