【发布时间】:2022-01-16 18:49:23
【问题描述】:
在 Java 中使用 SQLite 数据库时,假设我将自动提交设置为 false。发生 SQLException 时是否有必要调用rollback() 方法?或者我可以直接忽略调用它,事务将自动回滚(我在事务期间所做的所有更改都会自动撤消)?
【问题讨论】:
标签: java database sqlite rollback
在 Java 中使用 SQLite 数据库时,假设我将自动提交设置为 false。发生 SQLException 时是否有必要调用rollback() 方法?或者我可以直接忽略调用它,事务将自动回滚(我在事务期间所做的所有更改都会自动撤消)?
【问题讨论】:
标签: java database sqlite rollback
快速回答:你问的事实可能意味着你做错了。但是,如果您必须知道:是的,您需要显式回滚。
在 JDBC 级别(如果您使用 JOOQ、JDBI、Hibernate 或类似的东西,那通常是构建在 JDBC 之上的库),您有一个 Connection 实例。您可以通过 DriverManager.getConnection(...) 获得此信息 - 或者连接池为您获得了它,但确实发生了一些事情。
该连接可以在事务的中间(自动提交模式仅意味着该连接假定您打算在您关心在该连接的上下文中运行的每个 SQL 语句之后编写额外的commit(),这都是自动的commit 确实如此,但显然,如果开启,您可能处于“干净”状态,也就是说,该连接处理的最后一个命令是 COMMIT 或 ROLLBACK)。
如果它在事务的中间并且您关闭了连接,ROLLBACK 是隐含的。
连接必须做出选择,它不能保持存在,所以,它提交或回滚。该规范保证它不会仅仅为你提供乐趣,因此,它会回滚。
然后问题归结为您的具体设置。具体来说,这很危险:
try (Connection con = ...) {
con.setAutoCommit(false);
try {
try (var s = con.createStatement()) {
s.execute("DROP TABLE foobar");
}
} catch (SQLException ignore) {
// ignoring an exception usually bad idea. But for sake of example..
}
// A second statement on the same connection...
try (var s = con.createStatement()) {
s.execute("DROP TABLE quux");
}
}
就规范而言,JDBC 驱动程序可以随意抛出 SQLException,类似于“连接已中止;您必须先显式回滚,然后才能在第二条语句中再次使用它。
但是,上面的代码很糟糕。你不能在这种代码中使用事务隔离级别 SERIALIZABLE(一旦你的用户数量超过少数,应用程序就会崩溃并在一系列重试异常中烧毁),它要么是做一些无用的事情(当你有一个连接池正在使用时,为多个事务重新使用一个连接),或者正在糟糕地解决一个问题(问题:为每个事务使用一个新连接是昂贵的)。
上述危险的唯一原因是因为我们在与连接对象关联的单个 try-block 中做两件不相关的事情(即:2 个事务)。我们正在重新使用连接。这是一个坏主意:连接有与之关联的包袱:已设置的属性,是的,处于“中止”状态(在连接愿意执行任何其他 SQL)。只需关闭连接并获得一个新连接,您就可以抛弃所有这些包袱。这种包袱会导致单元测试不容易捕捉到的错误,也就是一旦触发就会花费大量金钱/眼球/善意/时间来修复的错误。客观地说,如果它避免了一个 100 倍难以捕捉的 bug,那么你肯定更喜欢 99 个易于捕捉的 bug,而这是属于后一类的那些 bug 之一。
这样做有一个问题:只需为单个事务使用一个连接,然后将其返回,这样就无需回滚,因为如果您 close() 它,连接将自动执行此操作:获取连接是相当资源-重。
因此,人们倾向于/可能应该使用连接池来避免这种成本。也不要在这里写你自己的;使用HikariCP 或类似的东西。这些工具为您汇集连接:您无需调用DriverManager.getConnection,而是向 HikariCP 索要一个,并在完成后将连接交还给 HikariCP。 Hikari 将负责为您重置它,包括在连接处于事务中途时回滚,并处理任何其他每个连接的设置,使其恢复到已知状态。
常见的 DB 交互模型本质上就是这种“流程”:
someDbAccessorObject.act(db -> {
// do a single transaction here
});
就是这样。这段代码在底层做了各种各样的事情:
在该模型中,您遇到问题的情况很少见,因为您最终会陷入 '1 事务? 1 个连接!模型。通常这是昂贵的(创建连接比像往常一样回滚/提交然后继续在同一个连接对象上进行新事务要昂贵得多),但一旦使用池化器,它就归结为同样的事情。
换句话说:正确编写的 DB 代码不应该有您的问题,除非您自己编写连接池,在这种情况下,答案肯定是:显式回滚。
【讨论】: