diff --git a/src/test/java/xyz/zhouxy/jdbc/test/AccountPO.java b/src/test/java/xyz/zhouxy/jdbc/test/AccountPO.java deleted file mode 100644 index 97ed2f3..0000000 --- a/src/test/java/xyz/zhouxy/jdbc/test/AccountPO.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2025 the original author or authors. - * - * 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.test; - -import java.time.LocalDateTime; -import java.util.Objects; - -public class AccountPO { - Long id; - String username; - String accountStatus; - LocalDateTime createTime; - Long createdBy; - LocalDateTime updateTime; - Long updatedBy; - Long version; - - public AccountPO() { - } - - public AccountPO(Long id, String username, String accountStatus, - Long createdBy, Long updatedBy) { - this.id = id; - this.username = username; - this.accountStatus = accountStatus; - this.createdBy = createdBy; - this.updatedBy = updatedBy; - } - - public AccountPO(Long id, String username, String accountStatus, - LocalDateTime createTime, Long createdBy, - LocalDateTime updateTime, Long updatedBy, - Long version) { - this.id = id; - this.username = username; - this.accountStatus = accountStatus; - this.createTime = createTime; - this.createdBy = createdBy; - this.updateTime = updateTime; - this.updatedBy = updatedBy; - this.version = version; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getAccountStatus() { - return accountStatus; - } - - public void setAccountStatus(String accountStatus) { - this.accountStatus = accountStatus; - } - - public LocalDateTime getCreateTime() { - return createTime; - } - - public void setCreateTime(LocalDateTime createTime) { - this.createTime = createTime; - } - - public Long getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(Long createdBy) { - this.createdBy = createdBy; - } - - public LocalDateTime getUpdateTime() { - return updateTime; - } - - public void setUpdateTime(LocalDateTime updateTime) { - this.updateTime = updateTime; - } - - public Long getUpdatedBy() { - return updatedBy; - } - - public void setUpdatedBy(Long updatedBy) { - this.updatedBy = updatedBy; - } - - public Long getVersion() { - return version; - } - - public void setVersion(Long version) { - this.version = version; - } - - @Override - public int hashCode() { - return Objects.hash(id, username, accountStatus, createTime, createdBy, updateTime, updatedBy, version); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - AccountPO other = (AccountPO) obj; - return Objects.equals(id, other.id) && Objects.equals(username, other.username) - && Objects.equals(accountStatus, other.accountStatus) && Objects.equals(createTime, other.createTime) - && Objects.equals(createdBy, other.createdBy) && Objects.equals(updateTime, other.updateTime) - && Objects.equals(updatedBy, other.updatedBy) && Objects.equals(version, other.version); - } - - @Override - public String toString() { - return "AccountPO [id=" + id + ", username=" + username + ", accountStatus=" + accountStatus + ", createTime=" - + createTime + ", createdBy=" + createdBy + ", updateTime=" + updateTime + ", updatedBy=" + updatedBy - + ", version=" + version + "]"; - } -} diff --git a/src/test/java/xyz/zhouxy/jdbc/test/BaseH2Test.java b/src/test/java/xyz/zhouxy/jdbc/test/BaseH2Test.java new file mode 100644 index 0000000..c8eee59 --- /dev/null +++ b/src/test/java/xyz/zhouxy/jdbc/test/BaseH2Test.java @@ -0,0 +1,91 @@ +package xyz.zhouxy.jdbc.test; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.Statement; +import java.util.stream.Collectors; + +import org.h2.jdbcx.JdbcConnectionPool; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import xyz.zhouxy.jdbc.SimpleJdbcTemplate; + +/** + * 测试基类,提供 H2 内存数据库连接池和模板实例。 + * + *

使用 H2 内置 JdbcConnectionPool 作为连接池,符合项目"基本仅考虑数据库连接池"的设计。

+ */ +public abstract class BaseH2Test { + + protected static final Logger logger = LoggerFactory.getLogger(BaseH2Test.class); + + protected static JdbcConnectionPool dataSource; + + protected SimpleJdbcTemplate createTemplate() { + return new SimpleJdbcTemplate(dataSource); + } + + @BeforeAll + static void initDatabase() throws Exception { + dataSource = JdbcConnectionPool.create( + "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL;DATABASE_TO_LOWER=TRUE;DATABASE_TO_UPPER=FALSE", + "sa", ""); + dataSource.setMaxConnections(10); + + // 加载并执行 SQL 初始化文件 + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + + String sqlContent = loadSqlFile("init_tables.sql"); + // 按分号拆分并逐条执行 + for (String singleSql : sqlContent.split(";")) { + String trimmed = singleSql.trim(); + if (!trimmed.isEmpty()) { + stmt.execute(trimmed); + } + } + } + + logger.info("H2 数据库初始化完成,连接池已就绪"); + } + + @AfterAll + static void closeDatabase() { + if (dataSource != null) { + dataSource.dispose(); + logger.info("H2 数据库连接池已关闭"); + } + } + + /** + * 重新执行初始化脚本,恢复数据到初始状态。 + */ + protected static void resetDatabase() throws Exception { + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + + String sqlContent = loadSqlFile("init_tables.sql"); + for (String singleSql : sqlContent.split(";")) { + String trimmed = singleSql.trim(); + if (!trimmed.isEmpty()) { + stmt.execute(trimmed); + } + } + } + + logger.info("数据库已重置为初始状态"); + } + + private static String loadSqlFile(String fileName) throws Exception { + try (InputStream is = BaseH2Test.class.getClassLoader().getResourceAsStream(fileName); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + return reader.lines().collect(Collectors.joining("\n")); + } + } +} diff --git a/src/test/java/xyz/zhouxy/jdbc/test/BatchUpdateTest.java b/src/test/java/xyz/zhouxy/jdbc/test/BatchUpdateTest.java new file mode 100644 index 0000000..66de33e --- /dev/null +++ b/src/test/java/xyz/zhouxy/jdbc/test/BatchUpdateTest.java @@ -0,0 +1,292 @@ +package xyz.zhouxy.jdbc.test; + +import static org.junit.jupiter.api.Assertions.*; +import static xyz.zhouxy.jdbc.ParamBuilder.*; + +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.Lists; + +import xyz.zhouxy.jdbc.BatchUpdateErrorInfo; +import xyz.zhouxy.jdbc.BatchUpdateResult; +import xyz.zhouxy.jdbc.BatchUpdateStatus; +import xyz.zhouxy.jdbc.SimpleJdbcTemplate; + +/** + * 批量更新 API 测试:batchUpdate。 + */ +@DisplayName("SimpleJdbcTemplate 批量更新操作") +class BatchUpdateTest extends BaseH2Test { + + private static final Logger logger = LoggerFactory.getLogger(BatchUpdateTest.class); + + private static final String INSERT_SQL = + "INSERT INTO users (username, email, age, balance, active) VALUES (?, ?, ?, ?, ?)"; + + @BeforeEach + void setUp() throws Exception { + resetDatabase(); + } + + // ================================ + // #region - 正常批量操作 + // ================================ + + @Test + @DisplayName("batchUpdate:正常批量插入,单批次") + void testBatchUpdateSingleBatch() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + List users = new ArrayList<>(); + users.add(new User("user01", "u01@test.com", 20, 1000L, true)); + users.add(new User("user02", "u02@test.com", 21, 2000L, false)); + users.add(new User("user03", "u03@test.com", 22, 3000L, true)); + + List params = buildBatchParams(users, a -> new Object[] { a.getUsername(), a.getEmail(), a.getAge(), a.getBalance(), a.getActive() }); + BatchUpdateResult result = template.batchUpdate(INSERT_SQL, params, 10); + + logger.info("batchUpdate 结果: {}", result); + assertEquals(BatchUpdateStatus.SUCCESS, result.getStatus()); + assertEquals(3, result.getTotal()); + assertEquals(1, result.getBatchCount()); + assertEquals(1, result.getCompleteBatchCount()); + assertEquals(1, result.getSuccessBatchCount()); + assertEquals(0, result.getErrorBatchCount()); + assertEquals(10, result.getBatchSize()); + assertEquals(0, result.getRemainingBatchCount()); + + // 验证数据已插入 + int count = template.query("SELECT COUNT(*) FROM users", + rs -> { rs.next(); return rs.getInt(1); }); + assertEquals(8, count); // 5 初始 + 3 + } + + @Test + @DisplayName("batchUpdate:多批次,batchSize 恰好整除") + void testBatchUpdateMultipleBatchesExact() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + List paramsList = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + paramsList.add(new User("user" + i, "u" + i + "@test.com", 20 + i, 1000L, true)); + } + + List params = buildBatchParams(paramsList, a -> new Object[] { a.getUsername(), a.getEmail(), a.getAge(), a.getBalance(), a.getActive() }); + BatchUpdateResult result = template.batchUpdate(INSERT_SQL, params, 5); + + logger.info("多批次 batchUpdate 结果: {}", result); + assertEquals(BatchUpdateStatus.SUCCESS, result.getStatus()); + assertEquals(10, result.getTotal()); + assertEquals(2, result.getBatchCount()); + assertEquals(2, result.getSuccessBatchCount()); + assertEquals(0, result.getErrorBatchCount()); + } + + @Test + @DisplayName("batchUpdate:batchSize 不整除") + void testBatchUpdateBatchSizeNotDivisible() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + List paramsList = new ArrayList<>(); + for (int i = 0; i < 7; i++) { + paramsList.add(new Object[]{"user" + i, "u" + i + "@test.com", 20, 1000L, true}); + } + + BatchUpdateResult result = template.batchUpdate(INSERT_SQL, paramsList, 3); + + logger.info("不整除 batchUpdate 结果: {}", result); + assertEquals(7, result.getTotal()); + assertEquals(3, result.getBatchCount()); + assertEquals(BatchUpdateStatus.SUCCESS, result.getStatus()); + } + + // ================================ + // #endregion - 正常批量操作 + // ================================ + + // ================================ + // #region - 边界情况 + // ================================ + + @Test + @DisplayName("batchUpdate:空 params 集合") + void testBatchUpdateEmptyParams() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + List params = buildBatchParams(Collections.emptyList(), + a -> new Object[] { a.getUsername(), a.getEmail(), a.getAge(), a.getBalance(), a.getActive() }); + assertEquals(Collections.emptyList(), params); + BatchUpdateResult result = template.batchUpdate( + INSERT_SQL, params, 10); + + assertEquals(0, result.getTotal()); + assertEquals(0, result.getBatchCount()); + assertEquals(10, result.getBatchSize()); + } + + @Test + @DisplayName("batchUpdate:null params") + void testBatchUpdateNullParams() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + BatchUpdateResult result = template.batchUpdate( + INSERT_SQL, null, 10); + + assertEquals(0, result.getTotal()); + assertEquals(0, result.getBatchCount()); + } + + @Test + @DisplayName("batchUpdate:单条数据") + void testBatchUpdateSingleItem() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + List paramsList = Collections.singletonList( + new Object[]{"single", "single@test.com", 30, 5000L, true}); + + BatchUpdateResult result = template.batchUpdate(INSERT_SQL, paramsList, 5); + + assertEquals(BatchUpdateStatus.SUCCESS, result.getStatus()); + assertEquals(1, result.getTotal()); + assertEquals(1, result.getBatchCount()); + } + + // ================================ + // #endregion - 边界情况 + // ================================ + + // ================================ + // #region - 包含错误数据 + // ================================ + + final List userListContainingInvalidData = Lists.newArrayList( + // batch 0 + new User("test_0001", "test_0001@example.com", 1, 1L, true), + new User("test_0002", "test_0002@example.com", 1, 1L, true), + new User("test_0003", "test_0003@example.com", 1, 1L, true), + // batch 1 + new User("test_0004", "test_0004@example.com", 1, 1L, true), + new User("test_0005", "test_0005@example.com", 1, 1L, true), + new User("test_0006", "test_0006@example.com", 1, 1L, true), + // batch 2 + new User("test_0007", "test_0007@example.com", 1, 1L, true), + new User("test_0007", "test_*0007@example.com", 1, 1L, true), + // new User("test_0008", "test_0008@example.com", 1, 1L, true), + new User("test_0009", "test_0009@example.com", 1, 1L, true), + // batch 3 + new User("test_0009", "test_*0009@example.com", 1, 1L, true), + // new User("test_0010", "test_0010@example.com", 1, 1L, true), + new User("test_0011", "test_0011@example.com", 1, 1L, true), + new User("test_0012", "test_0012@example.com", 1, 1L, true), + // batch 4 + new User("test_0013", "test_0013@example.com", 1, 1L, true) + ); + + // ==================== quietly=false 中断模式 ==================== + + @Test + @DisplayName("batchUpdate:quietly=false,中间出错中断返回 INTERRUPTED") + void testBatchUpdateQuietlyFalseInterrupted() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + int count0 = template.queryFirst("SELECT COUNT(*) FROM users", Integer.class) + .orElse(0); + + List params = buildBatchParams(userListContainingInvalidData, a -> new Object[] { a.getUsername(), a.getEmail(), a.getAge(), a.getBalance(), a.getActive() }); + BatchUpdateResult result = template.batchUpdate(INSERT_SQL, params, 3); + + assertEquals(BatchUpdateStatus.INTERRUPTED, result.getStatus()); + assertEquals(13, result.getTotal()); + assertEquals(5, result.getBatchCount()); + assertEquals(3, result.getCompleteBatchCount()); + assertEquals(2, result.getSuccessBatchCount()); + assertEquals(1, result.getErrorBatchCount()); + assertEquals(2, result.getRemainingBatchCount()); + + assertArrayEquals(new int[] { 2 }, result.getErrorBatchIndexes()); + + assertEquals(1, result.getAllErrorsInfo().size()); + assertTrue(result.getAllErrorsInfo().containsKey(2)); + + BatchUpdateErrorInfo batchUpdateErrorInfo = result.getBatchUpdateErrorInfo(2); + assertNotNull(batchUpdateErrorInfo); + assertEquals(2, batchUpdateErrorInfo.getBatchIndex()); + assertNotNull(batchUpdateErrorInfo.getCause()); + assertInstanceOf(SQLException.class, batchUpdateErrorInfo.getCause()); + assertTrue(SQLException.class.isAssignableFrom(batchUpdateErrorInfo.getErrorType())); + + assertArrayEquals(new int[] { 1, 1, 1 }, result.getUpdateCounts(0)); + assertArrayEquals(new int[] { 1, 1, 1 }, result.getUpdateCounts(1)); + assertArrayEquals(new int[] { 1, Statement.EXECUTE_FAILED, 1 }, result.getUpdateCounts(2)); + assertNull(result.getUpdateCounts(3)); + assertNull(result.getUpdateCounts(4)); + + Optional count8 = template.queryFirst("SELECT COUNT(*) FROM users", Integer.class); + assertEquals(count0 + 8, count8.get().intValue()); + } + + // ==================== quietly=true 静默模式 ==================== + + @Test + @DisplayName("batchUpdate:quietly=true,出错继续返回 COMPLETED_WITH_ERRORS") + void testBatchUpdateQuietlyTrue() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + int count0 = template.queryFirst("SELECT COUNT(*) FROM users", Integer.class) + .orElse(0); + + List params = buildBatchParams(userListContainingInvalidData, a -> new Object[] { a.getUsername(), a.getEmail(), a.getAge(), a.getBalance(), a.getActive() }); + BatchUpdateResult result = template.batchUpdate(INSERT_SQL, params, 3, true); + + assertEquals(BatchUpdateStatus.COMPLETED_WITH_ERRORS, result.getStatus()); + assertEquals(13, result.getTotal()); + assertEquals(5, result.getBatchCount()); + assertEquals(5, result.getCompleteBatchCount()); + assertEquals(3, result.getSuccessBatchCount()); + assertEquals(2, result.getErrorBatchCount()); + assertEquals(0, result.getRemainingBatchCount()); + assertArrayEquals(new int[] { 1, 1, 1 }, result.getUpdateCounts(0)); + assertArrayEquals(new int[] { 1, 1, 1 }, result.getUpdateCounts(1)); + assertArrayEquals(new int[] { 1, Statement.EXECUTE_FAILED, 1 }, result.getUpdateCounts(2)); + assertArrayEquals(new int[] { Statement.EXECUTE_FAILED, 1, 1 }, result.getUpdateCounts(3)); + assertArrayEquals(new int[] { 1 }, result.getUpdateCounts(4)); + + Optional count11 = template.queryFirst("SELECT COUNT(*) FROM users", Integer.class); + assertEquals(count0 + 11, count11.get().intValue()); + } + + // ================================ + // #endregion - 包含错误数据 + // ================================ + + // ================================ + // #region - wrong batchSize + // ================================ + + @Test + @DisplayName("batchUpdate:batchSize < 0,参数校验不通过") + void testBatchUpdateWithWrongBatchSize() { + SimpleJdbcTemplate template = createTemplate(); + + List params = buildBatchParams(userListContainingInvalidData, a -> new Object[] { a.getUsername(), a.getEmail(), a.getAge(), a.getBalance(), a.getActive() }); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> template.batchUpdate(INSERT_SQL, params, -1, true)); + assertEquals("The batch size must be greater than 0.", e.getMessage()); + } + + // ================================ + // #endregion - wrong batchSize + // ================================ + +} diff --git a/src/test/java/xyz/zhouxy/jdbc/test/BatchUpdateTests.java b/src/test/java/xyz/zhouxy/jdbc/test/BatchUpdateTests.java deleted file mode 100644 index b90af81..0000000 --- a/src/test/java/xyz/zhouxy/jdbc/test/BatchUpdateTests.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2026-present the original author or authors. - * - * 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.test; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static xyz.zhouxy.jdbc.ParamBuilder.buildBatchParams; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.List; - -import java.util.Optional; - -import org.h2.jdbcx.JdbcDataSource; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import com.google.common.collect.Lists; -import com.google.common.io.Resources; - -import xyz.zhouxy.jdbc.BatchUpdateResult; -import xyz.zhouxy.jdbc.BatchUpdateStatus; -import xyz.zhouxy.jdbc.SimpleJdbcTemplate; - -public class BatchUpdateTests { - - private static SimpleJdbcTemplate jdbcTemplate; - - @BeforeAll - static void initH2() throws IOException, SQLException { - JdbcDataSource dataSource = new JdbcDataSource(); - dataSource.setURL("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=FALSE;MODE=MySQL"); - dataSource.setUser("sa"); - dataSource.setPassword(""); - jdbcTemplate = new SimpleJdbcTemplate(dataSource); - - // 建表 - executeSqlFile("schema.sql"); - } - - @BeforeEach - void initData() throws SQLException { - // 初始化数据 - jdbcTemplate.update("truncate table sys_account"); - } - - static void executeSqlFile(String filePath) throws IOException, SQLException { - String[] sqls = Resources - .toString(Resources.getResource(filePath), StandardCharsets.UTF_8) - .split(";"); - for (String sql : sqls) { - jdbcTemplate.update(sql); - } - } - - final List accountPOs = Lists.newArrayList( - // batch 0 - new AccountPO(10001L, "test_0001", "1", 1L, 1L), - new AccountPO(10002L, "test_0002", "1", 1L, 1L), - new AccountPO(10003L, "test_0003", "1", 1L, 1L), - // batch 1 - new AccountPO(10004L, "test_0004", "1", 1L, 1L), - new AccountPO(10005L, "test_0005", "1", 1L, 1L), - new AccountPO(10006L, "test_0006", "1", 1L, 1L), - // batch 2 - new AccountPO(10007L, "test_0007", "1", 1L, 1L), - new AccountPO(10007L, "test_*0007", "1", 1L, 1L), - // new AccountPO(10008L, "test_0008", "1", 1L, 1L), - new AccountPO(10009L, "test_0009", "1", 1L, 1L), - // batch 3 - new AccountPO(10009L, "test_*0009", "1", 1L, 1L), - // new AccountPO(10010L, "test_0010", "1", 1L, 1L), - new AccountPO(10011L, "test_0011", "1", 1L, 1L), - new AccountPO(10012L, "test_0012", "1", 1L, 1L), - // batch 4 - new AccountPO(10013L, "test_0013", "1", 1L, 1L) - ); - - @Test - void testBatchUpdate() throws SQLException { - - Optional count0 = jdbcTemplate.queryFirst("SELECT COUNT(*) FROM sys_account", (rs, i) -> rs.getInt(1)); - assertEquals(0, count0.get().intValue()); - - BatchUpdateResult result = jdbcTemplate.batchUpdate( - "INSERT INTO sys_account (id, username, account_status, created_by, updated_by) VALUES (?, ?, ?, ?, ?)", - buildBatchParams(accountPOs, - a -> new Object[] { a.getId(), a.getUsername(), a.getAccountStatus(), a.getCreatedBy(), a.getUpdatedBy() }), - 3); - assertEquals(BatchUpdateStatus.INTERRUPTED, result.getStatus()); - assertEquals(13, result.getTotal()); - assertEquals(5, result.getBatchCount()); - assertEquals(3, result.getCompleteBatchCount()); - assertEquals(2, result.getSuccessBatchCount()); - assertEquals(1, result.getErrorBatchCount()); - assertEquals(2, result.getRemainingBatchCount()); - assertArrayEquals(new int[] { 1, 1, 1 }, result.getUpdateCounts(0)); - assertArrayEquals(new int[] { 1, 1, 1 }, result.getUpdateCounts(1)); - assertArrayEquals(new int[] { 1, Statement.EXECUTE_FAILED, 1 }, result.getUpdateCounts(2)); - assertNull(result.getUpdateCounts(3)); - assertNull(result.getUpdateCounts(4)); - - Optional count8 = jdbcTemplate.queryFirst("SELECT COUNT(*) FROM sys_account", (rs, i) -> rs.getInt(1)); - assertEquals(8, count8.get().intValue()); - } - - @Test - void testBatchUpdateQuietly() throws SQLException { - Optional count0 = jdbcTemplate.queryFirst("SELECT COUNT(*) FROM sys_account", (rs, i) -> rs.getInt(1)); - assertEquals(0, count0.get().intValue()); - - BatchUpdateResult result = jdbcTemplate.batchUpdate( - "INSERT INTO sys_account (id, username, account_status, created_by, updated_by) VALUES (?, ?, ?, ?, ?)", - buildBatchParams(accountPOs, - a -> new Object[] { a.getId(), a.getUsername(), a.getAccountStatus(), a.getCreatedBy(), a.getUpdatedBy() }), - 3, - true); - assertEquals(BatchUpdateStatus.COMPLETED_WITH_ERRORS, result.getStatus()); - assertEquals(13, result.getTotal()); - assertEquals(5, result.getBatchCount()); - assertEquals(5, result.getCompleteBatchCount()); - assertEquals(3, result.getSuccessBatchCount()); - assertEquals(2, result.getErrorBatchCount()); - assertEquals(0, result.getRemainingBatchCount()); - assertArrayEquals(new int[] { 1, 1, 1 }, result.getUpdateCounts(0)); - assertArrayEquals(new int[] { 1, 1, 1 }, result.getUpdateCounts(1)); - assertArrayEquals(new int[] { 1, Statement.EXECUTE_FAILED, 1 }, result.getUpdateCounts(2)); - assertArrayEquals(new int[] { Statement.EXECUTE_FAILED, 1, 1 }, result.getUpdateCounts(3)); - assertArrayEquals(new int[] { 1 }, result.getUpdateCounts(4)); - - Optional count11 = jdbcTemplate.queryFirst("SELECT COUNT(*) FROM sys_account", (rs, i) -> rs.getInt(1)); - assertEquals(11, count11.get().intValue()); - } -} diff --git a/src/test/java/xyz/zhouxy/jdbc/test/QueryTest.java b/src/test/java/xyz/zhouxy/jdbc/test/QueryTest.java new file mode 100644 index 0000000..aaa0080 --- /dev/null +++ b/src/test/java/xyz/zhouxy/jdbc/test/QueryTest.java @@ -0,0 +1,441 @@ +package xyz.zhouxy.jdbc.test; + +import static org.junit.jupiter.api.Assertions.*; +import static xyz.zhouxy.jdbc.ParamBuilder.buildParams; + +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import xyz.zhouxy.jdbc.ParamBuilder; +import xyz.zhouxy.jdbc.ResultHandler; +import xyz.zhouxy.jdbc.SimpleJdbcTemplate; + +/** + * 查询 API 测试:query、queryList、queryFirst、queryBoolean。 + */ +@DisplayName("SimpleJdbcTemplate 查询操作") +class QueryTest extends BaseH2Test { + + private static final Logger logger = LoggerFactory.getLogger(QueryTest.class); + + // ==================== query(ResultHandler) ==================== + + @Test + @DisplayName("query + ResultHandler:统计总行数") + void testQueryWithResultHandlerCount() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + Integer count = template.query( + "SELECT COUNT(*) FROM users", + new Object[0], + (ResultHandler) rs -> { + rs.next(); + return rs.getInt(1); + }); + + logger.info("query 返回总行数: {}", count); + assertEquals(5, count); + } + + @Test + @DisplayName("query + ResultHandler:聚合求和") + void testQueryWithResultHandlerAggregation() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + Long totalBalance = template.query( + "SELECT SUM(balance) FROM users", + new Object[0], + (ResultHandler) rs -> { + rs.next(); + return rs.getLong(1); + }); + + logger.info("query 聚合 balance 总和: {}", totalBalance); + assertNotNull(totalBalance); + } + + @Test + @DisplayName("query:无参数查询(default 方法)") + void testQueryNoParams() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + Integer count = template.query( + "SELECT COUNT(*) FROM users", + rs -> { + rs.next(); + return rs.getInt(1); + }); + + assertEquals(5, count); + } + + // ==================== queryList(RowMapper) ==================== + + @Test + @DisplayName("queryList + RowMapper:查询全部用户") + void testQueryListWithRowMapper() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + UserRowMapper rowMapper = new UserRowMapper(); + + List users = template.queryList("SELECT * FROM users ORDER BY id", + new Object[0], rowMapper); + + logger.info("queryList(RowMapper) 返回 {} 条记录", users.size()); + assertEquals(5, users.size()); + assertEquals("alice", users.get(0).getUsername()); + } + + @Test + @DisplayName("queryList + RowMapper:带参数条件查询") + void testQueryListWithParams() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + UserRowMapper rowMapper = new UserRowMapper(); + + List users = template.queryList( + "SELECT * FROM users WHERE active = ? ORDER BY id", + new Object[]{true}, rowMapper); + + logger.info("queryList 活跃用户: {} 条", users.size()); + assertEquals(4, users.size()); + users.forEach(u -> assertTrue(u.getActive())); + } + + @Test + @DisplayName("queryList + RowMapper:无参数重载") + void testQueryListRowMapperNoParams() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + List users = template.queryList( + "SELECT * FROM users ORDER BY id", + new UserRowMapper()); + + assertEquals(5, users.size()); + } + + // ==================== queryList(Class) ==================== + + @Test + @DisplayName("queryList(Class):单列查询返回 String 列表") + void testQueryListWithClassString() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + List usernames = template.queryList( + "SELECT username FROM users ORDER BY id", + String.class); + + logger.info("queryList(Class) 返回用户名: {}", usernames); + assertEquals(5, usernames.size()); + assertTrue(usernames.contains("alice")); + } + + @Test + @DisplayName("queryList(Class):空结果集返回空列表") + void testQueryListEmptyResult() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + List result = template.queryList( + "SELECT username FROM users WHERE id = ?", + buildParams(999), String.class); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + // ==================== queryList(Map) ==================== + + @Test + @DisplayName("queryList(Map):返回 List") + void testQueryListAsMap() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + List> users = template.queryList( + "SELECT id, username, email FROM users WHERE id = ?", + new Object[]{1}); + + logger.info("queryList(Map) 返回: {}", users); + assertEquals(1, users.size()); + assertEquals("alice", users.get(0).get("username")); + } + + @Test + @DisplayName("queryList(Map):无参数重载") + void testQueryListMapNoParams() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + List> users = template.queryList( + "SELECT id, username FROM users ORDER BY id"); + + assertEquals(5, users.size()); + } + + // ==================== queryFirst(RowMapper) ==================== + + @Test + @DisplayName("queryFirst + RowMapper:查询第一条记录") + void testQueryFirstWithRowMapper() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + UserRowMapper rowMapper = new UserRowMapper(); + + Optional user = template.queryFirst( + "SELECT * FROM users ORDER BY id", + rowMapper); + + assertTrue(user.isPresent()); + assertEquals("alice", user.get().getUsername()); + } + + @Test + @DisplayName("queryFirst + RowMapper:空结果返回 Optional.empty()") + void testQueryFirstEmpty() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + Optional user = template.queryFirst( + "SELECT * FROM users WHERE id = ?", + buildParams(999), new UserRowMapper()); + + assertFalse(user.isPresent()); + } + + @Test + @DisplayName("queryFirst + RowMapper:null 参数") + void testQueryFirstWithNullParams() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + Optional user = template.queryFirst( + "SELECT * FROM users ORDER BY id", + null, new UserRowMapper()); + + assertTrue(user.isPresent()); + } + + // ==================== queryFirst(Class) ==================== + + @Test + @DisplayName("queryFirst(Class):查询第一行第一列") + void testQueryFirstWithClass() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + Optional username = template.queryFirst( + "SELECT username FROM users ORDER BY id", + String.class); + + assertTrue(username.isPresent()); + assertEquals("alice", username.get()); + } + + @Test + @DisplayName("queryFirst(Class):空结果返回 Optional.empty()") + void testQueryFirstClassEmpty() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + Optional result = template.queryFirst( + "SELECT username FROM users WHERE id = ?", + buildParams(999), String.class); + + assertFalse(result.isPresent()); + } + + + @Test + @DisplayName("queryFirst + Class:统计总行数") + void testQueryFirstWithClass_queryCount() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + int count = template.queryFirst( + "SELECT COUNT(*) FROM users", + new Object[0], + Integer.class) + .orElse(0); + + logger.info("query 返回总行数: {}", count); + assertEquals(5, count); + } + + @Test + @DisplayName("queryFirst + Class:聚合求和") + void testQueryFirstWithClass_queryAggregation() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + Long totalBalance = template.queryFirst( + "SELECT SUM(balance) FROM users", + new Object[0], + Long.class) + .orElse(0L); + + logger.info("query 聚合 balance 总和: {}", totalBalance); + assertNotNull(totalBalance); + } + + // ==================== queryFirst(Map) ==================== + + @Test + @DisplayName("queryFirst(Map):返回 Optional") + void testQueryFirstAsMap() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + Optional> user = template.queryFirst( + "SELECT id, username FROM users WHERE id = ?", + buildParams(2)); + + assertTrue(user.isPresent()); + assertEquals("bob", user.get().get("username")); + } + + @Test + @DisplayName("queryFirst(Map):返回 Optional 无参数重载") + void testQueryFirstAsMapNoParams() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + Optional> user = template.queryFirst( + "SELECT id, username FROM users ORDER BY id"); + + assertTrue(user.isPresent()); + assertEquals("alice", user.get().get("username")); + } + + @Test + @DisplayName("queryFirst(Map):空结果返回 Optional.empty()") + void testQueryFirstMapEmpty() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + Optional> result = template.queryFirst( + "SELECT * FROM users WHERE id = ?", + new Object[]{-1}); + + assertFalse(result.isPresent()); + } + + // ==================== queryBoolean ==================== + + @Test + @DisplayName("queryBoolean:存在返回 true") + void testQueryBooleanTrue() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + boolean exists = template.queryBoolean( + "SELECT TRUE FROM users WHERE username = ?", + new Object[]{"alice"}); + + assertTrue(exists); + } + + @Test + @DisplayName("queryBoolean:不存在返回 false") + void testQueryBooleanFalse() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + boolean exists = template.queryBoolean( + "SELECT TRUE FROM users WHERE username = ?", + new Object[]{"nobody"}); + + assertFalse(exists); + } + + @Test + @DisplayName("queryBoolean:无参数重载") + void testQueryBooleanNoParams() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + boolean exists = template.queryBoolean( + "SELECT COUNT(*) > 0 FROM users"); + + assertTrue(exists); + } + + @Test + @DisplayName("queryBoolean:结果集为空返回 false") + void testQueryBooleanEmptyResult() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + boolean exists = template.queryBoolean( + "SELECT active FROM users WHERE id = ?", + new Object[]{999}); + + assertFalse(exists); + } + + // ==================== 边界情况 ==================== + + @Test + @DisplayName("边界:查询包含 null 字段的数据") + void testQueryWithNullFields() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + Optional user = template.queryFirst( + "SELECT * FROM users WHERE username = ?", + new Object[]{"charlie"}, new UserRowMapper()); + + assertTrue(user.isPresent()); + assertNull(user.get().getEmail()); + assertNull(user.get().getAge()); + assertNull(user.get().getBirthDate()); + assertNull(user.get().getWorkStartTime()); + } + + @Test + @DisplayName("边界:查询不存在的表应抛出 SQLException") + void testQueryInvalidTable() { + SimpleJdbcTemplate template = createTemplate(); + + assertThrows(SQLException.class, () -> + template.queryList("SELECT * FROM non_existent_table", + new Object[0], String.class)); + } + + @Test + @DisplayName("边界:查询语法错误应抛出 SQLException") + void testQueryInvalidSql() { + SimpleJdbcTemplate template = createTemplate(); + + assertThrows(SQLException.class, () -> + template.queryList("SELEC * FROM users", + new Object[0], String.class)); + } + + @Test + @DisplayName("边界:HASH_MAP_MAPPER 同名列静默覆盖") + void testHashMapMapperColumnOverride() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + // 查询同名列(如自连接或别名相同的情况) + List> results = template.queryList( + "SELECT id, id AS duplicated_id FROM users WHERE id = 1", + new Object[0]); + + assertEquals(1, results.size()); + // HASH_MAP_MAPPER 注:同名后者覆盖前者,所以 key 为 duplicated_id 的值存在 + assertNotNull(results.get(0).get("duplicated_id")); + } + + @Test + @DisplayName("边界:单行查询结果") + void testQuerySingleRow() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + Optional user = template.queryFirst( + "SELECT * FROM users WHERE id = ?", + new Object[]{1}, new UserRowMapper()); + + assertTrue(user.isPresent()); + assertEquals(Long.valueOf(1), user.get().getId()); + } + + @Test + @DisplayName("边界:使用 ParamBuilder.EMPTY_OBJECT_ARRAY") + void testQueryWithEmptyObjectArray() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + List users = template.queryList( + "SELECT * FROM users ORDER BY id", + ParamBuilder.EMPTY_OBJECT_ARRAY, new UserRowMapper()); + + assertEquals(5, users.size()); + } +} diff --git a/src/test/java/xyz/zhouxy/jdbc/test/RowMapperTest.java b/src/test/java/xyz/zhouxy/jdbc/test/RowMapperTest.java new file mode 100644 index 0000000..8ff3619 --- /dev/null +++ b/src/test/java/xyz/zhouxy/jdbc/test/RowMapperTest.java @@ -0,0 +1,256 @@ +package xyz.zhouxy.jdbc.test; + +import static org.junit.jupiter.api.Assertions.*; + +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import xyz.zhouxy.jdbc.DefaultBeanRowMapper; +import xyz.zhouxy.jdbc.RowMapper; +import xyz.zhouxy.jdbc.SimpleJdbcTemplate; + +/** + * RowMapper 测试:DefaultBeanRowMapper、HASH_MAP_MAPPER、自定义 RowMapper。 + * + *

验证 DefaultBeanRowMapper 的默认映射和自定义列映射,以及 RowMapper 接口的静态工厂方法。

+ */ +@DisplayName("RowMapper 映射测试") +class RowMapperTest extends BaseH2Test { + + private static final Logger logger = LoggerFactory.getLogger(RowMapperTest.class); + + // ==================== DefaultBeanRowMapper 默认映射 ==================== + + @Test + @DisplayName("DefaultBeanRowMapper:默认小驼峰→小写下划线映射") + void testDefaultBeanRowMapperDefaultMapping() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + RowMapper rowMapper = RowMapper.beanRowMapper(User.class); + + Optional user = template.queryFirst( + "SELECT * FROM users WHERE username = ?", + new Object[]{"alice"}, rowMapper); + + assertTrue(user.isPresent()); + User u = user.get(); + assertEquals(Long.valueOf(1), u.getId()); + assertEquals("alice", u.getUsername()); + assertEquals("alice@example.com", u.getEmail()); + assertEquals(Integer.valueOf(28), u.getAge()); + assertEquals(Long.valueOf(15000), u.getBalance()); + assertTrue(u.getActive()); + + logger.info("DefaultBeanRowMapper 默认映射: {}", u); + } + + @Test + @DisplayName("DefaultBeanRowMapper:null 值字段映射为 null") + void testDefaultBeanRowMapperNullFields() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + RowMapper rowMapper = RowMapper.beanRowMapper(User.class); + + Optional user = template.queryFirst( + "SELECT * FROM users WHERE username = ?", + new Object[]{"charlie"}, rowMapper); + + assertTrue(user.isPresent()); + User u = user.get(); + assertNull(u.getEmail()); + assertNull(u.getAge()); + assertNull(u.getBirthDate()); + assertNull(u.getWorkStartTime()); + // 非 null 字段应有值 + assertEquals("charlie", u.getUsername()); + } + + // ==================== DefaultBeanRowMapper 自定义列映射 ==================== + + @Test + @DisplayName("DefaultBeanRowMapper:propertyColMap 自定义列名映射") + void testDefaultBeanRowMapperWithPropertyColMap() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + // 自定义映射:属性名 -> 列名 + Map propertyColMap = new HashMap<>(); + propertyColMap.put("username", "user_name_1"); // 使用别名 + + RowMapper rowMapper = RowMapper.beanRowMapper(User.class, propertyColMap); + + // 查询时使用别名匹配自定义映射 + Optional user = template.queryFirst( + "SELECT id, username AS user_name_1, email, age, balance, active FROM users WHERE username = ?", + new Object[]{"bob"}, rowMapper); + + assertTrue(user.isPresent()); + assertEquals("bob", user.get().getUsername()); + + logger.info("自定义列映射: {}", user.get()); + } + + @Test + @DisplayName("DefaultBeanRowMapper:propertyColMap 未覆盖的属性走默认映射") + void testDefaultBeanRowMapperPartialPropertyColMap() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + // 只映射 username,其他走默认小驼峰→下划线 + Map propertyColMap = new HashMap<>(); + propertyColMap.put("username", "user_label"); + + RowMapper rowMapper = RowMapper.beanRowMapper(User.class, propertyColMap); + + Optional user = template.queryFirst( + "SELECT id, username AS user_label, email, age FROM users WHERE username = ?", + new Object[]{"eve"}, rowMapper); + + assertTrue(user.isPresent()); + assertEquals("eve", user.get().getUsername()); + assertEquals("eve@example.com", user.get().getEmail()); + assertEquals(Integer.valueOf(31), user.get().getAge()); + } + + // ==================== DefaultBeanRowMapper 边界 ==================== + + @Test + @DisplayName("DefaultBeanRowMapper:Bean 包含不匹配列时正常忽略") + void testDefaultBeanRowMapperUnmatchedColumns() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + RowMapper rowMapper = RowMapper.beanRowMapper(User.class); + + // 查询返回的列比 Bean 定义的少 + Optional user = template.queryFirst( + "SELECT id, username FROM users WHERE username = ?", + new Object[]{"alice"}, rowMapper); + + assertTrue(user.isPresent()); + assertEquals("alice", user.get().getUsername()); + // 未映射的属性应为 null + assertNull(user.get().getEmail()); + assertNull(user.get().getAge()); + } + + @Test + @DisplayName("DefaultBeanRowMapper:无无参构造器的 Bean 抛出 SQLException") + void testDefaultBeanRowMapperNoNoArgConstructor() { + assertThrows(SQLException.class, () -> + DefaultBeanRowMapper.of(BeanWithoutNoArgConstructor.class)); + } + + @Test + @DisplayName("DefaultBeanRowMapper:RowMapper.beanRowMapper 静态工厂方法") + void testRowMapperStaticBeanRowMapper() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + RowMapper rowMapper = RowMapper.beanRowMapper(User.class); + + Optional user = template.queryFirst( + "SELECT * FROM users WHERE username = ?", + new Object[]{"alice"}, rowMapper); + + assertTrue(user.isPresent()); + assertEquals("alice", user.get().getUsername()); + } + + // ==================== HASH_MAP_MAPPER ==================== + + @Test + @DisplayName("HASH_MAP_MAPPER:所有列映射为 Map") + void testHashMapMapper() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + Optional> user = template.queryFirst( + "SELECT id, username, email, age FROM users WHERE username = ?", + new Object[]{"bob"}); + + assertTrue(user.isPresent()); + Map map = user.get(); + assertEquals(2L, map.get("id")); + assertEquals("bob", map.get("username")); + assertEquals("bob@example.com", map.get("email")); + assertEquals(35, map.get("age")); + + logger.info("HASH_MAP_MAPPER 映射结果: {}", map); + } + + @Test + @DisplayName("HASH_MAP_MAPPER:空结果返回 Optional.empty()") + void testHashMapMapperEmpty() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + Optional> user = template.queryFirst( + "SELECT * FROM users WHERE id = ?", + new Object[]{999}); + + assertFalse(user.isPresent()); + } + + @Test + @DisplayName("HASH_MAP_MAPPER:查询列表返回 List") + void testHashMapMapperList() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + List> users = template.queryList( + "SELECT id, username FROM users ORDER BY id"); + + assertEquals(5, users.size()); + // 第一行 + assertEquals("alice", users.get(0).get("username")); + assertEquals(1L, users.get(0).get("id")); + } + + // ==================== 自定义 RowMapper 对比 ==================== + + @Test + @DisplayName("自定义 RowMapper 与 DefaultBeanRowMapper 结果一致") + void testCustomVsDefaultRowMapper() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + UserRowMapper customMapper = new UserRowMapper(); + DefaultBeanRowMapper defaultMapper = DefaultBeanRowMapper.of(User.class); + + Optional userByCustom = template.queryFirst( + "SELECT * FROM users WHERE username = ?", + new Object[]{"alice"}, customMapper); + + Optional userByDefault = template.queryFirst( + "SELECT * FROM users WHERE username = ?", + new Object[]{"alice"}, defaultMapper); + + assertTrue(userByCustom.isPresent()); + assertTrue(userByDefault.isPresent()); + + assertEquals(userByCustom.get().getId(), userByDefault.get().getId()); + assertEquals(userByCustom.get().getUsername(), userByDefault.get().getUsername()); + assertEquals(userByCustom.get().getEmail(), userByDefault.get().getEmail()); + + logger.info("自定义 RowMapper: {}", userByCustom.get()); + logger.info("DefaultBeanRowMapper: {}", userByDefault.get()); + } + + // ==================== 辅助类 ==================== + + /** + * 无无参构造器的 Bean,用于验证 DefaultBeanRowMapper 的异常处理。 + */ + public static class BeanWithoutNoArgConstructor { + private String name; + + public BeanWithoutNoArgConstructor(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/src/test/java/xyz/zhouxy/jdbc/test/SimpleJdbcTemplateTests.java b/src/test/java/xyz/zhouxy/jdbc/test/SimpleJdbcTemplateTests.java deleted file mode 100644 index 76b6fb4..0000000 --- a/src/test/java/xyz/zhouxy/jdbc/test/SimpleJdbcTemplateTests.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright 2026-present the original author or authors. - * - * 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.test; - -import static org.junit.jupiter.api.Assertions.*; -import static xyz.zhouxy.jdbc.ParamBuilder.buildParams; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.sql.SQLException; -import java.time.LocalDateTime; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import org.h2.jdbcx.JdbcDataSource; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.io.Resources; - -import xyz.zhouxy.jdbc.JdbcOperations; -import xyz.zhouxy.jdbc.RowMapper; -import xyz.zhouxy.jdbc.SimpleJdbcTemplate; -import xyz.zhouxy.jdbc.TransactionException; -import xyz.zhouxy.plusone.commons.util.IdGenerator; -import xyz.zhouxy.plusone.commons.util.IdWorker; - -class SimpleJdbcTemplateTests { - - private static final Logger log = LoggerFactory.getLogger(SimpleJdbcTemplateTests.class); - - private static SimpleJdbcTemplate jdbcTemplate; - - final IdWorker idGenerator = IdGenerator.getSnowflakeIdGenerator(0); - - @BeforeAll - static void initH2() throws IOException, SQLException { - JdbcDataSource dataSource = new JdbcDataSource(); - dataSource.setURL("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=FALSE;MODE=MySQL"); - dataSource.setUser("sa"); - dataSource.setPassword(""); - jdbcTemplate = new SimpleJdbcTemplate(dataSource); - - // 建表 - executeSqlFile("schema.sql"); - } - - @BeforeEach - void initData() throws IOException, SQLException { - // 初始化数据 - executeSqlFile("data.sql"); - } - - static void executeSqlFile(String filePath) throws IOException, SQLException { - String[] sqls = Resources - .toString(Resources.getResource(filePath), StandardCharsets.UTF_8) - .split(";"); - for (String sql : sqls) { - jdbcTemplate.update(sql); - } - } - - @Test - void testQuery() throws SQLException { - Object[] ids = buildParams(5, 9, 13, 14, 17, 20, 108); - String sql = "SELECT id, username, account_status FROM sys_account WHERE id IN (?, ?, ?, ?, ?, ?, ?)"; - log.info(sql); - List> rs = jdbcTemplate.queryList(sql, ids); - for (Map dbRecord : rs) { - log.info("{}", dbRecord); - } - List> expected = ImmutableList.of( - ImmutableMap.of("id", 5L, "account_status", "0", "username", "zhouxy5"), - ImmutableMap.of("id", 9L, "account_status", "0", "username", "zhouxy9"), - ImmutableMap.of("id", 13L, "account_status", "1", "username", "zhouxy13"), - ImmutableMap.of("id", 14L, "account_status", "1", "username", "zhouxy14"), - ImmutableMap.of("id", 17L, "account_status", "1", "username", "zhouxy17"), - ImmutableMap.of("id", 20L, "account_status", "2", "username", "zhouxy20") - ); - assertEquals(expected, rs); - } - - @Test - void testQueryExists() throws SQLException { - boolean isExists = jdbcTemplate.queryBoolean( - "SELECT EXISTS(SELECT 1 FROM sys_account WHERE id = ? LIMIT 1)", - buildParams(998)); - assertFalse(isExists); - } - - @Test - void testInsert() throws SQLException { - List> keys = jdbcTemplate.updateAndReturnKeys( - "INSERT INTO sys_account(username, account_status, created_by) VALUES (?, ?, ?), (?, ?, ?)", - buildParams("zhouxy21", "2", 123L, "code22", '2', 456L), - RowMapper.HASH_MAP_MAPPER); - log.info("keys: {}", keys); - assertEquals(2, keys.size()); - for (Map key : keys) { - assertTrue(key.containsKey("id")); - assertInstanceOf(Long.class, key.get("id")); - assertTrue(key.containsKey("create_time")); - assertInstanceOf(Date.class, key.get("create_time")); - } - List ids = jdbcTemplate.updateAndReturnKeys( - "INSERT INTO sys_account(username, account_status, created_by) VALUES (?, ?, ?), (?, ?, ?)", - buildParams("zhouxy21", "2", 123L, "code22", '2', 456L), - (rs, rowNumber) -> rs.getObject("id", Long.class)); - log.info("ids: {}", ids); - assertEquals(2, ids.size()); - } - - @Test - void testUpdate() throws SQLException { - List> keys = jdbcTemplate.updateAndReturnKeys( - "UPDATE sys_account SET account_status = ?, version = version + 1, update_time = now(), updated_by = ? WHERE id = ? AND version = ?", - buildParams("7", 886L, 20L, 88L), - RowMapper.HASH_MAP_MAPPER); - assertEquals(1, keys.size()); - log.info("keys: {}", keys); - keys = jdbcTemplate.updateAndReturnKeys( - "UPDATE sys_account SET account_status = ?, version = version + 1, update_time = now(), updated_by = ? WHERE id = ? AND version = ?", - buildParams("-1", 886L, 20L, 88L), - RowMapper.HASH_MAP_MAPPER); - assertEquals(0, keys.size()); - } - - @Test - void testTransaction() throws TransactionException, SQLException { - // 抛异常,回滚 - { - long id = this.idGenerator.nextId(); - TransactionException e = assertThrows(TransactionException.class, () -> { - jdbcTemplate.executeTransaction((JdbcOperations jdbc) -> { - jdbc.update("INSERT INTO sys_account (id, username, created_by, create_time, account_status) VALUES (?, ?, ?, ?, ?)", - buildParams(id, "testTransaction1", 100, LocalDateTime.now(), "55")); - throw new NullPointerException(); - }); - }); - assertEquals(NullPointerException.class, e.getCause().getClass()); - Optional> first = jdbcTemplate - .queryFirst("SELECT * FROM sys_account WHERE id = ?", buildParams(id)); - log.info("first: {}", first); - assertFalse(first.isPresent()); - } - - // 没有异常,提交事务 - { - long id = this.idGenerator.nextId(); - jdbcTemplate.executeTransaction(jdbc -> { - jdbc.update("INSERT INTO sys_account (id, username, created_by, create_time, account_status) VALUES (?, ?, ?, ?, ?)", - buildParams(id, "testTransaction2", 101, LocalDateTime.now(), "55")); - }); - - Optional> first = jdbcTemplate - .queryFirst("SELECT * FROM sys_account WHERE id = ?", buildParams(id)); - log.info("first: {}", first); - assertTrue(first.isPresent()); - } - - // 抛异常,回滚 - { - long id = this.idGenerator.nextId(); - TransactionException e = assertThrows(TransactionException.class, () -> { - jdbcTemplate.commitIfTrue(jdbc -> { - jdbc.update("INSERT INTO sys_account (id, username, created_by, create_time, account_status) VALUES (?, ?, ?, ?, ?)", - buildParams(id, "testTransaction3", 102, LocalDateTime.now(), "55")); - throw new NullPointerException(); - }); - }); - assertEquals(NullPointerException.class, e.getCause().getClass()); - Optional> first = jdbcTemplate - .queryFirst("SELECT * FROM sys_account WHERE id = ?", buildParams(id)); - log.info("first: {}", first); - assertFalse(first.isPresent()); - } - - // 返回 false,回滚 - { - long id = this.idGenerator.nextId(); - jdbcTemplate.commitIfTrue(jdbc -> { - jdbc.update("INSERT INTO sys_account (id, username, created_by, create_time, account_status) VALUES (?, ?, ?, ?, ?)", - buildParams(id, "testTransaction4", 103, LocalDateTime.now(), "55")); - return false; - }); - - Optional> first = jdbcTemplate - .queryFirst("SELECT * FROM sys_account WHERE id = ?", buildParams(id)); - log.info("first: {}", first); - assertFalse(first.isPresent()); - } - - // 返回 true,提交事务 - { - long id = this.idGenerator.nextId(); - jdbcTemplate.commitIfTrue(jdbc -> { - jdbc.update("INSERT INTO sys_account (id, username, created_by, create_time, account_status) VALUES (?, ?, ?, ?, ?)", - buildParams(id, "testTransaction5", 104, LocalDateTime.now(), "55")); - return true; - }); - - Optional> first = jdbcTemplate - .queryFirst("SELECT * FROM sys_account WHERE id = ?", buildParams(id)); - log.info("first: {}", first); - assertTrue(first.isPresent()); - } - } - - @Test - void testBean() throws Exception { - Optional t = jdbcTemplate.queryFirst( - "SELECT * FROM sys_account WHERE id = ?", - buildParams(18L), - RowMapper.beanRowMapper(AccountPO.class)); - assertTrue(t.isPresent()); - assertEquals( - new AccountPO(18L, "zhouxy18", "1", - LocalDateTime.of(2000, 1, 1, 0, 0), 118L, - LocalDateTime.of(2000, 1, 29, 0, 0), null, 7L), - t.get()); - log.info("{}", t); - } - - @Test - void testQueryBoolean() throws SQLException { - // 建议写法 - assertTrue(jdbcTemplate.queryBoolean("SELECT EXISTS(SELECT 1 FROM sys_account WHERE id = 10)")); - assertFalse(jdbcTemplate.queryBoolean("SELECT EXISTS(SELECT 1 FROM sys_account WHERE id = 999)")); - - // 不建议写法 - assertTrue(jdbcTemplate.queryBoolean("SELECT 1 FROM sys_account WHERE id = 10")); - assertFalse(jdbcTemplate.queryBoolean("SELECT 1 FROM sys_account WHERE id = 999")); - } -} diff --git a/src/test/java/xyz/zhouxy/jdbc/test/TransactionTest.java b/src/test/java/xyz/zhouxy/jdbc/test/TransactionTest.java new file mode 100644 index 0000000..d8cccc1 --- /dev/null +++ b/src/test/java/xyz/zhouxy/jdbc/test/TransactionTest.java @@ -0,0 +1,233 @@ +package xyz.zhouxy.jdbc.test; + +import static org.junit.jupiter.api.Assertions.*; +import static xyz.zhouxy.jdbc.ParamBuilder.buildParams; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import xyz.zhouxy.jdbc.JdbcOperations; +import xyz.zhouxy.jdbc.SimpleJdbcTemplate; +import xyz.zhouxy.jdbc.TransactionException; + +/** + * 事务 API 测试:executeTransaction、commitIfTrue。 + */ +@DisplayName("SimpleJdbcTemplate 事务操作") +class TransactionTest extends BaseH2Test { + + private static final Logger logger = LoggerFactory.getLogger(TransactionTest.class); + + @BeforeEach + void setUp() throws Exception { + resetDatabase(); + } + + // ==================== executeTransaction 正常提交 ==================== + + @Test + @DisplayName("executeTransaction:正常提交,数据持久化") + void testExecuteTransactionCommit() throws Exception { + SimpleJdbcTemplate template = createTemplate(); + + template.executeTransaction((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 = ?", + buildParams(99999L, "alice")); + }); + + // 验证事务已提交 + Optional newUser = template.queryFirst( + "SELECT username FROM users WHERE username = ?", + buildParams("txUser1"), String.class); + assertTrue(newUser.isPresent()); + + Optional balance = template.queryFirst( + "SELECT balance FROM users WHERE username = ?", + buildParams("alice"), Long.class); + assertEquals(Long.valueOf(99999L), balance.orElse(null)); + + logger.info("事务提交验证通过"); + } + + // ==================== executeTransaction 异常回滚 ==================== + + @Test + @DisplayName("executeTransaction:异常回滚,数据恢复原状") + void testExecuteTransactionRollback() throws Exception { + SimpleJdbcTemplate template = createTemplate(); + + // 记录原始 balance + Optional originalBalance = template.queryFirst( + "SELECT balance FROM users WHERE username = ?", + buildParams("alice"), Long.class); + + TransactionException ex = assertThrows(TransactionException.class, () -> + template.executeTransaction((JdbcOperations ops) -> { + ops.update("UPDATE users SET balance = ? WHERE username = ?", + buildParams(0L, "alice")); + ops.update("INSERT INTO users (username, email) VALUES (?, ?)", + buildParams("txUser2", "tx2@test.com")); + // 故意抛出异常触发回滚 + throw new RuntimeException("模拟业务异常"); + })); + + logger.info("捕获到 TransactionException: {}", ex.getMessage()); + assertNotNull(ex.getCause()); + assertEquals(RuntimeException.class, ex.getCause().getClass()); + assertEquals("模拟业务异常", ex.getCause().getMessage()); + + // 验证更新已回滚 + Optional currentBalance = template.queryFirst( + "SELECT balance FROM users WHERE username = ?", + buildParams("alice"), Long.class); + assertEquals(originalBalance.orElse(null), currentBalance.orElse(null)); + + // 验证插入已回滚 + Optional rolledBackUser = template.queryFirst( + "SELECT username FROM users WHERE username = ?", + buildParams("txUser2"), String.class); + assertFalse(rolledBackUser.isPresent()); + + logger.info("事务回滚验证通过"); + } + + @Test + @DisplayName("executeTransaction:SQL 异常触发回滚") + void testExecuteTransactionSqlExceptionRollback() { + SimpleJdbcTemplate template = createTemplate(); + + assertThrows(TransactionException.class, () -> + template.executeTransaction((JdbcOperations ops) -> { + ops.update("INSERT INTO users (username) VALUES (?)", + buildParams("validUser")); + // 错误的 SQL + ops.update("INVALID SQL STATEMENT"); + })); + + // 验证插入已回滚 + assertDoesNotThrow(() -> { + Optional user = template.queryFirst( + "SELECT username FROM users WHERE username = ?", + buildParams("validUser"), String.class); + assertFalse(user.isPresent()); + }); + } + + // ==================== commitIfTrue 返回 true 提交 ==================== + + @Test + @DisplayName("commitIfTrue:返回 true 提交事务") + void testCommitIfTrueCommit() throws Exception { + SimpleJdbcTemplate template = createTemplate(); + + template.commitIfTrue((JdbcOperations ops) -> { + ops.update("INSERT INTO users (username, email) VALUES (?, ?)", + buildParams("cftUser", "cft@test.com")); + return true; + }); + + // 验证数据已持久化 + Optional user = template.queryFirst( + "SELECT username FROM users WHERE username = ?", + buildParams("cftUser"), String.class); + assertTrue(user.isPresent()); + + logger.info("commitIfTrue(true) 提交验证通过"); + } + + @Test + @DisplayName("commitIfTrue:返回 false 回滚事务") + void testCommitIfFalseRollback() throws Exception { + SimpleJdbcTemplate template = createTemplate(); + + template.commitIfTrue((JdbcOperations ops) -> { + ops.update("INSERT INTO users (username, email) VALUES (?, ?)", + buildParams("cffUser", "cff@test.com")); + return false; + }); + + // 验证数据已回滚 + Optional user = template.queryFirst( + "SELECT username FROM users WHERE username = ?", + buildParams("cffUser"), String.class); + assertFalse(user.isPresent()); + + logger.info("commitIfTrue(false) 回滚验证通过"); + } + + @Test + @DisplayName("commitIfTrue:异常触发回滚") + void testCommitIfTrueExceptionRollback() { + SimpleJdbcTemplate template = createTemplate(); + + assertThrows(TransactionException.class, () -> + template.commitIfTrue((JdbcOperations ops) -> { + ops.update("INSERT INTO users (username) VALUES (?)", + buildParams("exUser")); + throw new IllegalStateException("条件不满足"); + })); + + // 验证回滚 + assertDoesNotThrow(() -> { + Optional user = template.queryFirst( + "SELECT username FROM users WHERE username = ?", + buildParams("exUser"), String.class); + assertFalse(user.isPresent()); + }); + } + + // ==================== 事务内查询可见性 ==================== + + @Test + @DisplayName("executeTransaction:事务内可查询到未提交的数据") + void testTransactionVisibility() throws Exception { + SimpleJdbcTemplate template = createTemplate(); + + template.executeTransaction((JdbcOperations ops) -> { + ops.update("INSERT INTO users (username, email) VALUES (?, ?)", + buildParams("visible", "visible@test.com")); + + // 在同一事务内可以查询到刚插入的数据 + Optional user = ops.queryFirst( + "SELECT username FROM users WHERE username = ?", + buildParams("visible"), String.class); + assertTrue(user.isPresent()); + + logger.info("事务内查询可见性验证通过"); + }); + } + + // ==================== 边界情况 ==================== + + @Test + @DisplayName("executeTransaction:空操作(无异常)正常提交") + void testExecuteTransactionEmpty() throws Exception { + SimpleJdbcTemplate template = createTemplate(); + + // 空操作不应抛异常 + assertDoesNotThrow(() -> + template.executeTransaction(ops -> { /* no-op */ })); + + // 数据应保持不变 + int count = template.query("SELECT COUNT(*) FROM users", + rs -> { rs.next(); return rs.getInt(1); }); + assertEquals(5, count); + } + + @Test + @DisplayName("executeTransaction:null 操作抛异常") + @SuppressWarnings("null") + void testExecuteTransactionNullOps() { + SimpleJdbcTemplate template = createTemplate(); + + assertThrows(Exception.class, () -> + template.executeTransaction(null)); + } +} diff --git a/src/test/java/xyz/zhouxy/jdbc/test/UpdateTest.java b/src/test/java/xyz/zhouxy/jdbc/test/UpdateTest.java new file mode 100644 index 0000000..e81df85 --- /dev/null +++ b/src/test/java/xyz/zhouxy/jdbc/test/UpdateTest.java @@ -0,0 +1,219 @@ +package xyz.zhouxy.jdbc.test; + +import static org.junit.jupiter.api.Assertions.*; +import static xyz.zhouxy.jdbc.ParamBuilder.buildParams; + +import java.sql.SQLException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import xyz.zhouxy.jdbc.RowMapper; +import xyz.zhouxy.jdbc.SimpleJdbcTemplate; + +/** + * 更新 API 测试:update、updateAndReturnKeys。 + */ +@DisplayName("SimpleJdbcTemplate 更新操作") +class UpdateTest extends BaseH2Test { + + private static final Logger logger = LoggerFactory.getLogger(UpdateTest.class); + + @BeforeEach + void setUp() throws Exception { + resetDatabase(); + } + + // ==================== update ==================== + + @Test + @DisplayName("update:INSERT 操作返回影响行数 1") + void testUpdateInsert() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + User user = new User( + "frank", + "frank@example.com", + 25, 10000L, + true, + LocalDateTime.now(), + LocalDate.now(), + LocalTime.now()); + String sql = "INSERT INTO users (" // + + " username," // + + " email," // + + " age," // + + " balance," // + + " active," // + + " created_at," // + + " birth_date," // + + " work_start_time" // + + ")" // + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + int rows = template.update( + sql, + buildParams( + user.getUsername(), + user.getEmail(), + user.getAge(), + user.getBalance(), + user.getActive(), + user.getCreatedAt(), + user.getBirthDate(), + user.getWorkStartTime() + )); + + logger.info("INSERT 影响行数: {}", rows); + assertEquals(1, rows); + } + + @Test + @DisplayName("update:UPDATE 操作返回影响行数") + void testUpdateModify() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + int rows = template.update( + "UPDATE users SET email = ? WHERE username = ?", + buildParams("newalice@example.com", "alice")); + + logger.info("UPDATE 影响行数: {}", rows); + assertEquals(1, rows); + + // 验证数据确实被更新 + int count = template.update( + "UPDATE users SET email = ? WHERE username = ? AND email = ?", + buildParams("alice@example.com", "alice", "newalice@example.com")); + assertEquals(1, count); + } + + @Test + @DisplayName("update:DELETE 操作返回影响行数") + void testUpdateDelete() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + int rows = template.update( + "DELETE FROM users WHERE username = ?", + buildParams("charlie")); + + logger.info("DELETE 影响行数: {}", rows); + assertEquals(1, rows); + } + + @Test + @DisplayName("update:影响 0 行") + void testUpdateZeroRows() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + int rows = template.update( + "UPDATE users SET email = ? WHERE username = ?", + buildParams("x@x.com", "nobody")); + + logger.info("影响 0 行: {}", rows); + assertEquals(0, rows); + } + + @Test + @DisplayName("update:DELETE 全表") + void testUpdateDeleteAll() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + int rows = template.update("DELETE FROM users"); + + logger.info("DELETE 全表影响行数: {}", rows); + assertEquals(5, rows); + + // 验证表为空 + int count = template.query("SELECT COUNT(*) FROM users", + rs -> { rs.next(); return rs.getInt(1); }); + assertEquals(0, count); + } + + @Test + @DisplayName("update:null 参数数组") + void testUpdateWithNullParams() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + int rows = template.update( + "DELETE FROM users WHERE username = ?", + new Object[]{ null }); + + // 因为 DELETE ? 中参数 null 不会匹配任何行 + assertEquals(0, rows); + } + + @Test + @DisplayName("update:无参数重载") + void testUpdateNoParams() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + int rows = template.update( + "UPDATE users SET balance = 9999 WHERE username = 'alice'"); + + assertEquals(1, rows); + } + + @Test + @DisplayName("update:语法错误抛出 SQLException") + void testUpdateInvalidSql() { + SimpleJdbcTemplate template = createTemplate(); + + assertThrows(SQLException.class, () -> + template.update("UPDAT users SET x = 1")); + } + + // ==================== updateAndReturnKeys ==================== + + @Test + @DisplayName("updateAndReturnKeys:INSERT 返回自增主键") + void testUpdateAndReturnKeys() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + List keys = template.updateAndReturnKeys( + "INSERT INTO users (username, email, age, balance, active) VALUES (?, ?, ?, ?, ?)", + new Object[]{"grace", "grace@example.com", 29, 12000L, true}, + (rs, rowNumber) -> rs.getLong(1)); + + logger.info("updateAndReturnKeys 返回: {}", keys); + assertEquals(1, keys.size()); + assertTrue(keys.get(0) > 0); + } + + @Test + @DisplayName("updateAndReturnKeys:批量插入,返回所有自增主键") + void testUpdateAndReturnKeysMultiple() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + // 使用多条 INSERT(H2 支持) + List keys = template.updateAndReturnKeys( + "INSERT INTO users (username, email, age, balance, active) VALUES " + + "(?, ?, ?, ?, ?), (?, ?, ?, ?, ?)", + new Object[]{ + "henry", "henry@example.com", 33, 18000L, true, + "iris", "iris@example.com", 27, 9000L, false + }, + (RowMapper) (rs, rowNumber) -> rs.getLong(1)); + + logger.info("updateAndReturnKeys 返回 {} 个主键", keys.size()); + assertEquals(2, keys.size()); + } + + @Test + @DisplayName("updateAndReturnKeys:无参数重载") + void testUpdateAndReturnKeysNoParams() throws SQLException { + SimpleJdbcTemplate template = createTemplate(); + + List keys = template.updateAndReturnKeys( + "INSERT INTO users (username) VALUES ('jack')", + (rs, rowNumber) -> rs.getLong(1)); + + assertEquals(1, keys.size()); + assertTrue(keys.get(0) > 0); + } +} diff --git a/src/test/java/xyz/zhouxy/jdbc/test/User.java b/src/test/java/xyz/zhouxy/jdbc/test/User.java new file mode 100644 index 0000000..f55096e --- /dev/null +++ b/src/test/java/xyz/zhouxy/jdbc/test/User.java @@ -0,0 +1,144 @@ +package xyz.zhouxy.jdbc.test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Objects; + +/** + * 测试用的 Java Bean,用于验证 RowMapper 映射。 + * + *

注意:属性全部使用引用类型,以匹配 DefaultBeanRowMapper 的设计约束。

+ */ +public class User { + + private Long id; + private String username; + private String email; + private Integer age; + private Long balance; + private Boolean active; + private LocalDateTime createdAt; + private LocalDate birthDate; + private LocalTime workStartTime; + + public User() { + } + + public User(String username, String email, Integer age, Long balance, Boolean active) { + this.username = username; + this.email = email; + this.age = age; + this.balance = balance; + this.active = active; + } + + public User(String username, String email, Integer age, Long balance, Boolean active, + LocalDateTime createdAt, LocalDate birthDate, LocalTime workStartTime) { + this.username = username; + this.email = email; + this.age = age; + this.balance = balance; + this.active = active; + this.createdAt = createdAt; + this.birthDate = birthDate; + this.workStartTime = workStartTime; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Long getBalance() { + return balance; + } + + public void setBalance(Long balance) { + this.balance = balance; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } + + public LocalTime getWorkStartTime() { + return workStartTime; + } + + public void setWorkStartTime(LocalTime workStartTime) { + this.workStartTime = workStartTime; + } + + @Override + public String toString() { + return "User{id=" + id + ", username='" + username + "', email='" + email + + "', age=" + age + ", balance=" + balance + ", active=" + active + '}'; + } + + @Override + public int hashCode() { + return Objects.hash(id, username, email, age, balance, active, createdAt, birthDate, workStartTime); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof User)) + return false; + User other = (User) obj; + return Objects.equals(id, other.id) && Objects.equals(username, other.username) + && Objects.equals(email, other.email) && Objects.equals(age, other.age) + && Objects.equals(balance, other.balance) && Objects.equals(active, other.active) + && Objects.equals(createdAt, other.createdAt) && Objects.equals(birthDate, other.birthDate) + && Objects.equals(workStartTime, other.workStartTime); + } +} diff --git a/src/test/java/xyz/zhouxy/jdbc/test/UserRowMapper.java b/src/test/java/xyz/zhouxy/jdbc/test/UserRowMapper.java new file mode 100644 index 0000000..fdec66c --- /dev/null +++ b/src/test/java/xyz/zhouxy/jdbc/test/UserRowMapper.java @@ -0,0 +1,29 @@ +package xyz.zhouxy.jdbc.test; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import xyz.zhouxy.jdbc.RowMapper; + +/** + * 自定义 User RowMapper,直接操作 ResultSet 进行映射。 + * + *

相较于 DefaultBeanRowMapper,自定义 RowMapper 避免了反射开销,性能更优。

+ */ +public class UserRowMapper implements RowMapper { + + @Override + public User mapRow(ResultSet rs, int rowNumber) throws SQLException { + User user = new User(); + user.setId(rs.getObject("id", Long.class)); + user.setUsername(rs.getString("username")); + user.setEmail(rs.getString("email")); + user.setAge(rs.getObject("age", Integer.class)); + user.setBalance(rs.getObject("balance", Long.class)); + user.setActive(rs.getObject("active", Boolean.class)); + user.setCreatedAt(rs.getObject("created_at", java.time.LocalDateTime.class)); + user.setBirthDate(rs.getObject("birth_date", java.time.LocalDate.class)); + user.setWorkStartTime(rs.getObject("work_start_time", java.time.LocalTime.class)); + return user; + } +} diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql deleted file mode 100644 index d392925..0000000 --- a/src/test/resources/data.sql +++ /dev/null @@ -1,23 +0,0 @@ -truncate table sys_account; - -INSERT INTO sys_account(id, username, account_status, created_by) VALUES (2, 'zhouxy2', '0', 108); -INSERT INTO sys_account(id, username, account_status, created_by) VALUES (3, 'zhouxy3', '0', 108); -INSERT INTO sys_account(id, username, account_status, created_by) VALUES (4, 'zhouxy4', '0', 108); -INSERT INTO sys_account(id, username, account_status, created_by) VALUES (5, 'zhouxy5', '0', 108); -INSERT INTO sys_account(id, username, account_status, created_by) VALUES (6, 'zhouxy6', '0', 108); -INSERT INTO sys_account(id, username, account_status, created_by) VALUES (7, 'zhouxy7', '0', 108); -INSERT INTO sys_account(id, username, account_status, created_by) VALUES (8, 'zhouxy8', '0', 108); -INSERT INTO sys_account(id, username, account_status, created_by) VALUES (9, 'zhouxy9', '0', 108); - -INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (10, 'zhouxy10', '1', 118, '2000-01-01', '2000-01-29', 31); -INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (11, 'zhouxy11', '1', 118, '2000-01-01', '2000-01-29', 28); -INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (12, 'zhouxy12', '1', 118, '2000-01-01', '2000-01-29', 25); -INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (13, 'zhouxy13', '1', 118, '2000-01-01', '2000-01-29', 22); -INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (14, 'zhouxy14', '1', 118, '2000-01-01', '2000-01-29', 19); -INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (15, 'zhouxy15', '1', 118, '2000-01-01', '2000-01-29', 16); -INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (16, 'zhouxy16', '1', 118, '2000-01-01', '2000-01-29', 13); -INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (17, 'zhouxy17', '1', 118, '2000-01-01', '2000-01-29', 10); -INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (18, 'zhouxy18', '1', 118, '2000-01-01', '2000-01-29', 7 ); -INSERT INTO sys_account(id, username, account_status, created_by, create_time, update_time, `version`) VALUES (19, 'zhouxy19', '1', 118, '2000-01-01', '2000-01-29', 0 ); - -INSERT INTO sys_account(id, username, account_status, created_by, create_time, updated_by, update_time, `version`) VALUES (20, 'zhouxy20', '2', 118, '2008-08-08 20:08:00', 31, now(), 88); diff --git a/src/test/resources/init_tables.sql b/src/test/resources/init_tables.sql new file mode 100644 index 0000000..95d5156 --- /dev/null +++ b/src/test/resources/init_tables.sql @@ -0,0 +1,30 @@ +DROP TABLE IF EXISTS users; + +CREATE TABLE users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL, + email VARCHAR(100), + age INT, + balance BIGINT, + active BOOLEAN, + created_at TIMESTAMP, + birth_date DATE, + work_start_time TIME +); + +ALTER TABLE users ADD CONSTRAINT uk_username UNIQUE (username); + +INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time) +VALUES ('alice', 'alice@example.com', 28, 15000, TRUE, '2024-01-15 09:30:00', '1996-05-20', '08:00:00'); + +INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time) +VALUES ('bob', 'bob@example.com', 35, 25000, TRUE, '2023-11-01 14:00:00', '1989-03-12', '09:30:00'); + +INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time) +VALUES ('charlie', NULL, NULL, 5000, FALSE, '2025-03-10 11:15:00', NULL, NULL); + +INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time) +VALUES ('diana', 'diana@example.com', 42, NULL, TRUE, NULL, '1982-11-08', '07:45:00'); + +INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time) +VALUES ('eve', 'eve@example.com', 31, 8000, TRUE, '2024-07-22 16:45:00', '1993-01-30', '10:00:00'); diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql deleted file mode 100644 index e22f5a0..0000000 --- a/src/test/resources/schema.sql +++ /dev/null @@ -1,12 +0,0 @@ -DROP TABLE IF EXISTS sys_account; - -CREATE TABLE sys_account ( - `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY - , `username` VARCHAR(255) NOT NULL - , `account_status` VARCHAR(2) NOT NULL - , `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP - , `created_by` BIGINT NOT NULL - , `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP - , `updated_by` BIGINT DEFAULT NULL - , `version` BIGINT NOT NULL DEFAULT 0 -);