Skip to content
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

Merged
merged 3 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 21 additions & 90 deletions app/src/main/java/com/techcourse/dao/UserDao.java
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);
}
}
49 changes: 5 additions & 44 deletions app/src/main/java/com/techcourse/dao/UserHistoryDao.java
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());
}
}
98 changes: 96 additions & 2 deletions jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java
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 {

Expand All @@ -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 -> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PreparedStatementCreator를 사용해주셨네요! 생성의 책임을 다른 객체로 넘긴 것 좋은 것 같아요 👍👍
다만 궁금한 점이 있는데, 해당 객체는 매번 생성되어야 하는 이유가 있을까요? 현재 preparedstatement를 생성하는 역할만 수행하는데, 필드로 두지 않으신 이유가 궁금해요 :)

Copy link
Author

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를 만들어줬었는데 폐기하면서 이를 꼼꼼하게 수정하지 않았네요! 😅

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");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

result == null 인 경우는 언제일까요?

Copy link
Author

@poi1649 poi1649 Sep 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

템플릿 콜백패턴을 위해 사용한 execute 메서드에서 Null 을 반환하는 경우가 있어 @Nullable 어노테이션을 붙였는데, 이 때문에 그냥 사용할 경우 경고가 표시되어 이런 검사로직을 넣게 됐네요~ 그렇지만 updateCount는 executeUpdate시에만 사용되고 이 메서드는 null을 반환하지 않으니 supressedWarning을 붙여주는게 더 적절한 사용방식일 수 있겠군요

}
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 {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 상황에서는 query 메서드의 기본 리턴 값을 List로 두는 것과, queryForList 라는 메서드를 추가하는 것 중 어떤게 나은 것 같으신가요? 이 부분은 정답이 없는 것 같아서 포이의 의견이 궁금하네요 😆

Copy link
Author

Choose a reason for hiding this comment

The 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를 사용자 정의를 통해 지정해주지 못하고, 특정 컬럼에 대한 반환타입만 지정하거나, 전부 컬럼이름(key, String) : 컬럼 값(value, Obejct) 방식으로 전달해줄 수 밖에 없습니다.

제가 이번 미션에서 만들고자 하는 메서드는 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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개인 취향이긴 하지만 resultSet은 어떨까요 ㅎㅎ 반영 안 하셔도 됩니다!

Copy link
Author

Choose a reason for hiding this comment

The 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;
}
}
12 changes: 12 additions & 0 deletions jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java
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;
}
Loading
Loading