From 100729b2e9935f02877a895c5320b95b455b8ab5 Mon Sep 17 00:00:00 2001 From: yoondgu Date: Thu, 5 Oct 2023 03:35:24 +0900 Subject: [PATCH 1/6] =?UTF-8?q?test:=20Transaction=20=ED=95=99=EC=8A=B5=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/transaction/stage1/Stage1Test.java | 75 +++++----------- .../transaction/stage2/FirstUserService.java | 8 +- .../java/transaction/stage2/Stage2Test.java | 90 +++++++++++-------- 3 files changed, 78 insertions(+), 95 deletions(-) diff --git a/study/src/test/java/transaction/stage1/Stage1Test.java b/study/src/test/java/transaction/stage1/Stage1Test.java index acd7c50cda..5dbd3e6fbb 100644 --- a/study/src/test/java/transaction/stage1/Stage1Test.java +++ b/study/src/test/java/transaction/stage1/Stage1Test.java @@ -20,23 +20,15 @@ import transaction.RunnableWrapper; /** - * 격리 레벨(Isolation Level)에 따라 여러 사용자가 동시에 db에 접근했을 때 어떤 문제가 발생하는지 확인해보자. - * ❗phantom reads는 docker를 실행한 상태에서 테스트를 실행한다. + * 격리 레벨(Isolation Level)에 따라 여러 사용자가 동시에 db에 접근했을 때 어떤 문제가 발생하는지 확인해보자. ❗phantom reads는 docker를 실행한 상태에서 테스트를 실행한다. * ❗phantom reads는 MySQL로 확인한다. H2 데이터베이스에서는 발생하지 않는다. - * - * 참고 링크 - * https://en.wikipedia.org/wiki/Isolation_(database_systems) - * - * 각 테스트에서 어떤 현상이 발생하는지 직접 경험해보고 아래 표를 채워보자. - * + : 발생 - * - : 발생하지 않음 - * Read phenomena | Dirty reads | Non-repeatable reads | Phantom reads - * Isolation level | | | - * -----------------|-------------|----------------------|-------------- - * Read Uncommitted | | | - * Read Committed | | | - * Repeatable Read | | | - * Serializable | | | + *

+ * 참고 링크 https://en.wikipedia.org/wiki/Isolation_(database_systems) + *

+ * 각 테스트에서 어떤 현상이 발생하는지 직접 경험해보고 아래 표를 채워보자. + : 발생 - : 발생하지 않음 Read phenomena | Dirty reads | Non-repeatable reads | + * Phantom reads Isolation level | | | + * -----------------|-------------|----------------------|-------------- Read Uncommitted | | | Read + * Committed | | | Repeatable Read | | | Serializable | | | */ class Stage1Test { @@ -51,16 +43,9 @@ private void setUp(final DataSource dataSource) { } /** - * 격리 수준에 따라 어떤 현상이 발생하는지 테스트를 돌려 직접 눈으로 확인하고 표를 채워보자. - * + : 발생 - * - : 발생하지 않음 - * Read phenomena | Dirty reads - * Isolation level | - * -----------------|------------- - * Read Uncommitted | - * Read Committed | - * Repeatable Read | - * Serializable | + * 격리 수준에 따라 어떤 현상이 발생하는지 테스트를 돌려 직접 눈으로 확인하고 표를 채워보자. + : 발생 - : 발생하지 않음 Read phenomena | Dirty reads Isolation + * level | -----------------|------------- Read Uncommitted | + Read Committed | - Repeatable Read | - + * Serializable | - */ @Test void dirtyReading() throws SQLException { @@ -80,7 +65,7 @@ void dirtyReading() throws SQLException { final var subConnection = dataSource.getConnection(); // 적절한 격리 레벨을 찾는다. - final int isolationLevel = Connection.TRANSACTION_NONE; + final int isolationLevel = Connection.TRANSACTION_READ_COMMITTED; // 트랜잭션 격리 레벨을 설정한다. subConnection.setTransactionIsolation(isolationLevel); @@ -104,19 +89,12 @@ void dirtyReading() throws SQLException { } /** - * 격리 수준에 따라 어떤 현상이 발생하는지 테스트를 돌려 직접 눈으로 확인하고 표를 채워보자. - * + : 발생 - * - : 발생하지 않음 - * Read phenomena | Non-repeatable reads - * Isolation level | - * -----------------|--------------------- - * Read Uncommitted | - * Read Committed | - * Repeatable Read | - * Serializable | + * 격리 수준에 따라 어떤 현상이 발생하는지 테스트를 돌려 직접 눈으로 확인하고 표를 채워보자. + : 발생 - : 발생하지 않음 Read phenomena | Non-repeatable reads + * Isolation level | -----------------|--------------------- Read Uncommitted | + Read Committed | + Repeatable + * Read | - Serializable | - */ @Test - void noneRepeatable() throws SQLException { + void noneRepeatable() throws SQLException, InterruptedException { setUp(createH2DataSource()); // 테스트 전에 필요한 데이터를 추가한다. @@ -129,7 +107,7 @@ void noneRepeatable() throws SQLException { connection.setAutoCommit(false); // 적절한 격리 레벨을 찾는다. - final int isolationLevel = Connection.TRANSACTION_NONE; + final int isolationLevel = Connection.TRANSACTION_REPEATABLE_READ; // 트랜잭션 격리 레벨을 설정한다. connection.setTransactionIsolation(isolationLevel); @@ -153,7 +131,7 @@ void noneRepeatable() throws SQLException { sleep(0.5); // 사용자A가 다시 gugu 객체를 조회했다. - // 사용자B는 패스워드를 변경하고 아직 커밋하지 않았다. + // 사용자B는 패스워드를 변경하고 아직 커밋하지 않았다.?? 커밋되어서 READ_COMMITED에서도 문제가 발생하는 것 아닌가? final var actual = userDao.findByAccount(connection, "gugu"); // 트랜잭션 격리 레벨에 따라 아래 테스트가 통과한다. @@ -165,17 +143,9 @@ void noneRepeatable() throws SQLException { } /** - * phantom read는 h2에서 발생하지 않는다. mysql로 확인해보자. - * 격리 수준에 따라 어떤 현상이 발생하는지 테스트를 돌려 직접 눈으로 확인하고 표를 채워보자. - * + : 발생 - * - : 발생하지 않음 - * Read phenomena | Phantom reads - * Isolation level | - * -----------------|-------------- - * Read Uncommitted | - * Read Committed | - * Repeatable Read | - * Serializable | + * phantom read는 h2에서 발생하지 않는다. mysql로 확인해보자. 격리 수준에 따라 어떤 현상이 발생하는지 테스트를 돌려 직접 눈으로 확인하고 표를 채워보자. + : 발생 - : 발생하지 않음 + * Read phenomena | Phantom reads Isolation level | -----------------|-------------- Read Uncommitted | + Read + * Committed | + Repeatable Read | + (select for update에 한해서) Serializable | - */ @Test void phantomReading() throws SQLException { @@ -183,6 +153,7 @@ void phantomReading() throws SQLException { // testcontainer로 docker를 실행해서 mysql에 연결한다. final var mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0.30")) .withLogConsumer(new Slf4jLogConsumer(log)); + mysql.withUrlParam("allowMultiQueries", "true"); mysql.start(); setUp(createMySQLDataSource(mysql)); @@ -196,7 +167,7 @@ void phantomReading() throws SQLException { connection.setAutoCommit(false); // 적절한 격리 레벨을 찾는다. - final int isolationLevel = Connection.TRANSACTION_NONE; + final int isolationLevel = Connection.TRANSACTION_SERIALIZABLE; // 트랜잭션 격리 레벨을 설정한다. connection.setTransactionIsolation(isolationLevel); diff --git a/study/src/test/java/transaction/stage2/FirstUserService.java b/study/src/test/java/transaction/stage2/FirstUserService.java index f662487454..ff35f0ad7e 100644 --- a/study/src/test/java/transaction/stage2/FirstUserService.java +++ b/study/src/test/java/transaction/stage2/FirstUserService.java @@ -65,7 +65,7 @@ public Set saveAndExceptionWithRequiredNew() { throw new RuntimeException(); } -// @Transactional(propagation = Propagation.REQUIRED) + @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithSupports() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); @@ -76,7 +76,7 @@ public Set saveFirstTransactionWithSupports() { return of(firstTransactionName, secondTransactionName); } -// @Transactional(propagation = Propagation.REQUIRED) + @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithMandatory() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); @@ -98,7 +98,7 @@ public Set saveFirstTransactionWithNotSupported() { return of(firstTransactionName, secondTransactionName); } - @Transactional(propagation = Propagation.REQUIRED) + // @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithNested() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); @@ -109,7 +109,7 @@ public Set saveFirstTransactionWithNested() { return of(firstTransactionName, secondTransactionName); } - @Transactional(propagation = Propagation.REQUIRED) + // @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithNever() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); diff --git a/study/src/test/java/transaction/stage2/Stage2Test.java b/study/src/test/java/transaction/stage2/Stage2Test.java index 3e9c95ab41..2c065d9d6f 100644 --- a/study/src/test/java/transaction/stage2/Stage2Test.java +++ b/study/src/test/java/transaction/stage2/Stage2Test.java @@ -11,12 +11,10 @@ import org.springframework.boot.test.context.SpringBootTest; /** - * 트랜잭션 전파(Transaction Propagation)란? - * 트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정하는 방식을 말한다. - * - * FirstUserService 클래스의 메서드를 실행할 때 첫 번째 트랜잭션이 생성된다. - * SecondUserService 클래스의 메서드를 실행할 때 두 번째 트랜잭션이 어떻게 되는지 관찰해보자. - * + * 트랜잭션 전파(Transaction Propagation)란? 트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정하는 방식을 말한다. + *

+ * FirstUserService 클래스의 메서드를 실행할 때 첫 번째 트랜잭션이 생성된다. SecondUserService 클래스의 메서드를 실행할 때 두 번째 트랜잭션이 어떻게 되는지 관찰해보자. + *

* https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-propagation */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -36,8 +34,7 @@ void tearDown() { } /** - * 생성된 트랜잭션이 몇 개인가? - * 왜 그런 결과가 나왔을까? + * 생성된 트랜잭션이 몇 개인가? 왜 그런 결과가 나왔을까? 전파 속성이 REQUIRED이면, 메서드 내 분리된 논리적 트랜잭션을 하나의 물리적 트랜잭션으로 묶기 때문이다. */ @Test void testRequired() { @@ -45,13 +42,12 @@ void testRequired() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithRequired"); } /** - * 생성된 트랜잭션이 몇 개인가? - * 왜 그런 결과가 나왔을까? + * 생성된 트랜잭션이 몇 개인가? 왜 그런 결과가 나왔을까? 전파 속성이 REQUIRES_NEW이면, 메서드 내 분리된 논리적 트랜잭션을 별개의 물리적 트랜잭션으로 두기 때문이다. */ @Test void testRequiredNew() { @@ -59,27 +55,31 @@ void testRequiredNew() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(2) + .containsExactly( + "transaction.stage2.SecondUserService.saveSecondTransactionWithRequiresNew", + "transaction.stage2.FirstUserService.saveFirstTransactionWithRequiredNew" + ); } /** - * firstUserService.saveAndExceptionWithRequiredNew()에서 강제로 예외를 발생시킨다. - * REQUIRES_NEW 일 때 예외로 인한 롤백이 발생하면서 어떤 상황이 발생하는 지 확인해보자. + * firstUserService.saveAndExceptionWithRequiredNew()에서 강제로 예외를 발생시킨다. REQUIRES_NEW 일 때 예외로 인한 롤백이 발생하면서 어떤 상황이 발생하는 + * 지 확인해보자. */ @Test void testRequiredNewWithRollback() { - assertThat(firstUserService.findAll()).hasSize(-1); + assertThat(firstUserService.findAll()).hasSize(0); assertThatThrownBy(() -> firstUserService.saveAndExceptionWithRequiredNew()) .isInstanceOf(RuntimeException.class); - assertThat(firstUserService.findAll()).hasSize(-1); + // REQUIRES_NEW 인 트랜잭션은 롤백되지 않고 user를 저장했다. + assertThat(firstUserService.findAll()).hasSize(1); } /** - * FirstUserService.saveFirstTransactionWithSupports() 메서드를 보면 @Transactional이 주석으로 되어 있다. - * 주석인 상태에서 테스트를 실행했을 때와 주석을 해제하고 테스트를 실행했을 때 어떤 차이점이 있는지 확인해보자. + * FirstUserService.saveFirstTransactionWithSupports() 메서드를 보면 @Transactional이 주석으로 되어 있다. 주석인 상태에서 테스트를 실행했을 때와 주석을 + * 해제하고 테스트를 실행했을 때 어떤 차이점이 있는지 확인해보자. */ @Test void testSupports() { @@ -87,30 +87,31 @@ void testSupports() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) // 주석인 상태에서는 1, 두번째 트랜잭션만 얻음. 주석 해제하면 첫번째 트랜잭션만 얻음 + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithSupports"); } /** - * FirstUserService.saveFirstTransactionWithMandatory() 메서드를 보면 @Transactional이 주석으로 되어 있다. - * 주석인 상태에서 테스트를 실행했을 때와 주석을 해제하고 테스트를 실행했을 때 어떤 차이점이 있는지 확인해보자. - * SUPPORTS와 어떤 점이 다른지도 같이 챙겨보자. + * FirstUserService.saveFirstTransactionWithMandatory() 메서드를 보면 @Transactional이 주석으로 되어 있다. 주석인 상태에서 테스트를 실행했을 때와 + * 주석을 해제하고 테스트를 실행했을 때 어떤 차이점이 있는지 확인해보자. SUPPORTS와 어떤 점이 다른지도 같이 챙겨보자. */ @Test void testMandatory() { final var actual = firstUserService.saveFirstTransactionWithMandatory(); log.info("transactions : {}", actual); + // 주석 처리: No existing transaction found for transaction marked with propagation 'mandatory' + // 주석 해제: 트랜잭션이 의무이므로 기존 트랜잭션에 참여함. + // SUPPORTS는 상위 트랜잭션이 없어도 트랜잭션이 적용됐지만 MANDATORY는 상위 트랜잭션이 없으면 예외가 발생한다. assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithMandatory"); } /** - * 아래 테스트는 몇 개의 물리적 트랜잭션이 동작할까? - * FirstUserService.saveFirstTransactionWithNotSupported() 메서드의 @Transactional을 주석 처리하자. - * 다시 테스트를 실행하면 몇 개의 물리적 트랜잭션이 동작할까? - * + * 아래 테스트는 몇 개의 물리적 트랜잭션이 동작할까? FirstUserService.saveFirstTransactionWithNotSupported() 메서드의 @Transactional을 주석 + * 처리하자. 다시 테스트를 실행하면 몇 개의 물리적 트랜잭션이 동작할까? + *

* 스프링 공식 문서에서 물리적 트랜잭션과 논리적 트랜잭션의 차이점이 무엇인지 찾아보자. */ @Test @@ -118,23 +119,31 @@ void testNotSupported() { final var actual = firstUserService.saveFirstTransactionWithNotSupported(); log.info("transactions : {}", actual); + // 주석 처리: 두번째 트랜잭션만 얻음. -> 트랜잭션 없이 진행함 + // 주석 해제: 둘 다 얻음. -> 기존 트랜잭션을 보류시키고 트랜잭션 없이 진행함 assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(2) + .containsExactly( + "transaction.stage2.SecondUserService.saveSecondTransactionWithNotSupported", + "transaction.stage2.FirstUserService.saveFirstTransactionWithNotSupported" + ); } /** - * 아래 테스트는 왜 실패할까? - * FirstUserService.saveFirstTransactionWithNested() 메서드의 @Transactional을 주석 처리하면 어떻게 될까? + * 아래 테스트는 왜 실패할까? FirstUserService.saveFirstTransactionWithNested() 메서드의 @Transactional을 주석 처리하면 어떻게 될까? */ @Test void testNested() { final var actual = firstUserService.saveFirstTransactionWithNested(); + // 실패 예외: JpaDialect does not support savepoints - check your JPA provider's capabilities + // secondUserService.saveSecondTransactionWithNested(); 호출 시 발생 + // NESTED인 자식 트랜잭션이 부모 트랜잭션에 중첩되어 실행되는데, 중첩 실행 전 변경 사항에 대한 savepoint를 찾지 못해 발생하는 것 같다. + // 주석처리하면 자식 트랜잭션이 중첩되지 않고 새 트랜잭션을 생성하므로 발생하지 않는 것 같다. log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithNested"); } /** @@ -144,9 +153,12 @@ void testNested() { void testNever() { final var actual = firstUserService.saveFirstTransactionWithNever(); + // 주석 처리: 자식 트랜잭션 정보만 얻음 + // 주석 해제: Existing transaction found for transaction marked with propagation 'never' + // 트랜잭션을 사용하지 않는 속성으로, 기존 트랜잭션이 있으면 예외 발생 log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithNever"); } } From 3f8aa587d35321ec610904ae84e9dae6a45abc69 Mon Sep 17 00:00:00 2001 From: yoondgu Date: Thu, 5 Oct 2023 22:05:02 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20TransactionManager=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=B4=20DB=20=EC=95=A1=EC=84=B8=EC=8A=A4=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JdbcTemplate의 메서드는 외부에서 Connection을 전달받도록 함 - JdbcTemplate 다건, 단건 조회 메서드 통합하면서 단건 조회 검증로직 추가 - Dao의 메서드는 Connection을 전달받을 수도, 받지 않을 수도 있음 - Connection을 전달받으면 이를 통해 JdbcTemplate 메서드 실행 - Connection을 전달받지 않으면 TransactionManager를 통해 독립된 트랜잭션으로 실행 --- .../main/java/com/techcourse/dao/UserDao.java | 45 ++++++++--- .../com/techcourse/dao/UserHistoryDao.java | 26 ++++++- .../com/techcourse/service/UserService.java | 16 +++- app/src/main/resources/schema.sql | 3 + .../java/com/techcourse/dao/UserDaoTest.java | 8 +- .../service/MockUserHistoryDao.java | 8 +- .../techcourse/service/UserServiceTest.java | 22 +++--- .../jdbc/core/ConnectionCallback.java | 12 --- .../jdbc/core/JdbcTemplate.java | 78 +++++++++++++++---- .../jdbc/core/PreparedStatementCallback.java | 11 +++ .../jdbc/core/TransactionManager.java | 50 ++++++++++++ 11 files changed, 221 insertions(+), 58 deletions(-) delete mode 100644 jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java create mode 100644 jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java create mode 100644 jdbc/src/main/java/org/springframework/jdbc/core/TransactionManager.java diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 81f5db453b..b9eba697f6 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -1,9 +1,11 @@ package com.techcourse.dao; import com.techcourse.domain.User; +import java.sql.Connection; import java.util.List; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.TransactionManager; public class UserDao { @@ -14,24 +16,43 @@ public class UserDao { resultSet.getString(3), resultSet.getString(4) ); + + private final TransactionManager transactionManager; private final JdbcTemplate jdbcTemplate; - public UserDao(JdbcTemplate jdbcTemplate) { + public UserDao(final TransactionManager transactionManager, final JdbcTemplate jdbcTemplate) { + this.transactionManager = transactionManager; this.jdbcTemplate = jdbcTemplate; } public void insert(final User user) { final var sql = "insert into users (account, password, email) values (?, ?, ?)"; - jdbcTemplate.executeUpdate(sql, - user.getAccount(), - user.getPassword(), - user.getEmail() - ); + transactionManager.save( + (connection, entity) -> jdbcTemplate.executeUpdate(sql, + user.getAccount(), + user.getPassword(), + user.getEmail() + ), user); } public void update(final User user) { final var sql = "update users set account = ?, password = ?, email = ? where id = ?"; - jdbcTemplate.executeUpdate(sql, + transactionManager.save( + (connection, entity) -> jdbcTemplate.executeUpdate( + connection, + sql, + user.getAccount(), + user.getPassword(), + user.getEmail(), + user.getId() + ), user); + } + + public void update(final Connection connection, final User user) { + final var sql = "update users set account = ?, password = ?, email = ? where id = ?"; + jdbcTemplate.executeUpdate( + connection, + sql, user.getAccount(), user.getPassword(), user.getEmail(), @@ -41,16 +62,20 @@ public void update(final User user) { public List findAll() { final var sql = "select id, account, password, email from users"; - return jdbcTemplate.executeQueryForList(sql, USER_ROW_MAPPER); + return transactionManager.find((connection, parameters) -> + jdbcTemplate.executeQueryForList(connection, sql, USER_ROW_MAPPER)); } public User findById(final Long id) { final var sql = "select id, account, password, email from users where id = ?"; - return jdbcTemplate.executeQueryForObject(sql, USER_ROW_MAPPER, id); + return transactionManager.find( + (connection, parameters) -> + jdbcTemplate.executeQueryForObject(connection, sql, USER_ROW_MAPPER, parameters), id); } public User findByAccount(final String account) { final var sql = "select id, account, password, email from users where account = ?"; - return jdbcTemplate.executeQueryForObject(sql, USER_ROW_MAPPER, account); + return transactionManager.find((connection, parameters) -> + jdbcTemplate.executeQueryForObject(connection, sql, USER_ROW_MAPPER, parameters), account); } } diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index 6efe3b7cf3..3d91e61d1a 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -1,24 +1,46 @@ package com.techcourse.dao; import com.techcourse.domain.UserHistory; +import java.sql.Connection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.TransactionManager; public class UserHistoryDao { private static final Logger log = LoggerFactory.getLogger(UserHistoryDao.class); + private final TransactionManager transactionManager; private final JdbcTemplate jdbcTemplate; - public UserHistoryDao(JdbcTemplate jdbcTemplate) { + public UserHistoryDao(final TransactionManager transactionManager, final JdbcTemplate jdbcTemplate) { + this.transactionManager = transactionManager; this.jdbcTemplate = jdbcTemplate; } public void log(final UserHistory userHistory) { final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; log.info("query: {}", sql); - jdbcTemplate.executeUpdate(sql, + transactionManager.save( + (connection, entity) -> jdbcTemplate.executeUpdate( + connection, + sql, + userHistory.getUserId(), + userHistory.getAccount(), + userHistory.getPassword(), + userHistory.getEmail(), + userHistory.getCreatedAt(), + userHistory.getCreateBy() + ), userHistory); + } + + public void log(final Connection connection, final UserHistory userHistory) { + final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; + log.info("query: {}", sql); + jdbcTemplate.executeUpdate( + connection, + sql, userHistory.getUserId(), userHistory.getAccount(), userHistory.getPassword(), diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index fcf2159dc8..9c39c1f476 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -4,13 +4,19 @@ import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; import com.techcourse.domain.UserHistory; +import org.springframework.jdbc.core.TransactionManager; public class UserService { + private final TransactionManager transactionManager; private final UserDao userDao; private final UserHistoryDao userHistoryDao; - public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { + public UserService( + final TransactionManager transactionManager, + final UserDao userDao, + final UserHistoryDao userHistoryDao) { + this.transactionManager = transactionManager; this.userDao = userDao; this.userHistoryDao = userHistoryDao; } @@ -26,7 +32,11 @@ public void insert(final User user) { public void changePassword(final long id, final String newPassword, final String createBy) { final var user = findById(id); user.changePassword(newPassword); - userDao.update(user); - userHistoryDao.log(new UserHistory(user, createBy)); + transactionManager.save( + (connection, entity) -> { + userDao.update(connection, entity); + userHistoryDao.log(connection, new UserHistory(entity, createBy)); + }, + user); } } diff --git a/app/src/main/resources/schema.sql b/app/src/main/resources/schema.sql index ba235931f1..c099e4888f 100644 --- a/app/src/main/resources/schema.sql +++ b/app/src/main/resources/schema.sql @@ -1,3 +1,6 @@ +drop table if exists users; +drop table if exists user_history; + create table if not exists users ( id bigint auto_increment, account varchar(100) not null, diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index 70f4645a68..738c7d5cbc 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -4,21 +4,25 @@ import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.User; -import com.techcourse.support.jdbc.init.PooledDataSourceConnectionManager; import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; +import com.techcourse.support.jdbc.init.PooledDataSourceConnectionManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.ConnectionManager; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.TransactionManager; class UserDaoTest { private UserDao userDao; + private ConnectionManager connectionManager; @BeforeEach void setup() { DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); - userDao = new UserDao(new JdbcTemplate(new PooledDataSourceConnectionManager())); + connectionManager = new PooledDataSourceConnectionManager(); + userDao = new UserDao(new TransactionManager(connectionManager), new JdbcTemplate(connectionManager)); final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao.insert(user); } diff --git a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java index 2ee12b195f..6c049c95d9 100644 --- a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java +++ b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java @@ -2,17 +2,19 @@ import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.UserHistory; +import java.sql.Connection; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.TransactionManager; public class MockUserHistoryDao extends UserHistoryDao { - public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) { - super(jdbcTemplate); + public MockUserHistoryDao(final TransactionManager transactionManager, final JdbcTemplate jdbcTemplate) { + super(transactionManager, jdbcTemplate); } @Override - public void log(final UserHistory userHistory) { + public void log(final Connection connection, final UserHistory userHistory) { throw new DataAccessException(); } } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index f320478955..5c7973fd98 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -7,24 +7,28 @@ import com.techcourse.dao.UserDao; import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; -import com.techcourse.support.jdbc.init.PooledDataSourceConnectionManager; import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; +import com.techcourse.support.jdbc.init.PooledDataSourceConnectionManager; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.ConnectionManager; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.TransactionManager; -@Disabled class UserServiceTest { + private ConnectionManager connectionManager; + private TransactionManager transactionManager; private JdbcTemplate jdbcTemplate; private UserDao userDao; @BeforeEach void setUp() { - this.jdbcTemplate = new JdbcTemplate(new PooledDataSourceConnectionManager()); - this.userDao = new UserDao(jdbcTemplate); + this.connectionManager = new PooledDataSourceConnectionManager(); + this.transactionManager = new TransactionManager(connectionManager); + this.jdbcTemplate = new JdbcTemplate(connectionManager); + this.userDao = new UserDao(new TransactionManager(connectionManager), jdbcTemplate); DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); final var user = new User("gugu", "password", "hkkang@woowahan.com"); @@ -33,8 +37,8 @@ void setUp() { @Test void testChangePassword() { - final var userHistoryDao = new UserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + final var userHistoryDao = new UserHistoryDao(new TransactionManager(connectionManager), jdbcTemplate); + final var userService = new UserService(transactionManager, userDao, userHistoryDao); final var newPassword = "qqqqq"; final var createBy = "gugu"; @@ -48,8 +52,8 @@ void testChangePassword() { @Test void testTransactionRollback() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 - final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + final var userHistoryDao = new MockUserHistoryDao(new TransactionManager(connectionManager), jdbcTemplate); + final var userService = new UserService(transactionManager, userDao, userHistoryDao); final var newPassword = "newPassword"; final var createBy = "gugu"; diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java b/jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java deleted file mode 100644 index bac8375457..0000000000 --- a/jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.springframework.jdbc.core; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; - -@FunctionalInterface -public interface ConnectionCallback { - - T doInConnection(Connection connection, PreparedStatement preparedStatement) throws SQLException; - -} diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index b6f5bc8cf2..68a23cc5f1 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -8,6 +8,7 @@ import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.dao.DataAccessException; import org.springframework.jdbc.SqlQueryException; public class JdbcTemplate { @@ -16,12 +17,22 @@ public class JdbcTemplate { private final ConnectionManager connectionManager; + // TODO Connection은 모두 외부에서 받게 한 뒤 ConnectionManager 제거 public JdbcTemplate(final ConnectionManager connectionManager) { this.connectionManager = connectionManager; } public int executeUpdate(final String query, final Object... parameters) { - return execute(query, (connection, preparedStatement) -> preparedStatement.executeUpdate(), parameters); + try (final Connection connection = connectionManager.getConnection()) { + return executeUpdate(connection, query, parameters); + } catch (SQLException exception) { + throw new DataAccessException(exception); + } + } + + public int executeUpdate(final Connection connection, final String query, final Object... parameters) { + PreparedStatementCallback preparedStatementCallback = PreparedStatement::executeUpdate; + return execute(connection, query, preparedStatementCallback, parameters); } public T executeQueryForObject( @@ -29,17 +40,45 @@ public T executeQueryForObject( final RowMapper rowMapper, final Object... parameters ) { - final ResultSetExtractor resultSetExtractor = resultSet -> { - if (resultSet.next()) { - return rowMapper.mapRow(resultSet); - } + try (final Connection connection = connectionManager.getConnection()) { + return executeQueryForObject(connection, query, rowMapper, parameters); + } catch (SQLException exception) { + throw new DataAccessException(exception); + } + } + + public T executeQueryForObject( + final Connection connection, + final String query, + final RowMapper rowMapper, + final Object... parameters + ) { + final var results = executeQueryForList(connection, query, rowMapper, parameters); + if (results.isEmpty()) { return null; - }; + } + if (results.size() > 1) { + throw new SqlQueryException(query, "cannot map for single result"); + } + return results.get(0); + } - return executeQuery(query, resultSetExtractor, parameters); + + // TODO 불필요한 오버로딩 삭제 + public List executeQueryForList( + final String query, + final RowMapper rowMapper, + final Object... parameters + ) { + try (final Connection connection = connectionManager.getConnection()) { + return executeQueryForList(connection, query, rowMapper, parameters); + } catch (SQLException exception) { + throw new DataAccessException(exception); + } } public List executeQueryForList( + final Connection connection, final String query, final RowMapper rowMapper, final Object... parameters @@ -52,31 +91,36 @@ public List executeQueryForList( return results; }; - return executeQuery(query, resultSetExtractor, parameters); + return executeQuery(connection, query, resultSetExtractor, parameters); } public T executeQuery( + final Connection connection, final String query, final ResultSetExtractor resultSetExtractor, final Object... parameters ) { - return execute(query, (connection, preparedStatement) -> { - try (final ResultSet resultSet = preparedStatement.executeQuery()) { - return resultSetExtractor.extract(resultSet); - } - }, parameters); + return execute( + connection, + query, + preparedStatement -> { + try (final ResultSet resultSet = preparedStatement.executeQuery()) { + return resultSetExtractor.extract(resultSet); + } + }, parameters); } private T execute( + final Connection connection, final String query, - final ConnectionCallback callback, + final PreparedStatementCallback callback, final Object... parameters ) { - try (final Connection connection = connectionManager.getConnection(); - final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) { log.info("query: {}", query); setParameters(preparedStatement, parameters); - return callback.doInConnection(connection, preparedStatement); + + return callback.doInConnection(preparedStatement); } catch (SQLException exception) { throw new SqlQueryException(exception.getMessage(), query); } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java b/jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java new file mode 100644 index 0000000000..1fb1fb6282 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java @@ -0,0 +1,11 @@ +package org.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@FunctionalInterface +public interface PreparedStatementCallback { + + T doInConnection(final PreparedStatement preparedStatement) throws SQLException; + +} diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/TransactionManager.java b/jdbc/src/main/java/org/springframework/jdbc/core/TransactionManager.java new file mode 100644 index 0000000000..160b1521a2 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/jdbc/core/TransactionManager.java @@ -0,0 +1,50 @@ +package org.springframework.jdbc.core; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import org.springframework.dao.DataAccessException; + +public class TransactionManager { + + private final ConnectionManager connectionManager; + + public TransactionManager(final ConnectionManager connectionManager) { + this.connectionManager = connectionManager; + } + + public T save(final BiConsumer consumer, final T entity) { + final var connection = connectionManager.getConnection(); + try (connection) { + connection.setAutoCommit(false); + consumer.accept(connection, entity); + connection.commit(); + return entity; + } catch (Exception exception) { + try { + connection.rollback(); + throw new DataAccessException(exception); + } catch (SQLException sqlException) { + throw new DataAccessException(sqlException); + } + } + } + + public T find(final BiFunction function, final Object... parameters) { + final var connection = connectionManager.getConnection(); + try (connection) { + connection.setAutoCommit(false); + final var result = function.apply(connection, parameters); + connection.commit(); + return result; + } catch (Exception exception) { + try { + connection.rollback(); + throw new DataAccessException(exception); + } catch (SQLException sqlException) { + throw new DataAccessException(sqlException); + } + } + } +} From e6fe8a2a554f10a1ae88efdca097fac4a91614f9 Mon Sep 17 00:00:00 2001 From: yoondgu Date: Thu, 5 Oct 2023 22:12:06 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20JdbcTemplate=EC=97=90=EC=84=9C=20Co?= =?UTF-8?q?nnectionManager=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/techcourse/dao/UserDao.java | 4 +- .../java/com/techcourse/dao/UserDaoTest.java | 6 +-- .../techcourse/service/UserServiceTest.java | 13 +++--- .../jdbc/core/JdbcTemplate.java | 42 ------------------- .../java/nextstep/jdbc/JdbcTemplateTest.java | 17 ++++++-- 5 files changed, 22 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index b9eba697f6..a7dc6e08a3 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -28,7 +28,9 @@ public UserDao(final TransactionManager transactionManager, final JdbcTemplate j public void insert(final User user) { final var sql = "insert into users (account, password, email) values (?, ?, ?)"; transactionManager.save( - (connection, entity) -> jdbcTemplate.executeUpdate(sql, + (connection, entity) -> jdbcTemplate.executeUpdate( + connection, + sql, user.getAccount(), user.getPassword(), user.getEmail() diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index 738c7d5cbc..0b1a7da1fe 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -8,21 +8,17 @@ import com.techcourse.support.jdbc.init.PooledDataSourceConnectionManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.jdbc.core.ConnectionManager; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.TransactionManager; class UserDaoTest { private UserDao userDao; - private ConnectionManager connectionManager; @BeforeEach void setup() { DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); - - connectionManager = new PooledDataSourceConnectionManager(); - userDao = new UserDao(new TransactionManager(connectionManager), new JdbcTemplate(connectionManager)); + userDao = new UserDao(new TransactionManager(new PooledDataSourceConnectionManager()), new JdbcTemplate()); final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao.insert(user); } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index 5c7973fd98..2fc41d2dab 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -12,23 +12,20 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.core.ConnectionManager; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.TransactionManager; class UserServiceTest { - private ConnectionManager connectionManager; private TransactionManager transactionManager; private JdbcTemplate jdbcTemplate; private UserDao userDao; @BeforeEach void setUp() { - this.connectionManager = new PooledDataSourceConnectionManager(); - this.transactionManager = new TransactionManager(connectionManager); - this.jdbcTemplate = new JdbcTemplate(connectionManager); - this.userDao = new UserDao(new TransactionManager(connectionManager), jdbcTemplate); + this.transactionManager = new TransactionManager(new PooledDataSourceConnectionManager()); + this.jdbcTemplate = new JdbcTemplate(); + this.userDao = new UserDao(transactionManager, jdbcTemplate); DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); final var user = new User("gugu", "password", "hkkang@woowahan.com"); @@ -37,7 +34,7 @@ void setUp() { @Test void testChangePassword() { - final var userHistoryDao = new UserHistoryDao(new TransactionManager(connectionManager), jdbcTemplate); + final var userHistoryDao = new UserHistoryDao(transactionManager, jdbcTemplate); final var userService = new UserService(transactionManager, userDao, userHistoryDao); final var newPassword = "qqqqq"; @@ -52,7 +49,7 @@ void testChangePassword() { @Test void testTransactionRollback() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 - final var userHistoryDao = new MockUserHistoryDao(new TransactionManager(connectionManager), jdbcTemplate); + final var userHistoryDao = new MockUserHistoryDao(transactionManager, jdbcTemplate); final var userService = new UserService(transactionManager, userDao, userHistoryDao); final var newPassword = "newPassword"; diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 68a23cc5f1..2a3a3a32b9 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -8,45 +8,17 @@ import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.dao.DataAccessException; import org.springframework.jdbc.SqlQueryException; public class JdbcTemplate { private static final Logger log = LoggerFactory.getLogger(JdbcTemplate.class); - private final ConnectionManager connectionManager; - - // TODO Connection은 모두 외부에서 받게 한 뒤 ConnectionManager 제거 - public JdbcTemplate(final ConnectionManager connectionManager) { - this.connectionManager = connectionManager; - } - - public int executeUpdate(final String query, final Object... parameters) { - try (final Connection connection = connectionManager.getConnection()) { - return executeUpdate(connection, query, parameters); - } catch (SQLException exception) { - throw new DataAccessException(exception); - } - } - public int executeUpdate(final Connection connection, final String query, final Object... parameters) { PreparedStatementCallback preparedStatementCallback = PreparedStatement::executeUpdate; return execute(connection, query, preparedStatementCallback, parameters); } - public T executeQueryForObject( - final String query, - final RowMapper rowMapper, - final Object... parameters - ) { - try (final Connection connection = connectionManager.getConnection()) { - return executeQueryForObject(connection, query, rowMapper, parameters); - } catch (SQLException exception) { - throw new DataAccessException(exception); - } - } - public T executeQueryForObject( final Connection connection, final String query, @@ -63,20 +35,6 @@ public T executeQueryForObject( return results.get(0); } - - // TODO 불필요한 오버로딩 삭제 - public List executeQueryForList( - final String query, - final RowMapper rowMapper, - final Object... parameters - ) { - try (final Connection connection = connectionManager.getConnection()) { - return executeQueryForList(connection, query, rowMapper, parameters); - } catch (SQLException exception) { - throw new DataAccessException(exception); - } - } - public List executeQueryForList( final Connection connection, final String query, diff --git a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java index d7ecce16c1..470007d3ea 100644 --- a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java +++ b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java @@ -17,12 +17,13 @@ class JdbcTemplateTest { + private ConnectionManager connectionManager; private JdbcTemplate jdbcTemplate; @BeforeEach void setUp() { - TestConnectionManager connectionManager = new TestConnectionManager(); - this.jdbcTemplate = new JdbcTemplate(connectionManager); + this.connectionManager = new TestConnectionManager(); + this.jdbcTemplate = new JdbcTemplate(); try (Connection conn = connectionManager.getConnection()) { conn.setAutoCommit(true); try (Statement stmt = conn.createStatement()) { @@ -43,7 +44,10 @@ void executeUpdate() { // given failIfExceptionThrownBy(() -> { // when - final var updated = jdbcTemplate.executeUpdate("insert into users (email) values (?)", "doy@gmail.com"); + final var updated = jdbcTemplate.executeUpdate( + connectionManager.getConnection(), + "insert into users (email) values (?)", "doy@gmail.com" + ); // then assertThat(updated).isOne(); @@ -57,6 +61,7 @@ void executeQueryForObject() { failIfExceptionThrownBy(() -> { // when User user = jdbcTemplate.executeQueryForObject( + connectionManager.getConnection(), "select email from users where id = ?", resultSet -> new User(resultSet.getString(1)), 1L ); @@ -71,11 +76,15 @@ void executeQueryForObject() { @DisplayName("다건 레코드를 조회하는 쿼리를 실행한다.") void executeQueryForList() { // given - jdbcTemplate.executeUpdate("insert into users (email) values (?)", "doy@gmail.com"); + jdbcTemplate.executeUpdate( + connectionManager.getConnection(), + "insert into users (email) values (?)", "doy@gmail.com" + ); failIfExceptionThrownBy(() -> { // when List users = jdbcTemplate.executeQueryForList( + connectionManager.getConnection(), "select email from users", resultSet -> new User(resultSet.getString(1)) ); From c37ab310b061a275e30b88b3fbe23f7d68c4fe7f Mon Sep 17 00:00:00 2001 From: yoondgu Date: Fri, 6 Oct 2023 13:48:58 +0900 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20Connection=20AutoCommit?= =?UTF-8?q?=EC=9D=98=20=EA=B8=B0=EB=B3=B8=EC=9D=80=20true=EC=9D=B4?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/springframework/jdbc/core/TransactionManager.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/TransactionManager.java b/jdbc/src/main/java/org/springframework/jdbc/core/TransactionManager.java index 160b1521a2..593eda857f 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/TransactionManager.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/TransactionManager.java @@ -20,6 +20,7 @@ public T save(final BiConsumer consumer, final T entity) { connection.setAutoCommit(false); consumer.accept(connection, entity); connection.commit(); + connection.setAutoCommit(true); return entity; } catch (Exception exception) { try { @@ -37,6 +38,7 @@ public T find(final BiFunction function, final Obje connection.setAutoCommit(false); final var result = function.apply(connection, parameters); connection.commit(); + connection.setAutoCommit(true); return result; } catch (Exception exception) { try { From fcc4d7aa8d11f4ebdafe92ced56cd4c601375ef8 Mon Sep 17 00:00:00 2001 From: yoondgu Date: Fri, 6 Oct 2023 14:00:43 +0900 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20setAutoCommit=20true=20finally?= =?UTF-8?q?=20=EB=B8=94=EB=A1=9D=EC=9C=BC=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jdbc/core/TransactionManager.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/TransactionManager.java b/jdbc/src/main/java/org/springframework/jdbc/core/TransactionManager.java index 593eda857f..8dc82d24e0 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/TransactionManager.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/TransactionManager.java @@ -2,12 +2,16 @@ import java.sql.Connection; import java.sql.SQLException; +import java.util.Arrays; import java.util.function.BiConsumer; import java.util.function.BiFunction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; public class TransactionManager { + private final Logger log = LoggerFactory.getLogger(TransactionManager.class); private final ConnectionManager connectionManager; public TransactionManager(final ConnectionManager connectionManager) { @@ -20,7 +24,6 @@ public T save(final BiConsumer consumer, final T entity) { connection.setAutoCommit(false); consumer.accept(connection, entity); connection.commit(); - connection.setAutoCommit(true); return entity; } catch (Exception exception) { try { @@ -29,6 +32,13 @@ public T save(final BiConsumer consumer, final T entity) { } catch (SQLException sqlException) { throw new DataAccessException(sqlException); } + } finally { + try { + connection.setAutoCommit(true); + } catch (SQLException ignored) { + log.warn("fail to set auto commit true due to {}", ignored.getMessage()); + log.warn(Arrays.toString(ignored.getStackTrace())); + } } } @@ -38,7 +48,6 @@ public T find(final BiFunction function, final Obje connection.setAutoCommit(false); final var result = function.apply(connection, parameters); connection.commit(); - connection.setAutoCommit(true); return result; } catch (Exception exception) { try { @@ -47,6 +56,13 @@ public T find(final BiFunction function, final Obje } catch (SQLException sqlException) { throw new DataAccessException(sqlException); } + } finally { + try { + connection.setAutoCommit(true); + } catch (SQLException ignored) { + log.warn("fail to set auto commit true due to {}", ignored.getMessage()); + log.warn(Arrays.toString(ignored.getStackTrace())); + } } } } From 4c33d88e25d776d52cdfe1ef88455a863cbc334a Mon Sep 17 00:00:00 2001 From: yoondgu Date: Fri, 6 Oct 2023 14:32:28 +0900 Subject: [PATCH 6/6] =?UTF-8?q?refactor:=20TransactionManager=EB=A5=BC=20?= =?UTF-8?q?=EA=B1=B0=EC=B9=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=EB=A5=BC=20=EA=B3=A0=EB=A0=A4=ED=95=B4=EC=84=9C=20con?= =?UTF-8?q?nection=20close?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/springframework/jdbc/core/JdbcTemplate.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 2a3a3a32b9..acdc6eed8f 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -74,7 +74,8 @@ private T execute( final PreparedStatementCallback callback, final Object... parameters ) { - try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + try (final Connection conn = connection; + final PreparedStatement preparedStatement = conn.prepareStatement(query)) { log.info("query: {}", query); setParameters(preparedStatement, parameters);