diff --git a/README.md b/README.md index 3b98e9c..992a510 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,9 @@ Maven 依赖: ## 4. 事务 -- **`executeTransaction(consumer)`**:执行事务。传入 `ThrowingConsumer`,若内部无异常则提交,有异常则回滚。 +通过 `TransactionTemplate` 管理事务,可直接创建或通过 `SimpleJdbcTemplate.transaction()` 获取。 + +- **`execute(consumer)`**:执行事务。传入 `ThrowingConsumer`,若内部无异常则提交,有异常则回滚。 - **`commitIfTrue(predicate)`**:执行事务。传入 `ThrowingPredicate`,返回 `true` 提交,返回 `false` 或抛异常则回滚。 ## 5. 参数构建 @@ -246,7 +248,7 @@ if (result.getStatus() == BatchUpdateStatus.COMPLETED_WITH_ERRORS) { ### 6.4 事务 ```java -jdbcTemplate.executeTransaction(jdbc -> { +jdbcTemplate.transaction().execute(jdbc -> { ... jdbc.update(...); ... @@ -255,7 +257,7 @@ jdbcTemplate.executeTransaction(jdbc -> { // 无异常则自动提交 }); -jdbcTemplate.commitIfTrue(jdbc -> { +jdbcTemplate.transaction().commitIfTrue(jdbc -> { ... jdbc.update(...); ... diff --git a/src/main/java/xyz/zhouxy/jdbc/SimpleJdbcTemplate.java b/src/main/java/xyz/zhouxy/jdbc/SimpleJdbcTemplate.java index 3570e1c..c6cf363 100644 --- a/src/main/java/xyz/zhouxy/jdbc/SimpleJdbcTemplate.java +++ b/src/main/java/xyz/zhouxy/jdbc/SimpleJdbcTemplate.java @@ -26,8 +26,6 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.sql.DataSource; -import xyz.zhouxy.plusone.commons.function.ThrowingConsumer; -import xyz.zhouxy.plusone.commons.function.ThrowingPredicate; import xyz.zhouxy.plusone.commons.util.AssertTools; /** @@ -35,19 +33,23 @@ import xyz.zhouxy.plusone.commons.util.AssertTools; * *

* 对 JDBC 的简单封装,方便数据库操作,支持事务,支持批量操作,支持自定义结果集映射 - *

