diff --git a/app/build.gradle b/app/build.gradle index 00444e6f91..514e7fd4f3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,6 +27,7 @@ dependencies { implementation "org.apache.commons:commons-lang3:3.13.0" implementation "com.fasterxml.jackson.core:jackson-databind:2.15.2" implementation "com.h2database:h2:2.2.220" + implementation 'com.zaxxer:HikariCP:5.0.1' testImplementation "org.assertj:assertj-core:3.24.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.2" diff --git a/app/src/main/java/com/techcourse/config/DataSourceConfig.java b/app/src/main/java/com/techcourse/config/DataSourceConfig.java index 183b03b4ce..7aa79113d7 100644 --- a/app/src/main/java/com/techcourse/config/DataSourceConfig.java +++ b/app/src/main/java/com/techcourse/config/DataSourceConfig.java @@ -1,7 +1,8 @@ package com.techcourse.config; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; import java.util.Objects; -import org.h2.jdbcx.JdbcDataSource; public class DataSourceConfig { @@ -9,17 +10,18 @@ public class DataSourceConfig { public static javax.sql.DataSource getInstance() { if (Objects.isNull(INSTANCE)) { - INSTANCE = createJdbcDataSource(); + INSTANCE = createHikariDataSource(); } return INSTANCE; } - private static JdbcDataSource createJdbcDataSource() { - final var jdbcDataSource = new JdbcDataSource(); - jdbcDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;"); - jdbcDataSource.setUser(""); - jdbcDataSource.setPassword(""); - return jdbcDataSource; + private static HikariDataSource createHikariDataSource() { + final var hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;"); + hikariConfig.setUsername(""); + hikariConfig.setPassword(""); + + return new HikariDataSource(hikariConfig); } private DataSourceConfig() {} diff --git a/app/src/main/java/com/techcourse/support/jdbc/init/DataSourceConnectionManager.java b/app/src/main/java/com/techcourse/support/jdbc/init/PooledDataSourceConnectionManager.java similarity index 57% rename from app/src/main/java/com/techcourse/support/jdbc/init/DataSourceConnectionManager.java rename to app/src/main/java/com/techcourse/support/jdbc/init/PooledDataSourceConnectionManager.java index bfffc10cc9..b12b952c82 100644 --- a/app/src/main/java/com/techcourse/support/jdbc/init/DataSourceConnectionManager.java +++ b/app/src/main/java/com/techcourse/support/jdbc/init/PooledDataSourceConnectionManager.java @@ -1,18 +1,25 @@ package com.techcourse.support.jdbc.init; import com.techcourse.config.DataSourceConfig; +import com.zaxxer.hikari.HikariDataSource; import java.sql.Connection; import java.sql.SQLException; -import javax.sql.DataSource; import org.springframework.jdbc.CannotGetJdbcConnectionException; import org.springframework.jdbc.core.ConnectionManager; -public class DataSourceConnectionManager implements ConnectionManager { +public class PooledDataSourceConnectionManager implements ConnectionManager { + private final HikariDataSource dataSource; + + public PooledDataSourceConnectionManager() { + this.dataSource = (HikariDataSource) DataSourceConfig.getInstance(); + } + + @Override public Connection getConnection() throws CannotGetJdbcConnectionException { try { - DataSource dataSource = DataSourceConfig.getInstance(); - return dataSource.getConnection(); + final var connection = dataSource.getConnection(); + return connection; } catch (final SQLException exception) { throw new CannotGetJdbcConnectionException("Datasource connection error:" + exception.getMessage()); } diff --git a/app/src/main/resources/logback.xml b/app/src/main/resources/logback.xml index c127b484d4..b909459e6e 100644 --- a/app/src/main/resources/logback.xml +++ b/app/src/main/resources/logback.xml @@ -12,4 +12,8 @@ + + + + diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index d7dee66a75..70f4645a68 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -4,7 +4,7 @@ import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.User; -import com.techcourse.support.jdbc.init.DataSourceConnectionManager; +import com.techcourse.support.jdbc.init.PooledDataSourceConnectionManager; import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -18,7 +18,7 @@ class UserDaoTest { void setup() { DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); - userDao = new UserDao(new JdbcTemplate(new DataSourceConnectionManager())); + userDao = new UserDao(new JdbcTemplate(new PooledDataSourceConnectionManager())); final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao.insert(user); } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index bbbddecdc1..f320478955 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -7,7 +7,7 @@ import com.techcourse.dao.UserDao; import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; -import com.techcourse.support.jdbc.init.DataSourceConnectionManager; +import com.techcourse.support.jdbc.init.PooledDataSourceConnectionManager; import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -23,7 +23,7 @@ class UserServiceTest { @BeforeEach void setUp() { - this.jdbcTemplate = new JdbcTemplate(new DataSourceConnectionManager()); + this.jdbcTemplate = new JdbcTemplate(new PooledDataSourceConnectionManager()); this.userDao = new UserDao(jdbcTemplate); DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); diff --git a/jdbc/build.gradle b/jdbc/build.gradle index 83f293f626..e1432935a9 100644 --- a/jdbc/build.gradle +++ b/jdbc/build.gradle @@ -14,6 +14,7 @@ dependencies { implementation "org.reflections:reflections:0.10.2" implementation "org.apache.commons:commons-lang3:3.13.0" implementation "ch.qos.logback:logback-classic:1.2.12" + implementation "com.h2database:h2:2.2.220" testImplementation "org.assertj:assertj-core:3.24.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.2" diff --git a/jdbc/src/main/java/org/springframework/SqlQueryException.java b/jdbc/src/main/java/org/springframework/jdbc/SqlQueryException.java similarity index 90% rename from jdbc/src/main/java/org/springframework/SqlQueryException.java rename to jdbc/src/main/java/org/springframework/jdbc/SqlQueryException.java index e315b0ce00..585e4c704b 100644 --- a/jdbc/src/main/java/org/springframework/SqlQueryException.java +++ b/jdbc/src/main/java/org/springframework/jdbc/SqlQueryException.java @@ -1,4 +1,4 @@ -package org.springframework; +package org.springframework.jdbc; public class SqlQueryException extends RuntimeException { diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java b/jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java new file mode 100644 index 0000000000..bac8375457 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java @@ -0,0 +1,12 @@ +package org.springframework.jdbc.core; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@FunctionalInterface +public interface ConnectionCallback { + + T doInConnection(Connection connection, PreparedStatement preparedStatement) throws SQLException; + +} diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 4db10da224..b6f5bc8cf2 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -8,7 +8,7 @@ import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.SqlQueryException; +import org.springframework.jdbc.SqlQueryException; public class JdbcTemplate { @@ -16,56 +16,74 @@ public class JdbcTemplate { private final ConnectionManager connectionManager; - public JdbcTemplate(ConnectionManager connectionManager) { + public JdbcTemplate(final ConnectionManager connectionManager) { this.connectionManager = connectionManager; } - public void executeUpdate(String query, Object... parameters) { - try (final Connection connection = connectionManager.getConnection(); - final PreparedStatement preparedStatement = connection.prepareStatement(query)) { - log.info("query: {}", query); - setParameters(preparedStatement, parameters); - preparedStatement.executeUpdate(); - } catch (SQLException exception) { - throw new SqlQueryException(exception.getMessage(), query); - } + public int executeUpdate(final String query, final Object... parameters) { + return execute(query, (connection, preparedStatement) -> preparedStatement.executeUpdate(), parameters); } - public T executeQueryForObject(String query, RowMapper rowMapper, Object... parameters) { - try (final Connection connection = connectionManager.getConnection(); - final PreparedStatement preparedStatement = connection.prepareStatement(query); - final ResultSet resultSet = executePreparedStatementQuery(preparedStatement, parameters)) { - log.info("query: {}", query); + public T executeQueryForObject( + final String query, + final RowMapper rowMapper, + final Object... parameters + ) { + final ResultSetExtractor resultSetExtractor = resultSet -> { if (resultSet.next()) { return rowMapper.mapRow(resultSet); } return null; - } catch (SQLException exception) { - throw new SqlQueryException(exception.getMessage(), query); - } + }; + + return executeQuery(query, resultSetExtractor, parameters); } - public List executeQueryForList(String query, RowMapper rowMapper, Object... parameters) { - try (final Connection connection = connectionManager.getConnection(); - final PreparedStatement preparedStatement = connection.prepareStatement(query); - final ResultSet resultSet = executePreparedStatementQuery(preparedStatement, parameters)) { - log.info("query: {}", query); + public List executeQueryForList( + final String query, + final RowMapper rowMapper, + final Object... parameters + ) { + final ResultSetExtractor> resultSetExtractor = resultSet -> { final List results = new ArrayList<>(); while (resultSet.next()) { results.add(rowMapper.mapRow(resultSet)); } return results; + }; + + return executeQuery(query, resultSetExtractor, parameters); + } + + public T executeQuery( + final String query, + final ResultSetExtractor resultSetExtractor, + final Object... parameters + ) { + return execute(query, (connection, preparedStatement) -> { + try (final ResultSet resultSet = preparedStatement.executeQuery()) { + return resultSetExtractor.extract(resultSet); + } + }, parameters); + } + + private T execute( + final String query, + final ConnectionCallback callback, + final Object... parameters + ) { + try (final Connection connection = connectionManager.getConnection(); + final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + log.info("query: {}", query); + setParameters(preparedStatement, parameters); + return callback.doInConnection(connection, preparedStatement); } catch (SQLException exception) { throw new SqlQueryException(exception.getMessage(), query); } } - private ResultSet executePreparedStatementQuery(PreparedStatement preparedStatement, Object... parameters) throws SQLException { - setParameters(preparedStatement, parameters); - return preparedStatement.executeQuery(); - } - - private void setParameters(PreparedStatement preparedStatement, Object... parameters) throws SQLException { + private void setParameters(final PreparedStatement preparedStatement, final Object... parameters) + throws SQLException { for (int index = 1; index <= parameters.length; index++) { preparedStatement.setString(index, String.valueOf(parameters[index - 1])); } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/ResultSetExtractor.java b/jdbc/src/main/java/org/springframework/jdbc/core/ResultSetExtractor.java new file mode 100644 index 0000000000..e95e71b393 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/jdbc/core/ResultSetExtractor.java @@ -0,0 +1,11 @@ +package org.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.SQLException; + + +public interface ResultSetExtractor { + + T extract(ResultSet resultSet) throws SQLException; + +} diff --git a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java index d777c8becf..d7ecce16c1 100644 --- a/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java +++ b/jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java @@ -1,5 +1,120 @@ package nextstep.jdbc; +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.jdbc.core.ConnectionManager; +import org.springframework.jdbc.core.JdbcTemplate; + class JdbcTemplateTest { + private JdbcTemplate jdbcTemplate; + + @BeforeEach + void setUp() { + TestConnectionManager connectionManager = new TestConnectionManager(); + this.jdbcTemplate = new JdbcTemplate(connectionManager); + try (Connection conn = connectionManager.getConnection()) { + conn.setAutoCommit(true); + try (Statement stmt = conn.createStatement()) { + stmt.execute("DROP TABLE IF EXISTS users;"); + stmt.execute( + "CREATE TABLE IF NOT EXISTS users (id INT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(100) NOT NULL)"); + stmt.executeUpdate("INSERT INTO users (email) VALUES ('hkkang@woowahan.com')"); + conn.setAutoCommit(false); + } + } catch (SQLException e) { + throw new AssertionError(e); + } + } + + @Test + @DisplayName("쓰기 작업(생성, 수정, 삭제)을 하는 쿼리를 실행한다.") + void executeUpdate() { + // given + failIfExceptionThrownBy(() -> { + // when + final var updated = jdbcTemplate.executeUpdate("insert into users (email) values (?)", "doy@gmail.com"); + + // then + assertThat(updated).isOne(); + }); + } + + @Test + @DisplayName("단건 레코드를 조회하는 쿼리를 실행한다.") + void executeQueryForObject() { + // given + failIfExceptionThrownBy(() -> { + // when + User user = jdbcTemplate.executeQueryForObject( + "select email from users where id = ?", + resultSet -> new User(resultSet.getString(1)), 1L + ); + + // then + assertThat(user).usingRecursiveComparison() + .comparingOnlyFields("hkkang@woowahan.com"); + }); + } + + @Test + @DisplayName("다건 레코드를 조회하는 쿼리를 실행한다.") + void executeQueryForList() { + // given + jdbcTemplate.executeUpdate("insert into users (email) values (?)", "doy@gmail.com"); + + failIfExceptionThrownBy(() -> { + // when + List users = jdbcTemplate.executeQueryForList( + "select email from users", + resultSet -> new User(resultSet.getString(1)) + ); + + // then + assertThat(users).extracting("email") + .containsExactlyInAnyOrder("hkkang@woowahan.com", "doy@gmail.com"); + }); + } + + private void failIfExceptionThrownBy(Runnable test) { + try { + test.run(); + } catch (Exception exception) { + Assertions.fail(exception); + } + } + + private static class User { + + private final String email; + + public User(final String email) { + this.email = email; + } + + } + + private static class TestConnectionManager implements ConnectionManager { + + @Override + public Connection getConnection() throws CannotGetJdbcConnectionException { + try { + final var jdbcDataSource = new JdbcDataSource(); + jdbcDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;"); + return jdbcDataSource.getConnection(); + } catch (SQLException sqlException) { + throw new AssertionError(); + } + } + } }