-
Notifications
You must be signed in to change notification settings - Fork 300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[JDBC 라이브러리 구현하기 - 1단계] 포이(김보준) 미션 제출합니다. #269
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,121 +1,52 @@ | ||
package com.techcourse.dao; | ||
|
||
import com.techcourse.domain.User; | ||
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.ResultSet; | ||
import java.sql.SQLException; | ||
import java.util.List; | ||
import javax.sql.DataSource; | ||
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> rowMapper = rs -> new User( | ||
rs.getLong(1), | ||
rs.getString(2), | ||
rs.getString(3), | ||
rs.getString(4) | ||
); | ||
|
||
private final DataSource dataSource; | ||
private final JdbcTemplate jdbcTemplate; | ||
|
||
public UserDao(final DataSource dataSource) { | ||
this.dataSource = dataSource; | ||
this.jdbcTemplate = new JdbcTemplate(dataSource); | ||
} | ||
|
||
public UserDao(final JdbcTemplate jdbcTemplate) { | ||
this.dataSource = null; | ||
this.jdbcTemplate = jdbcTemplate; | ||
} | ||
|
||
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 | ||
public int update(final User user) { | ||
final var sql = "update users set password = ?, email = ? where id = ?"; | ||
return jdbcTemplate.update(sql, user.getPassword(), user.getEmail(), user.getId()); | ||
} | ||
|
||
public List<User> findAll() { | ||
// todo | ||
return null; | ||
final var sql = "select id, account, password, email from users"; | ||
return jdbcTemplate.query(sql, rowMapper); | ||
} | ||
|
||
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, rowMapper, 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, rowMapper, account); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,62 +1,23 @@ | ||
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.execute(sql, userHistory.getUserId(), userHistory.getAccount(), userHistory.getPassword(), userHistory.getEmail(), userHistory.getCreatedAt(), userHistory.getCreateBy()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,15 @@ | ||
package org.springframework.jdbc.core; | ||
|
||
import java.sql.Connection; | ||
import java.sql.PreparedStatement; | ||
import java.sql.SQLException; | ||
import java.util.List; | ||
import javax.annotation.Nullable; | ||
import javax.sql.DataSource; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import javax.sql.DataSource; | ||
import org.springframework.dao.DataAccessException; | ||
import org.springframework.jdbc.CannotGetJdbcConnectionException; | ||
|
||
public class JdbcTemplate { | ||
|
||
|
@@ -14,4 +20,92 @@ public class JdbcTemplate { | |
public JdbcTemplate(final DataSource dataSource) { | ||
this.dataSource = dataSource; | ||
} | ||
|
||
public void execute(final String sql, final Object... params) throws DataAccessException { | ||
log.debug("query : {}", sql); | ||
execute(sql, new PreparedStatementCreator(), ps -> { | ||
doSetValue(params, ps); | ||
ps.execute(); | ||
return null; | ||
}); | ||
} | ||
|
||
public int update(final String sql, final Object... params) throws DataAccessException { | ||
log.debug("query : {}", sql); | ||
return updateCount(execute( | ||
sql, | ||
new PreparedStatementCreator(), | ||
ps -> { | ||
doSetValue(params, ps); | ||
return ps.executeUpdate(); | ||
} | ||
)); | ||
} | ||
|
||
private int updateCount(@Nullable final Integer result) throws DataAccessException { | ||
if (result == null) { | ||
throw new DataAccessException("no update count"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. result == null 인 경우는 언제일까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 템플릿 콜백패턴을 위해 사용한 execute 메서드에서 Null 을 반환하는 경우가 있어 |
||
} | ||
return result; | ||
} | ||
|
||
@Nullable | ||
public <T> T queryForObject(final String sql, final RowMapper<T> rowMapper, final Object... params) throws DataAccessException { | ||
final var results = query(sql, rowMapper, params); | ||
if (results.isEmpty()) { | ||
return null; | ||
} | ||
return results.get(0); | ||
} | ||
|
||
public <T> List<T> query(final String sql, final RowMapper<T> rowMapper, final Object... params) throws DataAccessException { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재 상황에서는 query 메서드의 기본 리턴 값을 List로 두는 것과, queryForList 라는 메서드를 추가하는 것 중 어떤게 나은 것 같으신가요? 이 부분은 정답이 없는 것 같아서 포이의 의견이 궁금하네요 😆 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 음 이 부분에서는 사실 기존의 JDBCTemplate를 의식하지 않는다면 어떤 이름도 상관 없을 것이라는 생각이 들어요! 오히려 List가 더 바람직한 이름이라고 생각하실 분들도 계실 것 같습니다! 그렇지만 기존의 JDBCTemplate를 고려한다면, 두 메서드는 약간의 차이를 갖는데요, JDBCTemplate의 queryForList 메서드를 살펴보면 @Override
public <T> List<T> queryForList(String sql, Class<T> elementType) throws DataAccessException {
return query(sql, getSingleColumnRowMapper(elementType));
}
@Override
public List<Map<String, Object>> queryForList(String sql) throws DataAccessException {
return query(sql, getColumnMapRowMapper());
} 위처럼 SingleColumnRowMapper 나 ColumnMapRowMapper를 사용하고 있는 것을 볼 수 있어요. 제가 이번 미션에서 만들고자 하는 메서드는 RowMapper를 통해 사용자가 지정한 타입의 객체로 각각의 Row를 매핑하여 반환하는 것이기 때문에 queryForList보다는 query가 적합하다는 생각이 드네요! ♞ |
||
log.debug("query : {}", sql); | ||
return results(execute( | ||
sql, | ||
new PreparedStatementCreator(), | ||
ps -> { | ||
doSetValue(params, ps); | ||
final var rs = ps.executeQuery(); | ||
final var extractor = new ResultSetExtractor<>(rs, rowMapper); | ||
return extractor.extractData(); | ||
} | ||
)); | ||
} | ||
|
||
private <T> List<T> results(@Nullable final List<T> results) throws DataAccessException { | ||
if (results == null) { | ||
throw new DataAccessException("no result"); | ||
} | ||
return results; | ||
} | ||
|
||
private void doSetValue(final Object[] params, final PreparedStatement ps) throws SQLException { | ||
for (int i = 1; i <= params.length; i++) { | ||
ps.setObject(i, params[i - 1]); | ||
} | ||
} | ||
|
||
@Nullable | ||
private <T> T execute( | ||
final String sql, | ||
final PreparedStatementCreator psc, | ||
final PreparedStatementCallback<T> action | ||
) throws DataAccessException { | ||
try ( | ||
final var conn = getConnection(); | ||
final var ps = psc.createPreparedStatement(conn, sql) | ||
) { | ||
return action.doInPreparedStatement(ps); | ||
} catch (SQLException e) { | ||
throw new DataAccessException(e); | ||
} | ||
} | ||
|
||
private Connection getConnection() { | ||
try { | ||
return dataSource.getConnection(); | ||
} catch (SQLException e) { | ||
throw new CannotGetJdbcConnectionException(e.getMessage()); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package org.springframework.jdbc.core; | ||
|
||
import java.sql.PreparedStatement; | ||
import java.sql.SQLException; | ||
import javax.annotation.Nullable; | ||
import org.springframework.dao.DataAccessException; | ||
|
||
@FunctionalInterface | ||
public interface PreparedStatementCallback<T> { | ||
|
||
@Nullable | ||
T doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package org.springframework.jdbc.core; | ||
|
||
import java.sql.Connection; | ||
import java.sql.PreparedStatement; | ||
import java.sql.SQLException; | ||
|
||
public class PreparedStatementCreator { | ||
|
||
public PreparedStatement createPreparedStatement(final Connection conn, final String sql) throws SQLException { | ||
return conn.prepareStatement(sql); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package org.springframework.jdbc.core; | ||
|
||
import java.sql.ResultSet; | ||
import java.sql.SQLException; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
public class ResultSetExtractor<T> { | ||
|
||
private final ResultSet rs; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 개인 취향이긴 하지만 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아직은 rs만으로 명확하다고 생각하여 혼동할만한 다른 변수명이 있을 때 바꿔보겠습니다~ |
||
private final RowMapper<T> rowMapper; | ||
|
||
public ResultSetExtractor(final ResultSet rs, final RowMapper<T> rowMapper) { | ||
this.rs = rs; | ||
this.rowMapper = rowMapper; | ||
} | ||
|
||
public List<T> extractData() throws SQLException { | ||
final var results = new ArrayList<T>(); | ||
while (rs.next()) { | ||
results.add(rowMapper.mapRow(rs)); | ||
} | ||
return results; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package org.springframework.jdbc.core; | ||
|
||
import java.sql.ResultSet; | ||
import java.sql.SQLException; | ||
import javax.annotation.Nullable; | ||
|
||
@FunctionalInterface | ||
public interface RowMapper<T> { | ||
|
||
@Nullable | ||
T mapRow(ResultSet rs) throws SQLException; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PreparedStatementCreator
를 사용해주셨네요! 생성의 책임을 다른 객체로 넘긴 것 좋은 것 같아요 👍👍다만 궁금한 점이 있는데, 해당 객체는 매번 생성되어야 하는 이유가 있을까요? 현재 preparedstatement를 생성하는 역할만 수행하는데, 필드로 두지 않으신 이유가 궁금해요 :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
처음에는 preparedStatement 또한 여러 종류가 있고, creator도 이에 따라 다양해 질 수 있을 것 같아 Functional Interface 로 구현하였는데 과한 확장인 것 같아 이를 폐기했습니다.
인터페이스이다 보니까 구현체를 만들어줘야했고, 중복되는 곳에서 사용되는 SimplePreparedStatementCreator를 만들어줬었는데 폐기하면서 이를 꼼꼼하게 수정하지 않았네요! 😅