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단계] 바론(이소민) 미션 제출합니다. #305

Merged
merged 5 commits into from
Oct 1, 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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
# JDBC 라이브러리 구현하기

### 1단계
- [x] JdbcTemplate insert, update, delete, select 쿼리 메서드 구현
- [x] UserDao rowmapper 구현
- [x] UserDao todo 쿼리 로직 구현
116 changes: 24 additions & 92 deletions app/src/main/java/com/techcourse/dao/UserDao.java
Original file line number Diff line number Diff line change
@@ -1,121 +1,53 @@
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 org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

public class UserDao {

private static final Logger log = LoggerFactory.getLogger(UserDao.class);

private final DataSource dataSource;
private static final RowMapper<User> ROW_MAPPER = (rs, count) ->
new User(
rs.getLong("id"),
rs.getString("account"),
rs.getString("password"),
rs.getString("email")
);

public UserDao(final DataSource dataSource) {
this.dataSource = dataSource;
}
private final JdbcTemplate jdbcTemplate;

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) {}
final String sql = "insert into users (account, password, email) values (?, ?, ?)";

try {
if (conn != null) {
conn.close();
}
} catch (SQLException ignored) {}
}
jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail());
}

public void update(final User user) {
// todo
final String sql = "update users set account = ?, password = ?, email = ? where id = ?";

jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId());
}

public List<User> findAll() {
// todo
return null;
final String sql = "select id, account, email, password from users";

return jdbcTemplate.query(sql, ROW_MAPPER);
}

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);
final String sql = "select id, account, password, email from users where id = ?";

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, ROW_MAPPER, id);
}

public User findByAccount(final String account) {
// todo
return null;
final String sql = "select id, account, password, email from users where account = ?";

return jdbcTemplate.queryForObject(sql, ROW_MAPPER, account);
}
}
3 changes: 2 additions & 1 deletion app/src/test/java/com/techcourse/dao/UserDaoTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.techcourse.support.jdbc.init.DatabasePopulatorUtils;
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;

Expand All @@ -16,7 +17,7 @@ class UserDaoTest {
void setup() {
DatabasePopulatorUtils.execute(DataSourceConfig.getInstance());

userDao = new UserDao(DataSourceConfig.getInstance());
userDao = new UserDao(new JdbcTemplate(DataSourceConfig.getInstance()));

Choose a reason for hiding this comment

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

Test는 최대한 건드리지 말라고 했었으니
생성자를 아래와 같이 해도 될것 같아요.

public UserDao(DataSource dataSource) {
    this.jdbcTemplate = new JdbcTemplate(dataSource);
}

Copy link
Author

Choose a reason for hiding this comment

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

헉 이런 요구사항이 있었군요!!!
감사합니다 안그래도 생성자를 어떻게 구성할지 고민하고 있었는데 그렇다면 이레가 제안해주신 방법이 더 좋을 것 같아요 👍 😗

Choose a reason for hiding this comment

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

바론 이 요구사항은 안지켜도 될것 같아요...

final var user = new User("gugu", "password", "[email protected]");
userDao.insert(user);
}
Expand Down
66 changes: 64 additions & 2 deletions jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
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 javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.sql.DataSource;

public class JdbcTemplate {

private static final Logger log = LoggerFactory.getLogger(JdbcTemplate.class);
Expand All @@ -14,4 +19,61 @@ public class JdbcTemplate {
public JdbcTemplate(final DataSource dataSource) {
this.dataSource = dataSource;
}

public <T> List<T> query(final String sql, final RowMapper<T> rowMapper, final Object... args) {
try (final Connection connection = dataSource.getConnection();
final PreparedStatement pstmt = connection.prepareStatement(sql)) {
log.debug("query : {}", sql);

setParamsToPreparedStatement(pstmt, args);

Choose a reason for hiding this comment

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

중복되는 코드 하나로 묶은 것 👍🏻👍🏻


final ResultSet resultSet = pstmt.executeQuery();
final List<T> results = new ArrayList<>();
if (resultSet.next()) {
results.add(rowMapper.mapRow(resultSet, resultSet.getRow()));
}

return results;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

private void setParamsToPreparedStatement(final PreparedStatement pstmt, final Object[] args)
throws SQLException
{
for (int i = 0; i < args.length; i++) {
pstmt.setObject(i + 1, args[i]);
}
}

public <T> T queryForObject(final String sql, final RowMapper<T> rowMapper, final Object... args) {
try (final Connection connection = dataSource.getConnection();
final PreparedStatement pstmt = connection.prepareStatement(sql)) {
log.debug("query : {}", sql);

setParamsToPreparedStatement(pstmt, args);

final ResultSet resultSet = pstmt.executeQuery();
if (resultSet.next()) {
return rowMapper.mapRow(resultSet, resultSet.getRow());
}
return null;

Choose a reason for hiding this comment

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

조회 조건에 해당하는 객체가 없을 경우 null을 반환하는 이유가 뭔가용?
저는 Optional을 사용해서 Dao에서 null인 경우, 예외를 터뜨렸는데,
JdbcTemplate을열어보니
image

1개의 결과를 예상했는데, 결과가 0개일 경우 EmptyResultDataAccessException을 터뜨리더라구요
image

왜 null을 반환하셨는지 이유가 궁금합니당

Copy link
Author

@somsom13 somsom13 Oct 1, 2023

Choose a reason for hiding this comment

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

오.. 사실 2단계 리팩터링 때 저도 Optional로 리팩터링을 할 계획이었는데요 ㅎㅎ

이레 말씀을 들어보니 queryForObject 는 메서드명 자체에서 "단일 객체 하나를 조회한다" 라는 의미를 담고있으니

  1. 조회 결과가 없는 경우
  2. 조회 결과가 하나 보다 많은 경우

에는 예외를 발생시키는 것이 맞을 것 같아요! 위 상황을 모두 고려하면 이 때는 EmptyResultException 이 아닌 단일 객체 조회 불가 와 같은 예외가 되겠지만요 ㅎㅎ

이레는 어떻게 생각하시나요?

Choose a reason for hiding this comment

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

코드에서는
조회 결과가 없는 경우 => emptyResult~
조회 결과가 하나 보다 많은 경우 => IncorrectResultSize~
예외를 터뜨리는데요.
바론의 말처럼,
queryForObject 는 메서드명 자체에서 "단일 객체 하나를 조회한다" 라는 의미를 담고있으니
Optional로 리팩터링하는 것보다, 문제 발생시 내부적으로 dataException을 발생시키는 게 맞아 보입니다.

} catch (SQLException e) {
throw new RuntimeException(e);
}
Comment on lines +62 to +64

Choose a reason for hiding this comment

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

resultSet을 닫아주지 않은 것 같아요!

Choose a reason for hiding this comment

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

바론 덕분에 좋은 사실 알아갑니다.
저도 바론의 리오 리뷰를 읽고 디버깅을 해보았는데요
대체 connection은 언제 닫아주는 걸까요?
try문이 끝나면 바로 JdbcPreparedStatement의 close메서드로 들어가서 connection을 null로만 바꿔주더라고요
image
혹시 바론 아는 바가 있다면 댓글 부탁합니다ㅎㅎ

Copy link
Author

@somsom13 somsom13 Oct 2, 2023

Choose a reason for hiding this comment

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

안녕하세요 이레 ㅎㅎ 이레의 말씀을 듣고 Connection.close() 가 호출되는 상황을 또 디버깅을 해보았는데요..!

  1. Connection 객체 생성
  2. query로직 수행
  3. JdbcPreparedStatementclose 호출 -> closeInternal 을 통해 ResultSet을 close 한 후, conn을 null로 세팅
  4. JdbcConnectionclose 호출

의 순서로 로직이 흘러가는 것 같더라구요! PreparedStatement에서 가지고 있던 커넥션을 먼저 해제한 후, 커넥션 자원을 해제하는 순서로 흘러가는 것 같아요.

}

public int update(final String sql, final Object... args) {
try (final Connection connection = dataSource.getConnection();
final PreparedStatement pstmt = connection.prepareStatement(sql)) {
log.debug("query : {}", sql);

setParamsToPreparedStatement(pstmt, args);

return pstmt.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
10 changes: 10 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,10 @@
package org.springframework.jdbc.core;

import java.sql.ResultSet;
import java.sql.SQLException;

@FunctionalInterface
public interface RowMapper<T> {

T mapRow(final ResultSet resultSet, final int count) throws SQLException;

Choose a reason for hiding this comment

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

mapRow에 count를 사용하신 이유가 있나요? 실제로 Dao에서 count를 유의미하게 사용하는 것 같지 않아서요. 어디에 사용되는 것인지 궁금합니다!

Copy link
Author

Choose a reason for hiding this comment

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

실제로는 사용하지 않는 부분인 것 같아요!!

rownum은 rownum 수 만큼 매핑 과정을 반복한다 라는 용도로 사용되는 것으로 알고 있는데, 실제로 제가 구현한 JdbcTemplate을 보면 rowNum을 전혀 사용하지 않고 while문을 돌면서 resultSet을 매핑하고 있네용

이 부분은 rowNum을 제거한 상태로 테스트 해보고 다시 Dm 또는 답변으로 전달 드리겠습니다! 🙇

Choose a reason for hiding this comment

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

코드 내부적으로도 while문으로 돌고 rownum은 몇개의 행을 update했는지 count를 세기 위한 것으로 알고 있습니다.

}
1 change: 1 addition & 0 deletions study/src/main/java/aop/config/DataSourceConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
1 change: 1 addition & 0 deletions study/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
spring:
jpa:
open-in-view: false
show-sql: true
generate-ddl: true
hibernate:
Expand Down