diff --git a/README.md b/README.md index 721b81d472..83b92f3e3f 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ # JDBC 라이브러리 구현하기 + +개발자는 SQL 쿼리 작성, 쿼리에 전달할 인자, SELECT 구문일 경우 +조회 결과를 추출하는 것만 집중할 수 있도록 라이브러리를 만들자. + +## 힌트 + +- 리팩터링은 UserDaoTest를 활용해 진행한다. +- 중복을 제거하기 위한 라이브러리는 JdbcTemplate 클래스에 구현한다. +- DataSource는 DataSourceConfig 클래스의 getInstance() 메서드를 호출하면 된다. diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index d14c545f34..b27e0f6c74 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -1,6 +1,8 @@ package com.techcourse.dao; +import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.User; +import java.util.ArrayList; import org.springframework.jdbc.core.JdbcTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,111 +13,50 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; +import org.springframework.jdbc.core.RowMapper; public class UserDao { private static final Logger log = LoggerFactory.getLogger(UserDao.class); - private final DataSource dataSource; + private final JdbcTemplate jdbcTemplate; - public UserDao(final DataSource dataSource) { - this.dataSource = dataSource; - } public UserDao(final JdbcTemplate jdbcTemplate) { - this.dataSource = null; + this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); } public void insert(final User user) { final var sql = "insert into users (account, password, email) values (?, ?, ?)"; - - Connection conn = null; - PreparedStatement pstmt = null; - try { - conn = dataSource.getConnection(); - pstmt = conn.prepareStatement(sql); - - log.debug("query : {}", sql); - - pstmt.setString(1, user.getAccount()); - pstmt.setString(2, user.getPassword()); - pstmt.setString(3, user.getEmail()); - 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.execute(sql, user.getAccount(), user.getPassword(), user.getEmail()); } public void update(final User user) { - // todo + final var sql = "update users set password = ?, email = ?, account = ? where id = ?"; + jdbcTemplate.execute(sql, user.getPassword(), user.getEmail(), user.getAccount(), user.getId()); } public List findAll() { - // todo - return null; + final var sql = "select id, account, password, email from users"; + return jdbcTemplate.query(sql, (rs, rowNum) -> new User(rs.getLong("id"), + rs.getString("account"), + rs.getString("password"), + rs.getString("email"))); } public User findById(final Long id) { final var sql = "select id, account, password, email from users where id = ?"; - - Connection conn = null; - PreparedStatement pstmt = null; - ResultSet rs = null; - try { - conn = dataSource.getConnection(); - pstmt = conn.prepareStatement(sql); - pstmt.setLong(1, id); - rs = pstmt.executeQuery(); - - log.debug("query : {}", sql); - - if (rs.next()) { - return new User( - rs.getLong(1), - rs.getString(2), - rs.getString(3), - rs.getString(4)); - } - return null; - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new RuntimeException(e); - } finally { - try { - if (rs != null) { - rs.close(); - } - } catch (SQLException ignored) {} - - try { - if (pstmt != null) { - pstmt.close(); - } - } catch (SQLException ignored) {} - - try { - if (conn != null) { - conn.close(); - } - } catch (SQLException ignored) {} - } + return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new User(rs.getLong("id"), + rs.getString("account"), + rs.getString("password"), + rs.getString("email")), id); } public User findByAccount(final String account) { - // todo - return null; + final var sql = "select id, account, password, email from users where account = ?"; + return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new User(rs.getLong("id"), + rs.getString("account"), + rs.getString("password"), + rs.getString("email")), account); } } diff --git a/app/src/test/java/com/techcourse/dao/UserDaoTest.java b/app/src/test/java/com/techcourse/dao/UserDaoTest.java index 773d7faf82..f692842434 100644 --- a/app/src/test/java/com/techcourse/dao/UserDaoTest.java +++ b/app/src/test/java/com/techcourse/dao/UserDaoTest.java @@ -3,24 +3,32 @@ import com.techcourse.config.DataSourceConfig; import com.techcourse.domain.User; import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.JdbcTemplate; import static org.assertj.core.api.Assertions.assertThat; class UserDaoTest { private UserDao userDao; + private JdbcTemplate jdbcTemplate; @BeforeEach void setup() { DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); - - userDao = new UserDao(DataSourceConfig.getInstance()); + jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); + userDao = new UserDao(jdbcTemplate); final var user = new User("gugu", "password", "hkkang@woowahan.com"); userDao.insert(user); } + @AfterEach + void tearDown() { + jdbcTemplate.execute("truncate table users restart identity"); + } + @Test void findAll() { final var users = userDao.findAll(); @@ -66,4 +74,5 @@ void update() { assertThat(actual.getPassword()).isEqualTo(newPassword); } + } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 52a0d30a17..4da1f3afc9 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -1,5 +1,11 @@ package org.springframework.jdbc.core; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,4 +20,53 @@ public class JdbcTemplate { public JdbcTemplate(final DataSource dataSource) { this.dataSource = dataSource; } + + public void execute(String sql, Object... parameters) { + try (final Connection conn = dataSource.getConnection(); + final PreparedStatement pstmt = conn.prepareStatement(sql)) { + log.debug("query : {}", sql); + + for (int i = 0; i < parameters.length; i++) { + pstmt.setObject(i + 1, parameters[i]); + } + + pstmt.executeUpdate(); + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e); + } + } + + public T queryForObject(String sql, RowMapper rowMapper, Object... params) { + List results = query(sql, rowMapper, params); + if (results.size() > 1) { + throw new RuntimeException("too many result. expected 1 but was " + results.size()); + } + if (results.isEmpty()) { + throw new RuntimeException("no result"); + } + return results.get(0); + } + + public List query(String sql, RowMapper rowMapper, Object... parameters) { + try (final Connection conn = dataSource.getConnection(); + final PreparedStatement pstmt = conn.prepareStatement(sql)) { + for (int i = 0; i < parameters.length; i++) { + pstmt.setObject(i + 1, parameters[i]); + } + ResultSet rs = pstmt.executeQuery(); + + log.debug("query : {}", sql); + + final List results = new ArrayList<>(); + while (rs.next()) { + results.add(rowMapper.mapRow(rs, rs.getRow())); + } + return results; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e); + } + } + } diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java b/jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java new file mode 100644 index 0000000000..7fee249381 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * An interface used by {@link JdbcTemplate} for mapping rows of a + * {@link ResultSet} on a per-row basis. Implementations of this + * interface perform the actual work of mapping each row to a result object, + * but don't need to worry about exception handling. + * {@link SQLException SQLExceptions} will be caught and handled + * by the calling JdbcTemplate. + * + *

Typically used either for {@link JdbcTemplate}'s query methods + * or for out parameters of stored procedures. RowMapper objects are + * typically stateless and thus reusable; they are an ideal choice for + * implementing row-mapping logic in a single place. + * + *

Alternatively, consider subclassing + * {@code jdbc.object} package: Instead of working with separate + * JdbcTemplate and RowMapper objects, you can build executable query + * objects (containing row-mapping logic) in that style. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @param the result type + * @see JdbcTemplate + */ +@FunctionalInterface +public interface RowMapper { + + /** + * Implementations must implement this method to map each row of data + * in the ResultSet. This method should not call {@code next()} on + * the ResultSet; it is only supposed to map values of the current row. + * @param rs the ResultSet to map (pre-initialized for the current row) + * @param rowNum the number of the current row + * @return the result object for the current row (maybe {@code null}) + * @throws SQLException if an SQLException is encountered getting + * column values (that is, there's no need to catch SQLException) + */ + T mapRow(ResultSet rs, int rowNum) throws SQLException; + +}