From 38a876a39ef10f19267a874bd57b5354b75641c7 Mon Sep 17 00:00:00 2001 From: This2sho Date: Sun, 8 Oct 2023 21:55:52 +0900 Subject: [PATCH 01/14] =?UTF-8?q?study:=20=ED=95=99=EC=8A=B5=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/transaction/stage1/Stage1Test.java | 25 ++++++++------ .../transaction/stage2/FirstUserService.java | 8 ++--- .../java/transaction/stage2/Stage2Test.java | 34 ++++++++++--------- 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/study/src/test/java/transaction/stage1/Stage1Test.java b/study/src/test/java/transaction/stage1/Stage1Test.java index 8c29944d4e..779d701b60 100644 --- a/study/src/test/java/transaction/stage1/Stage1Test.java +++ b/study/src/test/java/transaction/stage1/Stage1Test.java @@ -58,10 +58,10 @@ private void setUp(final DataSource dataSource) { * Read phenomena | Dirty reads * Isolation level | * -----------------|------------- - * Read Uncommitted | - * Read Committed | - * Repeatable Read | - * Serializable | + * Read Uncommitted | + + * Read Committed | - + * Repeatable Read | - + * Serializable | - */ @Test void dirtyReading() throws SQLException { @@ -81,7 +81,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); @@ -111,10 +111,10 @@ void dirtyReading() throws SQLException { * Read phenomena | Non-repeatable reads * Isolation level | * -----------------|--------------------- - * Read Uncommitted | - * Read Committed | - * Repeatable Read | - * Serializable | + * Read Uncommitted | + + * Read Committed | + + * Repeatable Read | - + * Serializable | - */ @Test void noneRepeatable() throws SQLException { @@ -130,7 +130,7 @@ void noneRepeatable() throws SQLException { connection.setAutoCommit(false); // 적절한 격리 레벨을 찾는다. - final int isolationLevel = Connection.TRANSACTION_NONE; + final int isolationLevel = Connection.TRANSACTION_REPEATABLE_READ; // 트랜잭션 격리 레벨을 설정한다. connection.setTransactionIsolation(isolationLevel); @@ -149,6 +149,9 @@ void noneRepeatable() throws SQLException { // ❗사용자B가 gugu 객체의 비밀번호를 변경했다.(subConnection은 auto commit 상태) anotherUser.changePassword("qqqq"); userDao.update(subConnection, anotherUser); + final var anotherUser2 = userDao.findByAccount(subConnection, "gugu"); + log.info("anotherUser2 : {}", anotherUser2); + })).start(); sleep(0.5); @@ -197,7 +200,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 9a1d415c18..38e38521b8 100644 --- a/study/src/test/java/transaction/stage2/FirstUserService.java +++ b/study/src/test/java/transaction/stage2/FirstUserService.java @@ -66,7 +66,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()); @@ -77,7 +77,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()); @@ -99,7 +99,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()); @@ -110,7 +110,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 e522bf4365..2b4831150a 100644 --- a/study/src/test/java/transaction/stage2/Stage2Test.java +++ b/study/src/test/java/transaction/stage2/Stage2Test.java @@ -1,5 +1,6 @@ package transaction.stage2; +import java.util.Set; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -45,8 +46,8 @@ void testRequired() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithRequired"); } /** @@ -59,8 +60,9 @@ void testRequiredNew() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(2) + .contains("transaction.stage2.FirstUserService.saveFirstTransactionWithRequiredNew", + "transaction.stage2.SecondUserService.saveSecondTransactionWithRequiresNew"); } /** @@ -69,12 +71,12 @@ void testRequiredNew() { */ @Test void testRequiredNewWithRollback() { - assertThat(firstUserService.findAll()).hasSize(-1); + assertThat(firstUserService.findAll()).hasSize(0); assertThatThrownBy(() -> firstUserService.saveAndExceptionWithRequiredNew()) .isInstanceOf(RuntimeException.class); - assertThat(firstUserService.findAll()).hasSize(-1); + assertThat(firstUserService.findAll()).hasSize(1); } /** @@ -87,8 +89,8 @@ void testSupports() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithSupports"); } /** @@ -102,8 +104,8 @@ void testMandatory() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithMandatory"); } /** @@ -119,8 +121,9 @@ void testNotSupported() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(2) + .contains("transaction.stage2.FirstUserService.saveFirstTransactionWithNotSupported", + "transaction.stage2.SecondUserService.saveSecondTransactionWithNotSupported"); } /** @@ -133,8 +136,8 @@ void testNested() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .contains("transaction.stage2.SecondUserService.saveSecondTransactionWithNested"); } /** @@ -146,7 +149,6 @@ void testNever() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1); } } From 0d1486781da71e6367edeabc2c4bb4285997deb5 Mon Sep 17 00:00:00 2001 From: This2sho Date: Sun, 8 Oct 2023 21:56:10 +0900 Subject: [PATCH 02/14] =?UTF-8?q?docs:=203=EB=8B=A8=EA=B3=84=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EA=B8=B0=EB=8A=A5=20=EB=AA=A9=EB=A1=9D=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index d22a76c970..e81d1ba31a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,5 +8,13 @@ - [x] 예외처리 코드 작성 - [x] 쿼리 조회 결과가 2개 이상이거나 없을 경우 예외 처리 - [x] RuntimeException 대신 구체적 예외 처리 - - [x] JDBC 탬플릿의 중복된 try catch문 제거 + +## 3단계 +- [ ] 매튜 리뷰 반영 + - [ ] JdbcTemplate 변수 네이밍 수정 + - [ ] 사용하지 않는 log 제거 + +- [ ] User에 비밀번호 변경 기능 구현 + - [ ] 변경 이력 저장 + - [ ] 트랜잭션 설정 From 3ebcdd68adc5aecfe6fb8a954893d5d2d4a15ada Mon Sep 17 00:00:00 2001 From: This2sho Date: Sun, 8 Oct 2023 22:01:43 +0900 Subject: [PATCH 03/14] =?UTF-8?q?refactor:=20=EB=A7=A4=ED=8A=9C=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 변수 네이밍 명확하게 수정 - 사용하지 않는 log 제거 --- docs/README.md | 6 ++-- .../jdbc/core/JdbcTemplate.java | 36 +++++++++---------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/docs/README.md b/docs/README.md index e81d1ba31a..32d5afa502 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,9 +11,9 @@ - [x] JDBC 탬플릿의 중복된 try catch문 제거 ## 3단계 -- [ ] 매튜 리뷰 반영 - - [ ] JdbcTemplate 변수 네이밍 수정 - - [ ] 사용하지 않는 log 제거 +- [x] 매튜 리뷰 반영 + - [x] JdbcTemplate 변수 네이밍 명확하게 수정 + - [x] 사용하지 않는 log 제거 - [ ] User에 비밀번호 변경 기능 구현 - [ ] 변경 이력 저장 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 876f1c5fb6..0dadda43b0 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -5,14 +5,10 @@ import java.util.ArrayList; import java.util.List; import javax.sql.DataSource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; public class JdbcTemplate { - private static final Logger log = LoggerFactory.getLogger(JdbcTemplate.class); - private final DataSource dataSource; public JdbcTemplate(final DataSource dataSource) { @@ -20,17 +16,17 @@ public JdbcTemplate(final DataSource dataSource) { } public void update(final String sql, final Object... parameters) { - execute(new PreparedStatementCreator(sql), ps -> { - final var pss = newArgumentPreparedStatementSetter(parameters); - pss.setValues(ps); - return ps.executeUpdate(); + execute(new PreparedStatementCreator(sql), statement -> { + final var statementSetter = newArgumentPreparedStatementSetter(parameters); + statementSetter.setValues(statement); + return statement.executeUpdate(); }); } - public T execute(PreparedStatementCreator psc, StatementCallback action) { - try (var conn = dataSource.getConnection(); - var stmt = psc.createPreparedStatement(conn)) { - return action.doInStatement(stmt); + public T execute(PreparedStatementCreator statementCreator, StatementCallback action) { + try (var connection = dataSource.getConnection(); + var statement = statementCreator.createPreparedStatement(connection)) { + return action.doInStatement(statement); } catch (SQLException e) { throw new DataAccessException(e); } @@ -49,18 +45,18 @@ public T queryForObject(final String sql, final RowMapper rowMapper, fina } public List query(final String sql, final RowMapper rowMapper, final Object... parameters) { - return execute(new PreparedStatementCreator(sql), ps -> { - final var pss = new ArgumentPreparedStatementSetter(parameters); - pss.setValues(ps); - final var rs = ps.executeQuery(); - return result(rowMapper, rs); + return execute(new PreparedStatementCreator(sql), statement -> { + final var statementSetter = new ArgumentPreparedStatementSetter(parameters); + statementSetter.setValues(statement); + final var resultSet = statement.executeQuery(); + return result(rowMapper, resultSet); }); } - private List result(final RowMapper rowMapper, final ResultSet rs) throws SQLException { + private List result(final RowMapper rowMapper, final ResultSet resultSet) throws SQLException { List result = new ArrayList<>(); - while (rs.next()) { - result.add(rowMapper.mapRow(rs)); + while (resultSet.next()) { + result.add(rowMapper.mapRow(resultSet)); } return result; } From 9948c6ce72595e361a4a3cd166fb469e9c6bc012 Mon Sep 17 00:00:00 2001 From: This2sho Date: Sun, 8 Oct 2023 22:15:52 +0900 Subject: [PATCH 04/14] =?UTF-8?q?refactor:=20ResultSet=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=BB=AC=EB=9F=BC=EB=AA=85=EC=9C=BC=EB=A1=9C=20=EB=B0=9B?= =?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 --- app/src/main/java/com/techcourse/dao/UserDao.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 8c0be3b9a5..7b95254ef7 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -3,17 +3,16 @@ import com.techcourse.domain.User; import java.util.List; import javax.sql.DataSource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; public class UserDao { - private static final Logger log = LoggerFactory.getLogger(UserDao.class); - - private static final RowMapper USER_ROW_MAPPER = rs -> new User(rs.getLong(1), rs.getString(2), - rs.getString(3), rs.getString(4)); + private static final RowMapper USER_ROW_MAPPER = rs -> new User( + rs.getLong("id"), + rs.getString("account"), + rs.getString("password"), + rs.getString("email")); private final JdbcTemplate jdbcTemplate; @@ -27,18 +26,16 @@ public UserDao(final JdbcTemplate jdbcTemplate) { public void insert(final User user) { final var sql = "insert into users (account, password, email) values (?, ?, ?)"; - jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail()); } public void update(final User user) { - final var sql = "update users set account = ?, password = ?, email = ? where id = ?"; + final var sql = "update users set (account, password, email) = (?, ?, ?) where id = ?"; jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); } public List findAll() { final var sql = "select * from users"; - return jdbcTemplate.query(sql, USER_ROW_MAPPER); } From ac565a05108ef7c97f1e6082b0ac53c5fe36e1a8 Mon Sep 17 00:00:00 2001 From: This2sho Date: Sun, 8 Oct 2023 22:16:22 +0900 Subject: [PATCH 05/14] =?UTF-8?q?refactor:=20UserHistoryDao=20JdbcTemplate?= =?UTF-8?q?=20=EC=93=B0=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/techcourse/dao/UserHistoryDao.java | 49 +++---------------- 1 file changed, 6 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index edb4338caa..a0500686d0 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -1,62 +1,25 @@ package com.techcourse.dao; import com.techcourse.domain.UserHistory; -import org.springframework.jdbc.core.JdbcTemplate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; +import org.springframework.jdbc.core.JdbcTemplate; public class UserHistoryDao { - private static final Logger log = LoggerFactory.getLogger(UserHistoryDao.class); - - private final DataSource dataSource; + private final JdbcTemplate jdbcTemplate; public UserHistoryDao(final DataSource dataSource) { - this.dataSource = dataSource; + this.jdbcTemplate = new JdbcTemplate(dataSource); } public UserHistoryDao(final JdbcTemplate jdbcTemplate) { - this.dataSource = null; + 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 (?, ?, ?, ?, ?, ?)"; - Connection conn = null; - PreparedStatement pstmt = null; - try { - conn = dataSource.getConnection(); - pstmt = conn.prepareStatement(sql); - - log.debug("query : {}", sql); - - pstmt.setLong(1, userHistory.getUserId()); - pstmt.setString(2, userHistory.getAccount()); - pstmt.setString(3, userHistory.getPassword()); - pstmt.setString(4, userHistory.getEmail()); - pstmt.setObject(5, userHistory.getCreatedAt()); - pstmt.setString(6, userHistory.getCreateBy()); - pstmt.executeUpdate(); - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new RuntimeException(e); - } finally { - try { - if (pstmt != null) { - pstmt.close(); - } - } catch (SQLException ignored) {} - - try { - if (conn != null) { - conn.close(); - } - } catch (SQLException ignored) {} - } + jdbcTemplate.update(sql, userHistory.getUserId(), userHistory.getAccount(), userHistory.getPassword(), + userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy()); } } From 29dffb381ca1b904a84a681a796c7b65b6b8797a Mon Sep 17 00:00:00 2001 From: This2sho Date: Sun, 8 Oct 2023 23:07:52 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20UserHistoryDao=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EC=9D=B4=EB=A0=A5=20=ED=99=95=EC=9D=B8?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/techcourse/dao/UserHistoryDao.java | 15 +++++++++ .../com/techcourse/service/UserService.java | 5 +++ .../techcourse/service/UserServiceTest.java | 33 ++++++++++++++++--- docs/README.md | 4 +-- 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index a0500686d0..d127da39c9 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -1,11 +1,21 @@ package com.techcourse.dao; import com.techcourse.domain.UserHistory; +import java.util.List; import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; public class UserHistoryDao { + private static final RowMapper USER_HISTORY_ROW_MAPPER = rs -> new UserHistory( + rs.getLong("id"), + rs.getLong("user_id"), + rs.getString("account"), + rs.getString("password"), + rs.getString("email"), + rs.getString("created_by")); + private final JdbcTemplate jdbcTemplate; public UserHistoryDao(final DataSource dataSource) { @@ -22,4 +32,9 @@ public void log(final UserHistory userHistory) { jdbcTemplate.update(sql, userHistory.getUserId(), userHistory.getAccount(), userHistory.getPassword(), userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy()); } + + public List findByUserId(final Long userId) { + final var sql = "select * from user_history where user_id = ?"; + return jdbcTemplate.query(sql, USER_HISTORY_ROW_MAPPER, userId); + } } diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index fcf2159dc8..cc1836679a 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -4,6 +4,7 @@ import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; import com.techcourse.domain.UserHistory; +import java.util.List; public class UserService { @@ -29,4 +30,8 @@ public void changePassword(final long id, final String newPassword, final String userDao.update(user); userHistoryDao.log(new UserHistory(user, createBy)); } + + public List findLogsByUserId(final long id) { + return userHistoryDao.findByUserId(id); + } } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index 255a0ebfe7..2370d45e03 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -1,18 +1,21 @@ package com.techcourse.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + import com.techcourse.config.DataSourceConfig; import com.techcourse.dao.UserDao; import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; +import com.techcourse.domain.UserHistory; import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; -import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.core.JdbcTemplate; +import java.util.List; +import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; @Disabled class UserServiceTest { @@ -44,6 +47,26 @@ void testChangePassword() { assertThat(actual.getPassword()).isEqualTo(newPassword); } + @Test + void testChangePasswordWithUserHistory() { + final var userHistoryDao = new UserHistoryDao(jdbcTemplate); + final var userService = new UserService(userDao, userHistoryDao); + + final var newPassword = "qqqqq"; + final var createBy = "gugu"; + userService.changePassword(1L, newPassword, createBy); + final List logs = userService.findLogsByUserId(1L); + + final var actual = userService.findById(1L); + + SoftAssertions.assertSoftly( + soft -> { + soft.assertThat(actual.getPassword()).isEqualTo(newPassword); + soft.assertThat(logs).hasSize(1); + } + ); + } + @Test void testTransactionRollback() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 diff --git a/docs/README.md b/docs/README.md index 32d5afa502..6981e8a6b0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,6 @@ - [x] JdbcTemplate 변수 네이밍 명확하게 수정 - [x] 사용하지 않는 log 제거 -- [ ] User에 비밀번호 변경 기능 구현 - - [ ] 변경 이력 저장 +- [x] User에 비밀번호 변경 기능 구현 + - [x] 변경 이력 저장 - [ ] 트랜잭션 설정 From fb3c95ef46be395fb372d137f37e3def7847eda4 Mon Sep 17 00:00:00 2001 From: This2sho Date: Tue, 10 Oct 2023 11:30:53 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat:=20Transaction=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dao/UserDaoWithTransaction.java | 55 ++++++++++++++++ .../dao/UserHistoryDaoWithTransaction.java | 44 +++++++++++++ .../service/UserServiceWithTransaction.java | 42 ++++++++++++ .../service/MockUserHistoryDao.java | 14 ++-- .../techcourse/service/UserServiceTest.java | 15 +++-- docs/README.md | 2 +- .../jdbc/core/TransactionTemplate.java | 65 +++++++++++++++++++ 7 files changed, 224 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/com/techcourse/dao/UserDaoWithTransaction.java create mode 100644 app/src/main/java/com/techcourse/dao/UserHistoryDaoWithTransaction.java create mode 100644 app/src/main/java/com/techcourse/service/UserServiceWithTransaction.java create mode 100644 jdbc/src/main/java/org/springframework/jdbc/core/TransactionTemplate.java diff --git a/app/src/main/java/com/techcourse/dao/UserDaoWithTransaction.java b/app/src/main/java/com/techcourse/dao/UserDaoWithTransaction.java new file mode 100644 index 0000000000..ecb30612cf --- /dev/null +++ b/app/src/main/java/com/techcourse/dao/UserDaoWithTransaction.java @@ -0,0 +1,55 @@ +package com.techcourse.dao; + +import com.techcourse.domain.User; +import java.sql.Connection; +import java.util.List; +import javax.sql.DataSource; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.TransactionTemplate; + +public class UserDaoWithTransaction extends UserDao { + + private static final RowMapper USER_ROW_MAPPER = rs -> new User( + rs.getLong("id"), + rs.getString("account"), + rs.getString("password"), + rs.getString("email")); + + private final TransactionTemplate transactionTemplate; + + public UserDaoWithTransaction(final DataSource dataSource) { + super(dataSource); + this.transactionTemplate = new TransactionTemplate(dataSource); + } + + public UserDaoWithTransaction(final TransactionTemplate transactionTemplate) { + super(transactionTemplate); + this.transactionTemplate = transactionTemplate; + } + + public void insert(final Connection connection, final User user) { + final var sql = "insert into users (account, password, email) values (?, ?, ?)"; + transactionTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail()); + } + + public void update(final Connection connection, final User user) { + final var sql = "update users set (account, password, email) = (?, ?, ?) where id = ?"; + transactionTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), + user.getId()); + } + + public List findAll(final Connection connection) { + final var sql = "select * from users"; + return transactionTemplate.query(connection, sql, USER_ROW_MAPPER); + } + + public User findById(final Connection connection, final Long id) { + final var sql = "select id, account, password, email from users where id = ?"; + return transactionTemplate.queryForObject(connection, sql, USER_ROW_MAPPER, id); + } + + public User findByAccount(final Connection connection, final String account) { + final var sql = "select id, account, password, email from users where account = ?"; + return transactionTemplate.queryForObject(connection, sql, USER_ROW_MAPPER, account); + } +} diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDaoWithTransaction.java b/app/src/main/java/com/techcourse/dao/UserHistoryDaoWithTransaction.java new file mode 100644 index 0000000000..4c1f27f72f --- /dev/null +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDaoWithTransaction.java @@ -0,0 +1,44 @@ +package com.techcourse.dao; + +import com.techcourse.domain.UserHistory; +import java.sql.Connection; +import java.util.List; +import javax.sql.DataSource; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.TransactionTemplate; + +public class UserHistoryDaoWithTransaction extends UserHistoryDao { + + private static final RowMapper USER_HISTORY_ROW_MAPPER = rs -> new UserHistory( + rs.getLong("id"), + rs.getLong("user_id"), + rs.getString("account"), + rs.getString("password"), + rs.getString("email"), + rs.getString("created_by")); + + private final TransactionTemplate transactionTemplate; + + public UserHistoryDaoWithTransaction(final DataSource dataSource) { + super(dataSource); + this.transactionTemplate = new TransactionTemplate(dataSource); + } + + public UserHistoryDaoWithTransaction(final TransactionTemplate transactionTemplate) { + super(transactionTemplate); + this.transactionTemplate = transactionTemplate; + } + + 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 (?, ?, ?, ?, ?, ?)"; + + transactionTemplate.update(connection, sql, userHistory.getUserId(), userHistory.getAccount(), + userHistory.getPassword(), + userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy()); + } + + public List findByUserId(final Connection connection, final Long userId) { + final var sql = "select * from user_history where user_id = ?"; + return transactionTemplate.query(connection, sql, USER_HISTORY_ROW_MAPPER, userId); + } +} diff --git a/app/src/main/java/com/techcourse/service/UserServiceWithTransaction.java b/app/src/main/java/com/techcourse/service/UserServiceWithTransaction.java new file mode 100644 index 0000000000..63e7fc5b59 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/UserServiceWithTransaction.java @@ -0,0 +1,42 @@ +package com.techcourse.service; + +import com.techcourse.dao.UserDaoWithTransaction; +import com.techcourse.dao.UserHistoryDaoWithTransaction; +import com.techcourse.domain.UserHistory; +import java.sql.SQLException; +import javax.sql.DataSource; +import org.springframework.dao.DataAccessException; + +public class UserServiceWithTransaction extends UserService { + + private final DataSource dataSource; + private final UserDaoWithTransaction userDao; + private final UserHistoryDaoWithTransaction userHistoryDao; + + public UserServiceWithTransaction(final UserDaoWithTransaction userDao, + final UserHistoryDaoWithTransaction userHistoryDao, + final DataSource dataSource) { + super(userDao, userHistoryDao); + this.dataSource = dataSource; + this.userDao = userDao; + this.userHistoryDao = userHistoryDao; + } + + public void changePasswordWithTransaction(final long id, final String newPassword, final String createBy) { + try (final var connection = dataSource.getConnection()) { + try { + connection.setAutoCommit(false); + final var user = findById(id); + user.changePassword(newPassword); + userDao.update(connection, user); + userHistoryDao.log(connection, new UserHistory(user, createBy)); + connection.commit(); + } catch (Exception e) { + connection.rollback(); + throw new DataAccessException(e); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java index 2ee12b195f..2fdf4004ca 100644 --- a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java +++ b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java @@ -1,18 +1,20 @@ package com.techcourse.service; -import com.techcourse.dao.UserHistoryDao; +import com.techcourse.dao.UserHistoryDaoWithTransaction; import com.techcourse.domain.UserHistory; +import java.sql.Connection; +import javax.sql.DataSource; import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.TransactionTemplate; -public class MockUserHistoryDao extends UserHistoryDao { +public class MockUserHistoryDao extends UserHistoryDaoWithTransaction { - public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) { - super(jdbcTemplate); + public MockUserHistoryDao(DataSource dataSource) { + super(new TransactionTemplate(dataSource)); } @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 2370d45e03..41839f33dd 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -5,27 +5,29 @@ import com.techcourse.config.DataSourceConfig; import com.techcourse.dao.UserDao; +import com.techcourse.dao.UserDaoWithTransaction; import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; import com.techcourse.domain.UserHistory; import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; import java.util.List; +import javax.sql.DataSource; import org.assertj.core.api.SoftAssertions; 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.JdbcTemplate; -@Disabled class UserServiceTest { private JdbcTemplate jdbcTemplate; private UserDao userDao; + private DataSource dataSource; @BeforeEach void setUp() { - this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); + this.dataSource = DataSourceConfig.getInstance(); + this.jdbcTemplate = new JdbcTemplate(dataSource); this.userDao = new UserDao(jdbcTemplate); DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); @@ -70,14 +72,15 @@ void testChangePasswordWithUserHistory() { @Test void testTransactionRollback() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 - final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + final var userHistoryDao = new MockUserHistoryDao(dataSource); + final var userDaoWithTransaction = new UserDaoWithTransaction(dataSource); + final var userService = new UserServiceWithTransaction(userDaoWithTransaction, userHistoryDao, dataSource); final var newPassword = "newPassword"; final var createBy = "gugu"; // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. assertThrows(DataAccessException.class, - () -> userService.changePassword(1L, newPassword, createBy)); + () -> userService.changePasswordWithTransaction(1L, newPassword, createBy)); final var actual = userService.findById(1L); diff --git a/docs/README.md b/docs/README.md index 6981e8a6b0..9d55053823 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,4 +17,4 @@ - [x] User에 비밀번호 변경 기능 구현 - [x] 변경 이력 저장 - - [ ] 트랜잭션 설정 + - [x] 트랜잭션 설정 diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/TransactionTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/TransactionTemplate.java new file mode 100644 index 0000000000..ae648a8b0d --- /dev/null +++ b/jdbc/src/main/java/org/springframework/jdbc/core/TransactionTemplate.java @@ -0,0 +1,65 @@ +package org.springframework.jdbc.core; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import javax.sql.DataSource; +import org.springframework.dao.DataAccessException; + +public class TransactionTemplate extends JdbcTemplate{ + + public TransactionTemplate(final DataSource dataSource) { + super(dataSource); + } + + public void update(final Connection connection, final String sql, final Object... parameters) { + execute(connection, new PreparedStatementCreator(sql), statement -> { + final var statementSetter = newArgumentPreparedStatementSetter(parameters); + statementSetter.setValues(statement); + return statement.executeUpdate(); + }); + } + + public T execute(Connection connection, PreparedStatementCreator statementCreator, + StatementCallback action) { + try (var statement = statementCreator.createPreparedStatement(connection)) { + return action.doInStatement(statement); + } catch (SQLException e) { + throw new DataAccessException(e); + } + } + + private ArgumentPreparedStatementSetter newArgumentPreparedStatementSetter(final Object... parameters) { + return new ArgumentPreparedStatementSetter(parameters); + } + + public T queryForObject(final Connection connection, final String sql, final RowMapper rowMapper, + final Object... parameters) { + final List result = query(connection, sql, rowMapper, parameters); + if (result.size() != 1) { + throw new IllegalStateException("Query 조회 결과가 하나가 아닙니다."); + } + return result.iterator().next(); + } + + public List query(final Connection connection, final String sql, final RowMapper rowMapper, + final Object... parameters) { + return execute( + connection, new PreparedStatementCreator(sql), statement -> { + final var statementSetter = new ArgumentPreparedStatementSetter(parameters); + statementSetter.setValues(statement); + final var resultSet = statement.executeQuery(); + return result(rowMapper, resultSet); + }); + } + + private List result(final RowMapper rowMapper, final ResultSet resultSet) throws SQLException { + List result = new ArrayList<>(); + while (resultSet.next()) { + result.add(rowMapper.mapRow(resultSet)); + } + return result; + } +} From cd7d868b413d028f4ac361f7a30a828962f650a8 Mon Sep 17 00:00:00 2001 From: This2sho Date: Tue, 10 Oct 2023 14:08:35 +0900 Subject: [PATCH 08/14] =?UTF-8?q?docs:=20=EA=B8=B0=EB=8A=A5=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/README.md b/docs/README.md index 9d55053823..68bc2fbd66 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,3 +18,7 @@ - [x] User에 비밀번호 변경 기능 구현 - [x] 변경 이력 저장 - [x] 트랜잭션 설정 + +## 4단계 +- [ ] Transaction synchronization 적용 +- [ ] 트랜잭션 서비스 추상화하기 From e1a6d55ff101d646208ca8ff2b3d044255a06cca Mon Sep 17 00:00:00 2001 From: This2sho Date: Tue, 10 Oct 2023 14:09:54 +0900 Subject: [PATCH 09/14] =?UTF-8?q?refactor:=20Transaction=20synchronization?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/UserServiceWithTransaction.java | 28 +++++++++++-------- docs/README.md | 2 +- .../jdbc/core/JdbcTemplate.java | 6 ++-- .../jdbc/datasource/DataSourceUtils.java | 2 +- .../TransactionSynchronizationManager.java | 17 +++++++++-- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/techcourse/service/UserServiceWithTransaction.java b/app/src/main/java/com/techcourse/service/UserServiceWithTransaction.java index 63e7fc5b59..21cc99bd6c 100644 --- a/app/src/main/java/com/techcourse/service/UserServiceWithTransaction.java +++ b/app/src/main/java/com/techcourse/service/UserServiceWithTransaction.java @@ -6,6 +6,8 @@ import java.sql.SQLException; import javax.sql.DataSource; import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.transaction.support.TransactionSynchronizationManager; public class UserServiceWithTransaction extends UserService { @@ -23,20 +25,24 @@ public UserServiceWithTransaction(final UserDaoWithTransaction userDao, } public void changePasswordWithTransaction(final long id, final String newPassword, final String createBy) { - try (final var connection = dataSource.getConnection()) { + final var connection = DataSourceUtils.getConnection(dataSource); + try { + connection.setAutoCommit(false); + final var user = findById(id); + user.changePassword(newPassword); + userDao.update(connection, user); + userHistoryDao.log(connection, new UserHistory(user, createBy)); + connection.commit(); + } catch (Exception e) { try { - connection.setAutoCommit(false); - final var user = findById(id); - user.changePassword(newPassword); - userDao.update(connection, user); - userHistoryDao.log(connection, new UserHistory(user, createBy)); - connection.commit(); - } catch (Exception e) { connection.rollback(); - throw new DataAccessException(e); + } catch (SQLException ex) { + throw new DataAccessException(ex); } - } catch (SQLException e) { - throw new RuntimeException(e); + throw new DataAccessException(e); + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + TransactionSynchronizationManager.unbindResource(dataSource); } } } diff --git a/docs/README.md b/docs/README.md index 68bc2fbd66..9f47d27529 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,5 +20,5 @@ - [x] 트랜잭션 설정 ## 4단계 -- [ ] Transaction synchronization 적용 +- [x] Transaction synchronization 적용 - [ ] 트랜잭션 서비스 추상화하기 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 0dadda43b0..3bdc06737b 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -1,11 +1,13 @@ package org.springframework.jdbc.core; +import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import javax.sql.DataSource; import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.DataSourceUtils; public class JdbcTemplate { @@ -24,8 +26,8 @@ public void update(final String sql, final Object... parameters) { } public T execute(PreparedStatementCreator statementCreator, StatementCallback action) { - try (var connection = dataSource.getConnection(); - var statement = statementCreator.createPreparedStatement(connection)) { + final Connection connection = DataSourceUtils.getConnection(dataSource); + try (var statement = statementCreator.createPreparedStatement(connection)) { return action.doInStatement(statement); } catch (SQLException e) { throw new DataAccessException(e); diff --git a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java index 3c40bfec52..062ea351e8 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -7,7 +7,6 @@ import java.sql.Connection; import java.sql.SQLException; -// 4단계 미션에서 사용할 것 public abstract class DataSourceUtils { private DataSourceUtils() {} @@ -29,6 +28,7 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd public static void releaseConnection(Connection connection, DataSource dataSource) { try { + TransactionSynchronizationManager.unbindResource(dataSource); connection.close(); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection"); diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index 715557fc66..eff53695da 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -1,23 +1,34 @@ package org.springframework.transaction.support; +import java.util.HashMap; import javax.sql.DataSource; import java.sql.Connection; import java.util.Map; public abstract class TransactionSynchronizationManager { - private static final ThreadLocal> resources = new ThreadLocal<>(); + private static final ThreadLocal> resources = init(); private TransactionSynchronizationManager() {} + private static ThreadLocal> init() { + final ThreadLocal> result = new ThreadLocal<>(); + result.set(new HashMap<>()); + return result; + } + public static Connection getResource(DataSource key) { - return null; + final Map map = resources.get(); + return map.get(key); } public static void bindResource(DataSource key, Connection value) { + final Map map = resources.get(); + map.put(key, value); } public static Connection unbindResource(DataSource key) { - return null; + final Map map = resources.get(); + return map.remove(key); } } From ab2d58e930bf4c81690c50f775244487cef7d72d Mon Sep 17 00:00:00 2001 From: This2sho Date: Tue, 10 Oct 2023 17:15:15 +0900 Subject: [PATCH 10/14] =?UTF-8?q?refactor:=203=EB=8B=A8=EA=B3=84=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=9E=91=EC=84=B1=ED=95=9C=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dao/UserDaoWithTransaction.java | 55 ---------------- .../dao/UserHistoryDaoWithTransaction.java | 44 ------------- .../service/UserServiceWithTransaction.java | 48 -------------- .../service/MockUserHistoryDao.java | 14 ++-- .../jdbc/core/TransactionTemplate.java | 65 ------------------- 5 files changed, 6 insertions(+), 220 deletions(-) delete mode 100644 app/src/main/java/com/techcourse/dao/UserDaoWithTransaction.java delete mode 100644 app/src/main/java/com/techcourse/dao/UserHistoryDaoWithTransaction.java delete mode 100644 app/src/main/java/com/techcourse/service/UserServiceWithTransaction.java delete mode 100644 jdbc/src/main/java/org/springframework/jdbc/core/TransactionTemplate.java diff --git a/app/src/main/java/com/techcourse/dao/UserDaoWithTransaction.java b/app/src/main/java/com/techcourse/dao/UserDaoWithTransaction.java deleted file mode 100644 index ecb30612cf..0000000000 --- a/app/src/main/java/com/techcourse/dao/UserDaoWithTransaction.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.techcourse.dao; - -import com.techcourse.domain.User; -import java.sql.Connection; -import java.util.List; -import javax.sql.DataSource; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.core.TransactionTemplate; - -public class UserDaoWithTransaction extends UserDao { - - private static final RowMapper USER_ROW_MAPPER = rs -> new User( - rs.getLong("id"), - rs.getString("account"), - rs.getString("password"), - rs.getString("email")); - - private final TransactionTemplate transactionTemplate; - - public UserDaoWithTransaction(final DataSource dataSource) { - super(dataSource); - this.transactionTemplate = new TransactionTemplate(dataSource); - } - - public UserDaoWithTransaction(final TransactionTemplate transactionTemplate) { - super(transactionTemplate); - this.transactionTemplate = transactionTemplate; - } - - public void insert(final Connection connection, final User user) { - final var sql = "insert into users (account, password, email) values (?, ?, ?)"; - transactionTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail()); - } - - public void update(final Connection connection, final User user) { - final var sql = "update users set (account, password, email) = (?, ?, ?) where id = ?"; - transactionTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), - user.getId()); - } - - public List findAll(final Connection connection) { - final var sql = "select * from users"; - return transactionTemplate.query(connection, sql, USER_ROW_MAPPER); - } - - public User findById(final Connection connection, final Long id) { - final var sql = "select id, account, password, email from users where id = ?"; - return transactionTemplate.queryForObject(connection, sql, USER_ROW_MAPPER, id); - } - - public User findByAccount(final Connection connection, final String account) { - final var sql = "select id, account, password, email from users where account = ?"; - return transactionTemplate.queryForObject(connection, sql, USER_ROW_MAPPER, account); - } -} diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDaoWithTransaction.java b/app/src/main/java/com/techcourse/dao/UserHistoryDaoWithTransaction.java deleted file mode 100644 index 4c1f27f72f..0000000000 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDaoWithTransaction.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.techcourse.dao; - -import com.techcourse.domain.UserHistory; -import java.sql.Connection; -import java.util.List; -import javax.sql.DataSource; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.core.TransactionTemplate; - -public class UserHistoryDaoWithTransaction extends UserHistoryDao { - - private static final RowMapper USER_HISTORY_ROW_MAPPER = rs -> new UserHistory( - rs.getLong("id"), - rs.getLong("user_id"), - rs.getString("account"), - rs.getString("password"), - rs.getString("email"), - rs.getString("created_by")); - - private final TransactionTemplate transactionTemplate; - - public UserHistoryDaoWithTransaction(final DataSource dataSource) { - super(dataSource); - this.transactionTemplate = new TransactionTemplate(dataSource); - } - - public UserHistoryDaoWithTransaction(final TransactionTemplate transactionTemplate) { - super(transactionTemplate); - this.transactionTemplate = transactionTemplate; - } - - 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 (?, ?, ?, ?, ?, ?)"; - - transactionTemplate.update(connection, sql, userHistory.getUserId(), userHistory.getAccount(), - userHistory.getPassword(), - userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy()); - } - - public List findByUserId(final Connection connection, final Long userId) { - final var sql = "select * from user_history where user_id = ?"; - return transactionTemplate.query(connection, sql, USER_HISTORY_ROW_MAPPER, userId); - } -} diff --git a/app/src/main/java/com/techcourse/service/UserServiceWithTransaction.java b/app/src/main/java/com/techcourse/service/UserServiceWithTransaction.java deleted file mode 100644 index 21cc99bd6c..0000000000 --- a/app/src/main/java/com/techcourse/service/UserServiceWithTransaction.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.techcourse.service; - -import com.techcourse.dao.UserDaoWithTransaction; -import com.techcourse.dao.UserHistoryDaoWithTransaction; -import com.techcourse.domain.UserHistory; -import java.sql.SQLException; -import javax.sql.DataSource; -import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.datasource.DataSourceUtils; -import org.springframework.transaction.support.TransactionSynchronizationManager; - -public class UserServiceWithTransaction extends UserService { - - private final DataSource dataSource; - private final UserDaoWithTransaction userDao; - private final UserHistoryDaoWithTransaction userHistoryDao; - - public UserServiceWithTransaction(final UserDaoWithTransaction userDao, - final UserHistoryDaoWithTransaction userHistoryDao, - final DataSource dataSource) { - super(userDao, userHistoryDao); - this.dataSource = dataSource; - this.userDao = userDao; - this.userHistoryDao = userHistoryDao; - } - - public void changePasswordWithTransaction(final long id, final String newPassword, final String createBy) { - final var connection = DataSourceUtils.getConnection(dataSource); - try { - connection.setAutoCommit(false); - final var user = findById(id); - user.changePassword(newPassword); - userDao.update(connection, user); - userHistoryDao.log(connection, new UserHistory(user, createBy)); - connection.commit(); - } catch (Exception e) { - try { - connection.rollback(); - } catch (SQLException ex) { - throw new DataAccessException(ex); - } - throw new DataAccessException(e); - } finally { - DataSourceUtils.releaseConnection(connection, dataSource); - TransactionSynchronizationManager.unbindResource(dataSource); - } - } -} diff --git a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java index 2fdf4004ca..2ee12b195f 100644 --- a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java +++ b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java @@ -1,20 +1,18 @@ package com.techcourse.service; -import com.techcourse.dao.UserHistoryDaoWithTransaction; +import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.UserHistory; -import java.sql.Connection; -import javax.sql.DataSource; import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.core.TransactionTemplate; +import org.springframework.jdbc.core.JdbcTemplate; -public class MockUserHistoryDao extends UserHistoryDaoWithTransaction { +public class MockUserHistoryDao extends UserHistoryDao { - public MockUserHistoryDao(DataSource dataSource) { - super(new TransactionTemplate(dataSource)); + public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); } @Override - public void log(final Connection connection, final UserHistory userHistory) { + public void log(final UserHistory userHistory) { throw new DataAccessException(); } } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/TransactionTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/TransactionTemplate.java deleted file mode 100644 index ae648a8b0d..0000000000 --- a/jdbc/src/main/java/org/springframework/jdbc/core/TransactionTemplate.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.springframework.jdbc.core; - -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import javax.sql.DataSource; -import org.springframework.dao.DataAccessException; - -public class TransactionTemplate extends JdbcTemplate{ - - public TransactionTemplate(final DataSource dataSource) { - super(dataSource); - } - - public void update(final Connection connection, final String sql, final Object... parameters) { - execute(connection, new PreparedStatementCreator(sql), statement -> { - final var statementSetter = newArgumentPreparedStatementSetter(parameters); - statementSetter.setValues(statement); - return statement.executeUpdate(); - }); - } - - public T execute(Connection connection, PreparedStatementCreator statementCreator, - StatementCallback action) { - try (var statement = statementCreator.createPreparedStatement(connection)) { - return action.doInStatement(statement); - } catch (SQLException e) { - throw new DataAccessException(e); - } - } - - private ArgumentPreparedStatementSetter newArgumentPreparedStatementSetter(final Object... parameters) { - return new ArgumentPreparedStatementSetter(parameters); - } - - public T queryForObject(final Connection connection, final String sql, final RowMapper rowMapper, - final Object... parameters) { - final List result = query(connection, sql, rowMapper, parameters); - if (result.size() != 1) { - throw new IllegalStateException("Query 조회 결과가 하나가 아닙니다."); - } - return result.iterator().next(); - } - - public List query(final Connection connection, final String sql, final RowMapper rowMapper, - final Object... parameters) { - return execute( - connection, new PreparedStatementCreator(sql), statement -> { - final var statementSetter = new ArgumentPreparedStatementSetter(parameters); - statementSetter.setValues(statement); - final var resultSet = statement.executeQuery(); - return result(rowMapper, resultSet); - }); - } - - private List result(final RowMapper rowMapper, final ResultSet resultSet) throws SQLException { - List result = new ArrayList<>(); - while (resultSet.next()) { - result.add(rowMapper.mapRow(resultSet)); - } - return result; - } -} From 6f48ae4a4941fac7253f0594a9fbb0bfeb56334b Mon Sep 17 00:00:00 2001 From: This2sho Date: Tue, 10 Oct 2023 17:16:47 +0900 Subject: [PATCH 11/14] =?UTF-8?q?refactor:=20TransactionManager=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=EC=9D=84=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=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 --- .../techcourse/service/AppUserService.java | 40 ++++++++++++++++ .../com/techcourse/service/TxUserService.java | 31 ++++++++++++ .../com/techcourse/service/UserService.java | 34 ++------------ .../techcourse/service/UserServiceTest.java | 16 ++++--- docs/README.md | 3 +- .../support/TransactionManager.java | 47 +++++++++++++++++++ 6 files changed, 134 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/com/techcourse/service/AppUserService.java create mode 100644 app/src/main/java/com/techcourse/service/TxUserService.java create mode 100644 jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java diff --git a/app/src/main/java/com/techcourse/service/AppUserService.java b/app/src/main/java/com/techcourse/service/AppUserService.java new file mode 100644 index 0000000000..3d3e075518 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/AppUserService.java @@ -0,0 +1,40 @@ +package com.techcourse.service; + +import com.techcourse.dao.UserDao; +import com.techcourse.dao.UserHistoryDao; +import com.techcourse.domain.User; +import com.techcourse.domain.UserHistory; +import java.util.List; + +public class AppUserService implements UserService{ + + private final UserDao userDao; + private final UserHistoryDao userHistoryDao; + + public AppUserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { + this.userDao = userDao; + this.userHistoryDao = userHistoryDao; + } + + @Override + public User findById(final long id) { + return userDao.findById(id); + } + + @Override + public void insert(final User user) { + userDao.insert(user); + } + + @Override + 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)); + } + + public List findLogsByUserId(final long id) { + return userHistoryDao.findByUserId(id); + } +} diff --git a/app/src/main/java/com/techcourse/service/TxUserService.java b/app/src/main/java/com/techcourse/service/TxUserService.java new file mode 100644 index 0000000000..e269584b17 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,31 @@ +package com.techcourse.service; + +import com.techcourse.domain.User; +import org.springframework.transaction.support.TransactionManager; + +public class TxUserService implements UserService { + + private final UserService userService; + + private final TransactionManager transactionManager; + + public TxUserService(UserService userService, TransactionManager transactionManager) { + this.userService = userService; + this.transactionManager = transactionManager; + } + + @Override + public User findById(final long id) { + return transactionManager.executeWithResult(() -> userService.findById(id)); + } + + @Override + public void insert(final User user) { + transactionManager.execute(() -> userService.insert(user)); + } + + @Override + public void changePassword(final long id, final String newPassword, final String createBy) { + transactionManager.execute(() -> userService.changePassword(id, newPassword, createBy)); + } +} diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index cc1836679a..bf507fc939 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,37 +1,13 @@ package com.techcourse.service; -import com.techcourse.dao.UserDao; -import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; -import com.techcourse.domain.UserHistory; -import java.util.List; -public class UserService { +public interface UserService { - private final UserDao userDao; - private final UserHistoryDao userHistoryDao; + User findById(final long id); - public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { - this.userDao = userDao; - this.userHistoryDao = userHistoryDao; - } + void insert(final User user); - public User findById(final long id) { - return userDao.findById(id); - } - - public void insert(final User user) { - userDao.insert(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)); - } - - public List findLogsByUserId(final long id) { - return userHistoryDao.findByUserId(id); - } + void changePassword(final long id, final String newPassword, final String createBy); } + diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index 41839f33dd..c545e81c2a 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -5,7 +5,6 @@ import com.techcourse.config.DataSourceConfig; import com.techcourse.dao.UserDao; -import com.techcourse.dao.UserDaoWithTransaction; import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; import com.techcourse.domain.UserHistory; @@ -17,6 +16,7 @@ import org.junit.jupiter.api.Test; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.transaction.support.TransactionManager; class UserServiceTest { @@ -38,7 +38,7 @@ void setUp() { @Test void testChangePassword() { final var userHistoryDao = new UserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + final var userService = new AppUserService(userDao, userHistoryDao); final var newPassword = "qqqqq"; final var createBy = "gugu"; @@ -52,7 +52,7 @@ void testChangePassword() { @Test void testChangePasswordWithUserHistory() { final var userHistoryDao = new UserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + final var userService = new AppUserService(userDao, userHistoryDao); final var newPassword = "qqqqq"; final var createBy = "gugu"; @@ -72,15 +72,17 @@ void testChangePasswordWithUserHistory() { @Test void testTransactionRollback() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 - final var userHistoryDao = new MockUserHistoryDao(dataSource); - final var userDaoWithTransaction = new UserDaoWithTransaction(dataSource); - final var userService = new UserServiceWithTransaction(userDaoWithTransaction, userHistoryDao, dataSource); + final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); + // 애플리케이션 서비스 + final var appUserService = new AppUserService(userDao, userHistoryDao); + // 트랜잭션 서비스 추상화 + final var userService = new TxUserService(appUserService, new TransactionManager(dataSource)); final var newPassword = "newPassword"; final var createBy = "gugu"; // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. assertThrows(DataAccessException.class, - () -> userService.changePasswordWithTransaction(1L, newPassword, createBy)); + () -> userService.changePassword(1L, newPassword, createBy)); final var actual = userService.findById(1L); diff --git a/docs/README.md b/docs/README.md index 9f47d27529..1aae34a258 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,4 +21,5 @@ ## 4단계 - [x] Transaction synchronization 적용 -- [ ] 트랜잭션 서비스 추상화하기 +- [x] 트랜잭션 서비스 추상화하기 + - [x] 공통 코드 분리 diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java new file mode 100644 index 0000000000..764ff1465a --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java @@ -0,0 +1,47 @@ +package org.springframework.transaction.support; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.function.Supplier; +import javax.sql.DataSource; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.DataSourceUtils; + +public class TransactionManager { + + private final DataSource dataSource; + + public TransactionManager(final DataSource dataSource) { + this.dataSource = dataSource; + } + + public void execute(final Runnable runnable) { + executeWithResult(() -> { + runnable.run(); + return null; + }); + } + + public T executeWithResult(final Supplier supplier) { + final Connection connection = DataSourceUtils.getConnection(dataSource); + try{ + connection.setAutoCommit(false); + final T result = supplier.get(); + connection.commit(); + return result; + } catch (SQLException e) { + rollback(connection); + throw new DataAccessException(e); + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + } + } + + public void rollback(Connection connection) { + try { + connection.rollback(); + } catch (SQLException e) { + throw new DataAccessException(e); + } + } +} From f3a013de5f9703e2edaa7608d9cf041a8ba04099 Mon Sep 17 00:00:00 2001 From: This2sho Date: Wed, 11 Oct 2023 13:28:35 +0900 Subject: [PATCH 12/14] =?UTF-8?q?refactor:=20ThreadLocal=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EB=B0=A9=EB=B2=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../support/TransactionSynchronizationManager.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index eff53695da..1656547a08 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -7,16 +7,10 @@ public abstract class TransactionSynchronizationManager { - private static final ThreadLocal> resources = init(); + private static final ThreadLocal> resources = ThreadLocal.withInitial(HashMap::new); private TransactionSynchronizationManager() {} - private static ThreadLocal> init() { - final ThreadLocal> result = new ThreadLocal<>(); - result.set(new HashMap<>()); - return result; - } - public static Connection getResource(DataSource key) { final Map map = resources.get(); return map.get(key); From 2edac4c5abde124d15241a3c138d35637bb36772 Mon Sep 17 00:00:00 2001 From: This2sho Date: Wed, 11 Oct 2023 15:05:51 +0900 Subject: [PATCH 13/14] =?UTF-8?q?refactor:=20null=20=EB=B0=98=ED=99=98=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20=EC=83=88=EB=A1=9C=EC=9A=B4=20=EB=A9=94?= =?UTF-8?q?=EC=86=8C=EB=93=9C=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transaction/support/TransactionManager.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java index 764ff1465a..5e4dd3cee8 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java @@ -16,15 +16,22 @@ public TransactionManager(final DataSource dataSource) { } public void execute(final Runnable runnable) { - executeWithResult(() -> { + final Connection connection = DataSourceUtils.getConnection(dataSource); + try { + connection.setAutoCommit(false); runnable.run(); - return null; - }); + connection.commit(); + } catch (SQLException e) { + rollback(connection); + throw new DataAccessException(e); + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); + } } public T executeWithResult(final Supplier supplier) { final Connection connection = DataSourceUtils.getConnection(dataSource); - try{ + try { connection.setAutoCommit(false); final T result = supplier.get(); connection.commit(); From 5d566d14702f8166fa907a5e3e6a8fc1ee93f629 Mon Sep 17 00:00:00 2001 From: This2sho Date: Wed, 11 Oct 2023 15:29:00 +0900 Subject: [PATCH 14/14] =?UTF-8?q?refactor:=20Transaction=20=EC=97=86?= =?UTF-8?q?=EC=9D=B4=20=EC=82=AC=EC=9A=A9=ED=96=88=EC=9D=84=20=EB=95=8C=20?= =?UTF-8?q?=EC=BB=A4=EB=82=B5=EC=85=98=EC=9D=84=20=EC=A2=85=EB=A3=8C?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jdbc/core/JdbcTemplate.java | 2 ++ .../jdbc/datasource/DataSourceUtils.java | 4 ++++ .../support/TransactionManager.java | 19 +++++++++++++++---- .../TransactionSynchronizationManager.java | 10 ++++++++++ 4 files changed, 31 insertions(+), 4 deletions(-) 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 3bdc06737b..4b0a26c493 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -31,6 +31,8 @@ public T execute(PreparedStatementCreator statementCreator, StatementCallbac return action.doInStatement(statement); } catch (SQLException e) { throw new DataAccessException(e); + } finally { + DataSourceUtils.releaseConnection(connection, dataSource); } } diff --git a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java index 062ea351e8..b81d23df65 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -27,6 +27,10 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd } public static void releaseConnection(Connection connection, DataSource dataSource) { + final boolean isActiveTransaction = TransactionSynchronizationManager.isActiveTransaction(connection); + if (isActiveTransaction) { + return; + } try { TransactionSynchronizationManager.unbindResource(dataSource); connection.close(); diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java index 5e4dd3cee8..564dd5082d 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java @@ -16,7 +16,7 @@ public TransactionManager(final DataSource dataSource) { } public void execute(final Runnable runnable) { - final Connection connection = DataSourceUtils.getConnection(dataSource); + final Connection connection = getConnection(); try { connection.setAutoCommit(false); runnable.run(); @@ -25,12 +25,23 @@ public void execute(final Runnable runnable) { rollback(connection); throw new DataAccessException(e); } finally { - DataSourceUtils.releaseConnection(connection, dataSource); + releaseConnection(connection); } } - public T executeWithResult(final Supplier supplier) { + private Connection getConnection() { final Connection connection = DataSourceUtils.getConnection(dataSource); + TransactionSynchronizationManager.setActiveTransaction(connection, true); + return connection; + } + + private void releaseConnection(final Connection connection) { + TransactionSynchronizationManager.setActiveTransaction(connection, false); + DataSourceUtils.releaseConnection(connection, dataSource); + } + + public T executeWithResult(final Supplier supplier) { + final Connection connection = getConnection(); try { connection.setAutoCommit(false); final T result = supplier.get(); @@ -40,7 +51,7 @@ public T executeWithResult(final Supplier supplier) { rollback(connection); throw new DataAccessException(e); } finally { - DataSourceUtils.releaseConnection(connection, dataSource); + releaseConnection(connection); } } diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index 1656547a08..e2c6aa71ea 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -9,6 +9,8 @@ public abstract class TransactionSynchronizationManager { private static final ThreadLocal> resources = ThreadLocal.withInitial(HashMap::new); + private static final ThreadLocal> activeTransactions = ThreadLocal.withInitial(HashMap::new); + private TransactionSynchronizationManager() {} public static Connection getResource(DataSource key) { @@ -25,4 +27,12 @@ public static Connection unbindResource(DataSource key) { final Map map = resources.get(); return map.remove(key); } + + public static boolean isActiveTransaction(Connection connection) { + return activeTransactions.get().getOrDefault(connection, false); + } + + public static void setActiveTransaction(Connection connection, boolean status) { + activeTransactions.get().put(connection, status); + } }