* * @author ZhouXY * @since 1.0.0 + * @see TransactionTemplate */ public class SimpleJdbcTemplate implements JdbcOperations { @Nonnull private final DataSource dataSource; + @Nonnull + private final TransactionTemplate transactionTemplate; + public SimpleJdbcTemplate(@Nonnull DataSource dataSource) { AssertTools.checkNotNull(dataSource); this.dataSource = dataSource; + this.transactionTemplate = new TransactionTemplate(dataSource); } // #region - query @@ -184,203 +186,10 @@ public class SimpleJdbcTemplate implements JdbcOperations { // #region - transaction - /** - * 执行事务。如果未发生异常,则提交事务;当有异常发生时,回滚事务 - * - *

- * operations 中使用 JdbcExecutor 实参进行 JDBC 操作,这些操作在一个连接中 - *

- * - * @param 异常类型 - * @param operations 事务操作 - * @throws SQLException SQL 异常 - * @throws TransactionException 事务异常。事务中的异常会包装在该异常中。 - */ - public void executeTransaction( - @Nonnull final ThrowingConsumer operations) - throws TransactionException, SQLException { - AssertTools.checkNotNull(operations, "Operations can not be null."); - try (Connection conn = this.dataSource.getConnection()) { - final boolean autoCommit = conn.getAutoCommit(); - try { - conn.setAutoCommit(false); - operations.accept(new TransactionJdbcExecutor(conn)); - conn.commit(); - } - catch (Exception e) { - try { - conn.rollback(); - } - catch (SQLException ex) { - e.addSuppressed(ex); - } - throw new TransactionException(e); - } - finally { - conn.setAutoCommit(autoCommit); - } - } - } - - /** - * 执行事务。 - * 如果 {@code operations} 返回 {@code true},则提交事务; - * 如果抛出异常,或返回 {@code false},则回滚事务 - * - * @param 事务中的异常 - * @param operations 事务操作 - * @throws SQLException 数据库异常 - * @throws TransactionException 事务异常。事务中的异常会包装在该异常中。 - */ - public void commitIfTrue( - @Nonnull final ThrowingPredicate operations) - throws SQLException, TransactionException { - AssertTools.checkNotNull(operations, "Operations can not be null."); - try (Connection conn = this.dataSource.getConnection()) { - final boolean autoCommit = conn.getAutoCommit(); - try { - conn.setAutoCommit(false); - if (operations.test(new TransactionJdbcExecutor(conn))) { - conn.commit(); - } - else { - conn.rollback(); - } - } - catch (Exception e) { - try { - conn.rollback(); - } - catch (SQLException ex) { - e.addSuppressed(ex); - } - throw new TransactionException(e); - } - finally { - conn.setAutoCommit(autoCommit); - } - } + public TransactionTemplate transaction() { + return this.transactionTemplate; } // #endregion - private static final class TransactionJdbcExecutor implements JdbcOperations { - - private final Connection conn; - - private TransactionJdbcExecutor(Connection conn) { - this.conn = conn; - } - - // #region - query - - /** {@inheritDoc} */ - @Override - public T query(String sql, Object[] params, ResultHandler resultHandler) - throws SQLException { - return JdbcOperationSupport.query(this.conn, sql, params, resultHandler); - } - - // #endregion - - // #region - queryList - - /** {@inheritDoc} */ - @Override - public List queryList(String sql, Object[] params, RowMapper rowMapper) - throws SQLException { - return JdbcOperationSupport.queryList(this.conn, sql, params, rowMapper); - } - - /** {@inheritDoc} */ - @Override - public List queryList(String sql, Object[] params, Class clazz) - throws SQLException { - return JdbcOperationSupport.queryList(this.conn, sql, params, clazz); - } - - /** {@inheritDoc} */ - @Override - public List> queryList(String sql, Object[] params) - throws SQLException { - return JdbcOperationSupport.queryList(this.conn, sql, params, RowMapper.HASH_MAP_MAPPER); - } - - // #endregion - - // #region - queryFirst - - /** {@inheritDoc} */ - @Override - public Optional queryFirst(String sql, Object[] params, RowMapper rowMapper) - throws SQLException { - final T result = JdbcOperationSupport.queryFirst(this.conn, sql, params, rowMapper); - return Optional.ofNullable(result); - } - - /** {@inheritDoc} */ - @Override - public Optional queryFirst(String sql, Object[] params, Class clazz) - throws SQLException { - final T result = JdbcOperationSupport.queryFirst(this.conn, sql, params, clazz); - return Optional.ofNullable(result); - } - - /** {@inheritDoc} */ - @Override - public Optional> queryFirst(String sql, Object[] params) - throws SQLException { - final Map result = JdbcOperationSupport - .queryFirst(this.conn, sql, params, RowMapper.HASH_MAP_MAPPER); - return Optional.ofNullable(result); - } - - /** {@inheritDoc} */ - @Override - public boolean queryBoolean(String sql, Object[] params) - throws SQLException { - final Boolean result = JdbcOperationSupport - .queryFirst(this.conn, sql, params, Boolean.class); - return Boolean.TRUE.equals(result); - } - - // #endregion - - // #region - update & batchUpdate - - /** {@inheritDoc} */ - @Override - public int update(String sql, Object[] params) - throws SQLException { - return JdbcOperationSupport.update(this.conn, sql, params); - } - - /** {@inheritDoc} */ - @Override - public List updateAndReturnKeys(String sql, Object[] params, RowMapper rowMapper) - throws SQLException { - return JdbcOperationSupport.updateAndReturnKeys(this.conn, sql, params, rowMapper); - } - - /** {@inheritDoc} */ - @Override - public BatchUpdateResult batchUpdate(String sql, @Nullable Collection params, int batchSize) - throws SQLException { - return JdbcOperationSupport.batchUpdate(this.conn, sql, params, batchSize, false); - } - - /** {@inheritDoc} */ - @Override - public BatchUpdateResult batchUpdate(String sql, - @Nullable Collection params, - int batchSize, - boolean quietly) throws SQLException { - return JdbcOperationSupport - .batchUpdate(this.conn, sql, params, batchSize, quietly); - } - - // #endregion - - } - } diff --git a/src/main/java/xyz/zhouxy/jdbc/TransactionTemplate.java b/src/main/java/xyz/zhouxy/jdbc/TransactionTemplate.java new file mode 100644 index 0000000..b64a5c3 --- /dev/null +++ b/src/main/java/xyz/zhouxy/jdbc/TransactionTemplate.java @@ -0,0 +1,271 @@ +/* + * Copyright 2026-present ZhouXY + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package xyz.zhouxy.jdbc; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.sql.DataSource; + +import xyz.zhouxy.plusone.commons.function.ThrowingConsumer; +import xyz.zhouxy.plusone.commons.function.ThrowingPredicate; +import xyz.zhouxy.plusone.commons.util.AssertTools; + +/** + * 事务模板,提供事务执行能力。 + * + *

+ * 负责管理事务的生命周期:开启、提交、回滚、恢复自动提交。 + * 事务内的 JDBC 操作通过 {@link JdbcOperations} 接口进行, + * 所有操作共享同一个数据库连接。 + *

+ * + *

使用示例:

+ *
{@code
+ * TransactionTemplate tx = new TransactionTemplate(dataSource);
+ *
+ * // 消费者模式:无异常自动提交
+ * tx.execute(ops -> {
+ *     ops.update("INSERT INTO ...", buildParams(...));
+ *     ops.update("UPDATE ...", buildParams(...));
+ * });
+ *
+ * // 谓词模式:返回 true 提交,false 回滚
+ * tx.commitIfTrue(ops -> {
+ *     ops.update("UPDATE ...", buildParams(...));
+ *     return ops.queryBoolean("SELECT ...", buildParams(...));
+ * });
+ * }
+ * + * @author ZhouXY + * @since 1.1.0 + */ +public class TransactionTemplate { + + @Nonnull + private final DataSource dataSource; + + public TransactionTemplate(@Nonnull DataSource dataSource) { + AssertTools.checkNotNull(dataSource); + this.dataSource = dataSource; + } + + /** + * 执行事务。如果未发生异常,则提交事务;当有异常发生时,回滚事务 + * + *

+ * operations 中使用 JdbcExecutor 实参进行 JDBC 操作,这些操作在一个连接中 + *

+ * + * @param 异常类型 + * @param operations 事务操作 + * @throws SQLException SQL 异常 + * @throws TransactionException 事务异常。事务中的异常会包装在该异常中。 + */ + public void execute( + @Nonnull final ThrowingConsumer operations) + throws TransactionException, SQLException { + AssertTools.checkNotNull(operations, "Operations can not be null."); + try (Connection conn = this.dataSource.getConnection()) { + final boolean autoCommit = conn.getAutoCommit(); + try { + conn.setAutoCommit(false); + operations.accept(new TransactionJdbcExecutor(conn)); + conn.commit(); + } + catch (Exception e) { + rollbackSilently(conn, e); + throw new TransactionException(e); + } + finally { + conn.setAutoCommit(autoCommit); + } + } + } + + /** + * 执行事务。 + * 如果 {@code operations} 返回 {@code true},则提交事务; + * 如果抛出异常,或返回 {@code false},则回滚事务 + * + * @param 事务中的异常 + * @param operations 事务操作 + * @throws SQLException 数据库异常 + * @throws TransactionException 事务异常。事务中的异常会包装在该异常中。 + */ + public void commitIfTrue( + @Nonnull final ThrowingPredicate operations) + throws SQLException, TransactionException { + AssertTools.checkNotNull(operations, "Operations can not be null."); + try (Connection conn = this.dataSource.getConnection()) { + final boolean autoCommit = conn.getAutoCommit(); + try { + conn.setAutoCommit(false); + if (operations.test(new TransactionJdbcExecutor(conn))) { + conn.commit(); + } + else { + conn.rollback(); + } + } + catch (Exception e) { + rollbackSilently(conn, e); + throw new TransactionException(e); + } + finally { + conn.setAutoCommit(autoCommit); + } + } + } + + private void rollbackSilently(Connection conn, Exception e) { + try { + conn.rollback(); + } + catch (SQLException ex) { + e.addSuppressed(ex); + } + } + + // #region - TransactionJdbcExecutor + + private static final class TransactionJdbcExecutor implements JdbcOperations { + + private final Connection conn; + + private TransactionJdbcExecutor(Connection conn) { + this.conn = conn; + } + + // #region - query + + /** {@inheritDoc} */ + @Override + public T query(String sql, Object[] params, ResultHandler resultHandler) + throws SQLException { + return JdbcOperationSupport.query(this.conn, sql, params, resultHandler); + } + + // #endregion + + // #region - queryList + + /** {@inheritDoc} */ + @Override + public List queryList(String sql, Object[] params, RowMapper rowMapper) + throws SQLException { + return JdbcOperationSupport.queryList(this.conn, sql, params, rowMapper); + } + + /** {@inheritDoc} */ + @Override + public List queryList(String sql, Object[] params, Class clazz) + throws SQLException { + return JdbcOperationSupport.queryList(this.conn, sql, params, clazz); + } + + /** {@inheritDoc} */ + @Override + public List> queryList(String sql, Object[] params) + throws SQLException { + return JdbcOperationSupport.queryList(this.conn, sql, params, RowMapper.HASH_MAP_MAPPER); + } + + // #endregion + + // #region - queryFirst + + /** {@inheritDoc} */ + @Override + public Optional queryFirst(String sql, Object[] params, RowMapper rowMapper) + throws SQLException { + final T result = JdbcOperationSupport.queryFirst(this.conn, sql, params, rowMapper); + return Optional.ofNullable(result); + } + + /** {@inheritDoc} */ + @Override + public Optional queryFirst(String sql, Object[] params, Class clazz) + throws SQLException { + final T result = JdbcOperationSupport.queryFirst(this.conn, sql, params, clazz); + return Optional.ofNullable(result); + } + + /** {@inheritDoc} */ + @Override + public Optional> queryFirst(String sql, Object[] params) + throws SQLException { + final Map result = JdbcOperationSupport + .queryFirst(this.conn, sql, params, RowMapper.HASH_MAP_MAPPER); + return Optional.ofNullable(result); + } + + /** {@inheritDoc} */ + @Override + public boolean queryBoolean(String sql, Object[] params) + throws SQLException { + final Boolean result = JdbcOperationSupport + .queryFirst(this.conn, sql, params, Boolean.class); + return Boolean.TRUE.equals(result); + } + + // #endregion + + // #region - update & batchUpdate + + /** {@inheritDoc} */ + @Override + public int update(String sql, Object[] params) + throws SQLException { + return JdbcOperationSupport.update(this.conn, sql, params); + } + + /** {@inheritDoc} */ + @Override + public List updateAndReturnKeys(String sql, Object[] params, RowMapper rowMapper) + throws SQLException { + return JdbcOperationSupport.updateAndReturnKeys(this.conn, sql, params, rowMapper); + } + + /** {@inheritDoc} */ + @Override + public BatchUpdateResult batchUpdate(String sql, @Nullable Collection params, int batchSize) + throws SQLException { + return JdbcOperationSupport.batchUpdate(this.conn, sql, params, batchSize, false); + } + + /** {@inheritDoc} */ + @Override + public BatchUpdateResult batchUpdate(String sql, + @Nullable Collection params, + int batchSize, + boolean quietly) throws SQLException { + return JdbcOperationSupport + .batchUpdate(this.conn, sql, params, batchSize, quietly); + } + + // #endregion + + } + + // #endregion +} diff --git a/src/test/java/xyz/zhouxy/jdbc/test/TransactionTest.java b/src/test/java/xyz/zhouxy/jdbc/test/TransactionTest.java index d8cccc1..b30c3df 100644 --- a/src/test/java/xyz/zhouxy/jdbc/test/TransactionTest.java +++ b/src/test/java/xyz/zhouxy/jdbc/test/TransactionTest.java @@ -16,9 +16,10 @@ import xyz.zhouxy.jdbc.SimpleJdbcTemplate; import xyz.zhouxy.jdbc.TransactionException; /** - * 事务 API 测试:executeTransaction、commitIfTrue。 + * 事务 API 测试:通过 {@link xyz.zhouxy.jdbc.TransactionTemplate#execute} 和 + * {@link xyz.zhouxy.jdbc.TransactionTemplate#commitIfTrue} 测试事务提交与回滚。 */ -@DisplayName("SimpleJdbcTemplate 事务操作") +@DisplayName("TransactionTemplate 事务操作") class TransactionTest extends BaseH2Test { private static final Logger logger = LoggerFactory.getLogger(TransactionTest.class); @@ -28,14 +29,14 @@ class TransactionTest extends BaseH2Test { resetDatabase(); } - // ==================== executeTransaction 正常提交 ==================== + // ==================== execute 正常提交 ==================== @Test - @DisplayName("executeTransaction:正常提交,数据持久化") + @DisplayName("execute:正常提交,数据持久化") void testExecuteTransactionCommit() throws Exception { SimpleJdbcTemplate template = createTemplate(); - template.executeTransaction((JdbcOperations ops) -> { + template.transaction().execute((JdbcOperations ops) -> { ops.update("INSERT INTO users (username, email, age, balance, active) VALUES (?, ?, ?, ?, ?)", buildParams("txUser1", "tx1@test.com", 25, 1000L, true)); ops.update("UPDATE users SET balance = ? WHERE username = ?", @@ -56,10 +57,10 @@ class TransactionTest extends BaseH2Test { logger.info("事务提交验证通过"); } - // ==================== executeTransaction 异常回滚 ==================== + // ==================== execute 异常回滚 ==================== @Test - @DisplayName("executeTransaction:异常回滚,数据恢复原状") + @DisplayName("execute:异常回滚,数据恢复原状") void testExecuteTransactionRollback() throws Exception { SimpleJdbcTemplate template = createTemplate(); @@ -69,7 +70,7 @@ class TransactionTest extends BaseH2Test { buildParams("alice"), Long.class); TransactionException ex = assertThrows(TransactionException.class, () -> - template.executeTransaction((JdbcOperations ops) -> { + template.transaction().execute((JdbcOperations ops) -> { ops.update("UPDATE users SET balance = ? WHERE username = ?", buildParams(0L, "alice")); ops.update("INSERT INTO users (username, email) VALUES (?, ?)", @@ -99,12 +100,12 @@ class TransactionTest extends BaseH2Test { } @Test - @DisplayName("executeTransaction:SQL 异常触发回滚") + @DisplayName("execute:SQL 异常触发回滚") void testExecuteTransactionSqlExceptionRollback() { SimpleJdbcTemplate template = createTemplate(); assertThrows(TransactionException.class, () -> - template.executeTransaction((JdbcOperations ops) -> { + template.transaction().execute((JdbcOperations ops) -> { ops.update("INSERT INTO users (username) VALUES (?)", buildParams("validUser")); // 错误的 SQL @@ -120,14 +121,14 @@ class TransactionTest extends BaseH2Test { }); } - // ==================== commitIfTrue 返回 true 提交 ==================== + // ==================== commitIfTrue:返回 true 提交 ==================== @Test @DisplayName("commitIfTrue:返回 true 提交事务") void testCommitIfTrueCommit() throws Exception { SimpleJdbcTemplate template = createTemplate(); - template.commitIfTrue((JdbcOperations ops) -> { + template.transaction().commitIfTrue((JdbcOperations ops) -> { ops.update("INSERT INTO users (username, email) VALUES (?, ?)", buildParams("cftUser", "cft@test.com")); return true; @@ -147,7 +148,7 @@ class TransactionTest extends BaseH2Test { void testCommitIfFalseRollback() throws Exception { SimpleJdbcTemplate template = createTemplate(); - template.commitIfTrue((JdbcOperations ops) -> { + template.transaction().commitIfTrue((JdbcOperations ops) -> { ops.update("INSERT INTO users (username, email) VALUES (?, ?)", buildParams("cffUser", "cff@test.com")); return false; @@ -168,7 +169,7 @@ class TransactionTest extends BaseH2Test { SimpleJdbcTemplate template = createTemplate(); assertThrows(TransactionException.class, () -> - template.commitIfTrue((JdbcOperations ops) -> { + template.transaction().commitIfTrue((JdbcOperations ops) -> { ops.update("INSERT INTO users (username) VALUES (?)", buildParams("exUser")); throw new IllegalStateException("条件不满足"); @@ -186,11 +187,11 @@ class TransactionTest extends BaseH2Test { // ==================== 事务内查询可见性 ==================== @Test - @DisplayName("executeTransaction:事务内可查询到未提交的数据") + @DisplayName("execute:事务内可查询到未提交的数据") void testTransactionVisibility() throws Exception { SimpleJdbcTemplate template = createTemplate(); - template.executeTransaction((JdbcOperations ops) -> { + template.transaction().execute((JdbcOperations ops) -> { ops.update("INSERT INTO users (username, email) VALUES (?, ?)", buildParams("visible", "visible@test.com")); @@ -207,13 +208,13 @@ class TransactionTest extends BaseH2Test { // ==================== 边界情况 ==================== @Test - @DisplayName("executeTransaction:空操作(无异常)正常提交") + @DisplayName("execute:空操作(无异常)正常提交") void testExecuteTransactionEmpty() throws Exception { SimpleJdbcTemplate template = createTemplate(); // 空操作不应抛异常 assertDoesNotThrow(() -> - template.executeTransaction(ops -> { /* no-op */ })); + template.transaction().execute(ops -> { /* no-op */ })); // 数据应保持不变 int count = template.query("SELECT COUNT(*) FROM users", @@ -222,12 +223,12 @@ class TransactionTest extends BaseH2Test { } @Test - @DisplayName("executeTransaction:null 操作抛异常") + @DisplayName("execute:null 操作抛异常") @SuppressWarnings("null") void testExecuteTransactionNullOps() { SimpleJdbcTemplate template = createTemplate(); assertThrows(Exception.class, () -> - template.executeTransaction(null)); + template.transaction().execute(null)); } }