From 393ace4f41abf425221980a6f42f2c9b207f5670 Mon Sep 17 00:00:00 2001 From: kevstevie <109793396+kevstevie@users.noreply.github.com> Date: Fri, 13 Oct 2023 20:38:50 +0900 Subject: [PATCH] =?UTF-8?q?jdbc=20step4=20=EC=A3=BC=EB=93=9C=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=A0=9C=EC=B6=9C=ED=95=A9=EB=8B=88=EB=8B=A4=20(#5?= =?UTF-8?q?52)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 패키지 위치 변경 및 코드 정리 * feat: 트랜잭션 required 형태 전파 * refactor: exception 처리 수정 --------- Co-authored-by: kang-hyungu --- .../main/java/com/techcourse/dao/UserDao.java | 25 +++--- .../com/techcourse/dao/UserHistoryDao.java | 5 +- .../techcourse/service/AppUserService.java | 35 +++++++++ .../com/techcourse/service/TxUserService.java | 30 ++++++++ .../com/techcourse/service/UserService.java | 77 +------------------ .../java/com/techcourse/dao/UserDaoTest.java | 26 +++---- .../service/MockUserHistoryDao.java | 4 +- ...erviceTest.java => TxUserServiceTest.java} | 26 ++++--- .../jdbc/core/JdbcTemplate.java | 25 +++--- .../jdbc/datasource/DataSourceUtils.java | 29 ++++--- .../CannotGetJdbcConnectionException.java | 4 +- .../jdbc/exception/DataAccessException.java | 2 +- .../PreparedStatementExecuteException.java | 2 +- .../exception/ResultSetMappingException.java | 2 +- .../jdbc/exception/TransactionException.java | 15 ++++ .../transaction/support/ConnectionHolder.java | 55 +++++++++++++ .../support/TransactionManager.java | 40 ++++++++++ .../TransactionSynchronizationManager.java | 18 +++-- .../support/TransactionTemplate.java | 40 ++++++++++ .../java/aop/config/DataSourceConfig.java | 1 + study/src/main/resources/application.yml | 1 + 21 files changed, 313 insertions(+), 149 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 rename app/src/test/java/com/techcourse/service/{UserServiceTest.java => TxUserServiceTest.java} (60%) rename jdbc/src/main/java/org/springframework/jdbc/{ => exception}/CannotGetJdbcConnectionException.java (73%) create mode 100644 jdbc/src/main/java/org/springframework/jdbc/exception/TransactionException.java create mode 100644 jdbc/src/main/java/org/springframework/transaction/support/ConnectionHolder.java create mode 100644 jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java create mode 100644 jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index 2a2d16c52e..789fe20bd5 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -5,7 +5,6 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.exception.ResultSetMappingException; -import java.sql.Connection; import java.sql.SQLException; import java.util.List; @@ -29,39 +28,39 @@ public UserDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public void insert(final Connection conn, final User user) { + public void insert(final User user) { final String sql = "insert into users (account, password, email) values (?, ?, ?)"; - jdbcTemplate.update(conn, sql, user.getAccount(), user.getPassword(), user.getEmail()); + jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail()); } - public void update(final Connection conn, final User user) { + public void update(final User user) { final var sql = "update users set account = ?, email = ?, password = ? where id = ?"; - jdbcTemplate.update(conn, sql, user.getAccount(), user.getEmail(), user.getPassword(), user.getId()); + jdbcTemplate.update(sql, user.getAccount(), user.getEmail(), user.getPassword(), user.getId()); } - public List findAll(final Connection conn) { + public List findAll() { final String sql = "select * from users"; - return jdbcTemplate.query(conn, sql, ROW_MAPPER); + return jdbcTemplate.query(sql, ROW_MAPPER); } - public User findById(final Connection conn, final Long id) { + public User findById(final Long id) { final var sql = "select id, account, password, email from users where id = ?"; - return jdbcTemplate.queryForObject(conn, sql, ROW_MAPPER, id); + return jdbcTemplate.queryForObject(sql, ROW_MAPPER, id); } - public User findByAccount(final Connection conn, final String account) { + public User findByAccount(final String account) { final var sql = "select id, account, password, email from users where account = ?"; - return jdbcTemplate.queryForObject(conn, sql, ROW_MAPPER, account); + return jdbcTemplate.queryForObject(sql, ROW_MAPPER, account); } - public void deleteAll(final Connection conn) { + public void deleteAll() { final var sql = "delete from users"; - jdbcTemplate.update(conn, sql); + jdbcTemplate.update(sql); } } diff --git a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java index adcdb61a80..a278e0574c 100644 --- a/app/src/main/java/com/techcourse/dao/UserHistoryDao.java +++ b/app/src/main/java/com/techcourse/dao/UserHistoryDao.java @@ -5,8 +5,6 @@ import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; -import java.sql.Connection; - public class UserHistoryDao { private static final Logger log = LoggerFactory.getLogger(UserHistoryDao.class); @@ -17,11 +15,10 @@ public UserHistoryDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public void log(final Connection conn, final UserHistory userHistory) { + public void log(final UserHistory userHistory) { final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; jdbcTemplate.update( - conn, sql, userHistory.getUserId(), userHistory.getAccount(), 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..9ca0dc95fb --- /dev/null +++ b/app/src/main/java/com/techcourse/service/AppUserService.java @@ -0,0 +1,35 @@ +package com.techcourse.service; + +import com.techcourse.dao.UserDao; +import com.techcourse.dao.UserHistoryDao; +import com.techcourse.domain.User; +import com.techcourse.domain.UserHistory; + +public class AppUserService implements UserService { + + private final UserDao userDao; + private final UserHistoryDao userHistoryDao; + + public AppUserService(UserDao userDao, UserHistoryDao userHistoryDao) { + this.userDao = userDao; + this.userHistoryDao = userHistoryDao; + } + + @Override + public User findById(long id) { + return userDao.findById(id); + } + + @Override + public void insert(User user) { + userDao.insert(user); + } + + @Override + public void changePassword(long id, String newPassword, String createBy) { + final var user = findById(id); + user.changePassword(newPassword); + userDao.update(user); + userHistoryDao.log(new UserHistory(user, createBy)); + } +} 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..ad89262ce2 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,30 @@ +package com.techcourse.service; + +import com.techcourse.domain.User; +import org.springframework.transaction.support.TransactionTemplate; + +public class TxUserService implements UserService { + + private final UserService userService; + private final TransactionTemplate transactionTemplate; + + public TxUserService(UserService userService, TransactionTemplate transactionTemplate) { + this.userService = userService; + this.transactionTemplate = transactionTemplate; + } + + @Override + public User findById(final long id) { + return transactionTemplate.execute(() -> userService.findById(id)); + } + + @Override + public void insert(final User user) { + transactionTemplate.execute(() -> userService.insert(user)); + } + + @Override + public void changePassword(final long id, final String newPassword, final String createBy) { + transactionTemplate.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 4294bef5a7..b14dbcacbf 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,81 +1,12 @@ package com.techcourse.service; -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 org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import javax.sql.DataSource; -import java.sql.SQLException; +public interface UserService { -public class UserService { + User findById(final long id); - private static final Logger log = LoggerFactory.getLogger(UserService.class); + void insert(final User user); - private final UserDao userDao; - private final UserHistoryDao userHistoryDao; - private final DataSource dataSource = DataSourceConfig.getInstance(); - - public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { - this.userDao = userDao; - this.userHistoryDao = userHistoryDao; - } - - public User findById(final long id) { - try (final var conn = dataSource.getConnection()) { - try { - conn.setAutoCommit(false); - - return userDao.findById(conn, id); - } catch (Exception e) { - conn.rollback(); - } finally { - conn.setAutoCommit(true); - } - conn.commit(); - } catch (SQLException e) { - log.info(e.getMessage()); - } - throw new RuntimeException(); - } - - public void insert(final User user) { - try (final var conn = dataSource.getConnection()) { - try { - conn.setAutoCommit(false); - - userDao.insert(conn, user); - } catch (Exception e) { - conn.rollback(); - } finally { - conn.setAutoCommit(true); - } - conn.commit(); - } catch (SQLException e) { - log.info(e.getMessage()); - } - } - - public void changePassword(final long id, final String newPassword, final String createBy) { - try (final var conn = dataSource.getConnection()) { - try { - conn.setAutoCommit(false); - - final var user = findById(id); - user.changePassword(newPassword); - userDao.update(conn, user); - userHistoryDao.log(conn, new UserHistory(user, createBy)); - } catch (Exception e) { - conn.rollback(); - } finally { - conn.setAutoCommit(true); - } - conn.commit(); - } catch (SQLException e) { - log.info(e.getMessage()); - } - } + void changePassword(final long id, final String newPassword, final String createBy); } diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index e9afa2cd62..1fa673a2cd 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -35,19 +35,19 @@ void setup() { userDao = new UserDao(new JdbcTemplate(DataSourceConfig.getInstance())); final var user = new User("gugu", "password", "hkkang@woowahan.com"); - userDao.insert(conn, user); + userDao.insert(user); } @Test void findAll() { - final var users = userDao.findAll(conn); + final var users = userDao.findAll(); assertThat(users).isNotEmpty(); } @Test void findById() { - final var user = userDao.findById(conn, 1L); + final var user = userDao.findById(1L); assertThat(user.getAccount()).isEqualTo("gugu"); } @@ -55,7 +55,7 @@ void findById() { @Test void findByAccount() { final var account = "gugu"; - final var user = userDao.findByAccount(conn, account); + final var user = userDao.findByAccount(account); assertThat(user.getAccount()).isEqualTo(account); } @@ -64,9 +64,9 @@ void findByAccount() { void insert() { final var account = "insert-gugu"; final var user = new User(account, "password", "hkkang@woowahan.com"); - userDao.insert(conn, user); + userDao.insert(user); - final var actual = userDao.findById(conn, 2L); + final var actual = userDao.findById(2L); assertThat(actual.getAccount()).isEqualTo(account); } @@ -74,29 +74,29 @@ void insert() { @Test void update() { final var newPassword = "password99"; - final var user = userDao.findById(conn, 1L); + final var user = userDao.findById(1L); user.changePassword(newPassword); - userDao.update(conn, user); + userDao.update(user); - final var actual = userDao.findById(conn, 1L); + final var actual = userDao.findById(1L); assertThat(actual.getPassword()).isEqualTo(newPassword); } @Test void deleteAll() { - userDao.deleteAll(conn); + userDao.deleteAll(); - List users = userDao.findAll(conn); + List users = userDao.findAll(); assertThat(users).isEmpty(); } @Test void findObjectReturnNull() { - userDao.deleteAll(conn); + userDao.deleteAll(); - assertThatThrownBy(() -> userDao.findById(conn, 1L)) + assertThatThrownBy(() -> userDao.findById(1L)) .isInstanceOf(DataAccessException.class); } } diff --git a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java index 7a06745d90..2ee12b195f 100644 --- a/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java +++ b/app/src/test/java/com/techcourse/service/MockUserHistoryDao.java @@ -5,8 +5,6 @@ import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; -import java.sql.Connection; - public class MockUserHistoryDao extends UserHistoryDao { public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) { @@ -14,7 +12,7 @@ public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) { } @Override - public void log(Connection conn, final UserHistory userHistory) { + public void log(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/TxUserServiceTest.java similarity index 60% rename from app/src/test/java/com/techcourse/service/UserServiceTest.java rename to app/src/test/java/com/techcourse/service/TxUserServiceTest.java index 04a9e4cec9..ab34802bd7 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/TxUserServiceTest.java @@ -7,31 +7,35 @@ import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; - -import java.sql.SQLException; +import org.springframework.transaction.support.TransactionManager; +import org.springframework.transaction.support.TransactionTemplate; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; -class UserServiceTest { +class TxUserServiceTest { private JdbcTemplate jdbcTemplate; private UserDao userDao; @BeforeEach - void setUp() throws SQLException { + void setUp() { this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); this.userDao = new UserDao(jdbcTemplate); DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); final var user = new User("gugu", "password", "hkkang@woowahan.com"); - userDao.insert(DataSourceConfig.getInstance().getConnection(), user); + userDao.insert(user); } @Test void testChangePassword() { + final var transactionTemplate = new TransactionTemplate(new TransactionManager(DataSourceConfig.getInstance())); final var userHistoryDao = new UserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + final var appUserService = new AppUserService(userDao, userHistoryDao); + final var userService = new TxUserService(appUserService, transactionTemplate); final var newPassword = "qqqqq"; final var createBy = "gugu"; @@ -43,15 +47,19 @@ void testChangePassword() { } @Test - void testTransactionRollback() { + void testTransactionRollback2() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + // 애플리케이션 서비스 + final var appUserService = new AppUserService(userDao, userHistoryDao); + // 트랜잭션 서비스 추상화 + final var userService = new TxUserService(appUserService, new TransactionTemplate(new TransactionManager(DataSourceConfig.getInstance()))); final var newPassword = "newPassword"; final var createBy = "gugu"; // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. - userService.changePassword(1L, newPassword, createBy); + assertThrows(DataAccessException.class, + () -> userService.changePassword(1L, newPassword, createBy)); final var actual = userService.findById(1L); 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 8fa5aa9594..f7783ca691 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.jdbc.exception.DataAccessException; import org.springframework.jdbc.exception.PreparedStatementExecuteException; @@ -23,25 +24,25 @@ public JdbcTemplate(final DataSource dataSource) { this.dataSource = dataSource; } - public void update(Connection conn, String sql, Object... args) { - execute(conn, sql, PreparedStatement::execute, args); + public void update(String sql, Object... args) { + execute(sql, PreparedStatement::execute, args); log.debug("query : {}", sql); } - public T queryForObject(Connection conn, String sql, RowMapper rowMapper, Object... args) { - return executeForObject(conn, sql, rowMapper, args); + public T queryForObject(String sql, RowMapper rowMapper, Object... args) { + return executeForObject(sql, rowMapper, args); } - public List query(Connection conn, String sql, RowMapper rowMapper, Object... args) { - return executeForObjects(conn, sql, rowMapper, args); + public List query(String sql, RowMapper rowMapper, Object... args) { + return executeForObjects(sql, rowMapper, args); } - private List executeForObjects(Connection conn, String sql, RowMapper rowMapper, Object[] args) { - return execute(conn, sql, preparedStatement -> getObjects(preparedStatement, rowMapper), args); + private List executeForObjects(String sql, RowMapper rowMapper, Object[] args) { + return execute(sql, preparedStatement -> getObjects(preparedStatement, rowMapper), args); } - private T executeForObject(Connection conn, String sql, RowMapper rowMapper, Object[] args) { - return execute(conn, sql, preparedStatement -> getObject(preparedStatement, rowMapper), args); + private T executeForObject(String sql, RowMapper rowMapper, Object[] args) { + return execute(sql, preparedStatement -> getObject(preparedStatement, rowMapper), args); } private T getObject(PreparedStatement preparedStatement, RowMapper rowMapper) throws SQLException { @@ -63,11 +64,11 @@ private List getObjects(PreparedStatement preparedStatement, RowMapper } private T execute( - Connection conn, String sql, PreparedStatementExecutor preparedStatementExecutor, Object... args ) { + Connection conn = DataSourceUtils.getConnection(dataSource); try (var pstmt = conn.prepareStatement(sql)) { setParams(pstmt, args); @@ -75,6 +76,8 @@ private T execute( } catch (SQLException e) { log.error(e.getMessage()); throw new PreparedStatementExecuteException(); + } finally { + DataSourceUtils.releaseConnection(conn, 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 3c40bfec52..3626b1af9a 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -1,6 +1,7 @@ package org.springframework.jdbc.datasource; -import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.jdbc.exception.CannotGetJdbcConnectionException; +import org.springframework.transaction.support.ConnectionHolder; import org.springframework.transaction.support.TransactionSynchronizationManager; import javax.sql.DataSource; @@ -10,27 +11,35 @@ // 4단계 미션에서 사용할 것 public abstract class DataSourceUtils { - private DataSourceUtils() {} + private DataSourceUtils() { + } public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { - Connection connection = TransactionSynchronizationManager.getResource(dataSource); - if (connection != null) { - return connection; + ConnectionHolder connectionHolder = TransactionSynchronizationManager.getResource(dataSource); + if (connectionHolder != null) { + return connectionHolder.getConnection(); } try { - connection = dataSource.getConnection(); - TransactionSynchronizationManager.bindResource(dataSource, connection); + Connection connection = dataSource.getConnection(); + TransactionSynchronizationManager.bindResource(dataSource, new ConnectionHolder(connection)); return connection; } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex); } } - public static void releaseConnection(Connection connection, DataSource dataSource) { + public static void releaseConnection(final Connection connection, final DataSource dataSource) { + final ConnectionHolder connectionHolder = TransactionSynchronizationManager.getResource(dataSource); + if (connectionHolder.isTransactionActive()) { + return; + } try { - connection.close(); - } catch (SQLException ex) { + final ConnectionHolder unbindResource = TransactionSynchronizationManager.unbindResource(dataSource); + if (unbindResource.isSameConnection(connection)) { + connection.close(); + } + } catch (final SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection"); } } diff --git a/jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java b/jdbc/src/main/java/org/springframework/jdbc/exception/CannotGetJdbcConnectionException.java similarity index 73% rename from jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java rename to jdbc/src/main/java/org/springframework/jdbc/exception/CannotGetJdbcConnectionException.java index c5c3bcd010..bc4d37b587 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java +++ b/jdbc/src/main/java/org/springframework/jdbc/exception/CannotGetJdbcConnectionException.java @@ -1,8 +1,8 @@ -package org.springframework.jdbc; +package org.springframework.jdbc.exception; import java.sql.SQLException; -public class CannotGetJdbcConnectionException extends RuntimeException { +public class CannotGetJdbcConnectionException extends TransactionException { public CannotGetJdbcConnectionException(String msg) { super(msg); diff --git a/jdbc/src/main/java/org/springframework/jdbc/exception/DataAccessException.java b/jdbc/src/main/java/org/springframework/jdbc/exception/DataAccessException.java index 345b139089..5abb296b78 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/exception/DataAccessException.java +++ b/jdbc/src/main/java/org/springframework/jdbc/exception/DataAccessException.java @@ -1,4 +1,4 @@ package org.springframework.jdbc.exception; -public class DataAccessException extends RuntimeException { +public class DataAccessException extends TransactionException { } diff --git a/jdbc/src/main/java/org/springframework/jdbc/exception/PreparedStatementExecuteException.java b/jdbc/src/main/java/org/springframework/jdbc/exception/PreparedStatementExecuteException.java index 00e97a0b2e..06cafdab48 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/exception/PreparedStatementExecuteException.java +++ b/jdbc/src/main/java/org/springframework/jdbc/exception/PreparedStatementExecuteException.java @@ -1,5 +1,5 @@ package org.springframework.jdbc.exception; -public class PreparedStatementExecuteException extends RuntimeException { +public class PreparedStatementExecuteException extends TransactionException { } diff --git a/jdbc/src/main/java/org/springframework/jdbc/exception/ResultSetMappingException.java b/jdbc/src/main/java/org/springframework/jdbc/exception/ResultSetMappingException.java index a348ee9e10..03809b7717 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/exception/ResultSetMappingException.java +++ b/jdbc/src/main/java/org/springframework/jdbc/exception/ResultSetMappingException.java @@ -1,5 +1,5 @@ package org.springframework.jdbc.exception; -public class ResultSetMappingException extends RuntimeException { +public class ResultSetMappingException extends TransactionException { } diff --git a/jdbc/src/main/java/org/springframework/jdbc/exception/TransactionException.java b/jdbc/src/main/java/org/springframework/jdbc/exception/TransactionException.java new file mode 100644 index 0000000000..8d491dd367 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/jdbc/exception/TransactionException.java @@ -0,0 +1,15 @@ +package org.springframework.jdbc.exception; + +public class TransactionException extends RuntimeException { + + public TransactionException() { + } + + public TransactionException(String message) { + super(message); + } + + public TransactionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jdbc/src/main/java/org/springframework/transaction/support/ConnectionHolder.java b/jdbc/src/main/java/org/springframework/transaction/support/ConnectionHolder.java new file mode 100644 index 0000000000..d58cf16c5c --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/ConnectionHolder.java @@ -0,0 +1,55 @@ +package org.springframework.transaction.support; + +import org.springframework.jdbc.exception.TransactionException; + +import java.sql.Connection; + +public class ConnectionHolder { + + private final Connection connection; + private boolean isTransactionActive; + + public ConnectionHolder(Connection connection) { + this.connection = connection; + } + + public void commit() { + try { + connection.commit(); + } catch (Exception e) { + throw new TransactionException(e.getMessage(), e); + } + } + + public void rollback() { + try { + connection.rollback(); + } catch (Exception e) { + throw new TransactionException(e.getMessage(), e); + } + } + + public void close() { + try { + connection.close(); + } catch (Exception e) { + throw new TransactionException(e.getMessage(), e); + } + } + + public Connection getConnection() { + return connection; + } + + public void setIsTransactionActive(boolean isTransactionActive) { + this.isTransactionActive = isTransactionActive; + } + + public boolean isTransactionActive() { + return isTransactionActive; + } + + public boolean isSameConnection(Connection connection) { + return this.connection == connection; + } +} 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..c48a377994 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionManager.java @@ -0,0 +1,40 @@ +package org.springframework.transaction.support; + +import org.springframework.jdbc.exception.TransactionException; + +import javax.sql.DataSource; +import java.sql.Connection; + +public class TransactionManager { + + private final DataSource dataSource; + + public TransactionManager(DataSource dataSource) { + this.dataSource = dataSource; + } + + public void createConnection() { + try { + Connection connection = dataSource.getConnection(); + ConnectionHolder connectionHolder = new ConnectionHolder(connection); + connection.setAutoCommit(false); + connectionHolder.setIsTransactionActive(true); + TransactionSynchronizationManager.bindResource(dataSource, connectionHolder); + } catch (Exception e) { + throw new TransactionException(e.getMessage(), e); + } + } + + public void commit() { + TransactionSynchronizationManager.getResource(dataSource).commit(); + } + + public void rollback() { + TransactionSynchronizationManager.getResource(dataSource).rollback(); + } + + public void close() { + TransactionSynchronizationManager.getResource(dataSource).close(); + TransactionSynchronizationManager.unbindResource(dataSource); + } +} 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..b859975950 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,25 @@ package org.springframework.transaction.support; import javax.sql.DataSource; -import java.sql.Connection; +import java.util.HashMap; import java.util.Map; public abstract class TransactionSynchronizationManager { - private static final ThreadLocal> resources = new ThreadLocal<>(); + private static final ThreadLocal> resources = ThreadLocal.withInitial(HashMap::new); - private TransactionSynchronizationManager() {} + private TransactionSynchronizationManager() { + } - public static Connection getResource(DataSource key) { - return null; + public static ConnectionHolder getResource(DataSource key) { + return resources.get().get(key); } - public static void bindResource(DataSource key, Connection value) { + public static void bindResource(DataSource key, ConnectionHolder value) { + resources.get().put(key, value); } - public static Connection unbindResource(DataSource key) { - return null; + public static ConnectionHolder unbindResource(DataSource key) { + return resources.get().remove(key); } } diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java new file mode 100644 index 0000000000..36271c1a2a --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java @@ -0,0 +1,40 @@ +package org.springframework.transaction.support; + +import org.springframework.jdbc.exception.TransactionException; + +import java.util.function.Supplier; + +public class TransactionTemplate { + + private final TransactionManager transactionManager; + + public TransactionTemplate(TransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + private T executeWithTransaction(Supplier supplier) { + try { + transactionManager.createConnection(); + T result = supplier.get(); + transactionManager.commit(); + return result; + } catch (TransactionException ex) { + transactionManager.rollback(); + throw ex; + } finally { + transactionManager.close(); + } + } + + public void execute(Runnable runnable) { + executeWithTransaction( + () -> { + runnable.run(); + return null; + }); + } + + public T execute(Supplier supplier) { + return executeWithTransaction(supplier); + } +} diff --git a/study/src/main/java/aop/config/DataSourceConfig.java b/study/src/main/java/aop/config/DataSourceConfig.java index b27245b932..d64fc2ca2b 100644 --- a/study/src/main/java/aop/config/DataSourceConfig.java +++ b/study/src/main/java/aop/config/DataSourceConfig.java @@ -14,6 +14,7 @@ public class DataSourceConfig { public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) + .setName("test;DB_CLOSE_DELAY=-1;MODE=MYSQL;") .addScript("classpath:schema.sql") .build(); } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 84c31c77b5..b9451e1013 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -1,5 +1,6 @@ spring: jpa: + open-in-view: false show-sql: true generate-ddl: true hibernate: