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단계] 홍실(홍혁준) 미션 제출합니다. #270

Merged
merged 13 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from 10 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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
# JDBC 라이브러리 구현하기

- [x] UserDaoTest 통과시키기
- [x] user findAll 구현하기
- [x] user update 구현하기
- [x] user findByAccount 구현하기
- [x] UserDao 리팩터링
- [x] 공통부분 분리
- [ ] JdbcTemplate 구현하기
- [x] 조회기능 구현
- [x] 쓰기기능 구현
- [x] UserDao를 JdbcTemplate을 사용하도록 리팩터링
- [x] findByAccount
- [x] findById
- [x] findAll
- [x] update
- [x] insert
117 changes: 31 additions & 86 deletions app/src/main/java/com/techcourse/dao/UserDao.java
Original file line number Diff line number Diff line change
@@ -1,121 +1,66 @@
package com.techcourse.dao;

import com.techcourse.domain.User;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
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.Mapper;

public class UserDao {

private static final Mapper<User> USER_MAPPER = (rs) -> new User(
rs.getLong(1),
rs.getString(2),
rs.getString(3),
rs.getString(4)
);
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;
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.executeUpdate(sql,
user.getAccount(), user.getPassword(), user.getEmail());
}

public void update(final User user) {
// todo
final var sql = "update users set account = ?, password = ? , email = ? where id = ?";
jdbcTemplate.executeUpdate(sql,
user.getAccount(), 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.executeQuery(sql, (rs) -> {
final List<User> users = new ArrayList<>();
while (rs.next()) {
users.add(USER_MAPPER.map(rs));
}
return users;
});
}

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.executeQuery(sql, USER_MAPPER, id);
}

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

return jdbcTemplate.executeQuery(sql, USER_MAPPER, account);
}
}
2 changes: 2 additions & 0 deletions jdbc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.2"
testImplementation "org.mockito:mockito-core:5.4.0"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.2"

implementation "com.h2database:h2:2.2.220"
}
61 changes: 59 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,14 @@
package org.springframework.jdbc.core;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static java.sql.Statement.RETURN_GENERATED_KEYS;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JdbcTemplate {

Expand All @@ -14,4 +19,56 @@ public class JdbcTemplate {
public JdbcTemplate(final DataSource dataSource) {
this.dataSource = dataSource;
}

public Long executeUpdate(final String sql, final Object... parameters) {
try (final Connection connection = dataSource.getConnection();
final PreparedStatement pstmt = connection.prepareStatement(sql,
RETURN_GENERATED_KEYS)) {
log.debug("query : {}", sql);
setPreparedStatement(pstmt, parameters);
pstmt.executeUpdate();
return extractId(pstmt);
Copy link
Member

Choose a reason for hiding this comment

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

이전에 jdbcTempate을 사용했을 때를 생각하면 update 시 해당 쿼리의 영향을 받은 행의 개수가 반환되었던 걸로 기억하고 있습니다.
그런데 영향을 받은 행이 아닌 영향을 받은 행의 id를 반환하는 이유가 있을까요?

Copy link
Member Author

@hong-sile hong-sile Oct 3, 2023

Choose a reason for hiding this comment

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

JdbcTemplate이 영향을 받은 행의 개수를 반환한다는 것은 알고 있었어요. 하지만 굳이 JdbcTemplate과 똑같이 할 필욘 없다고 생각했습니다.
사실 영향을 받은 행의 개수를 반환하게 해도 해당 값을 사용할 일이 적다고 생각했어요.
잘 쓰이지도 않는 영향을 받은 행의 개수를 반환할 바엔, id를 반환하게 하는 게 조금 더 유용하게 쓰일 메서드라고 생각했어요.
id는 보통 db에 의존하는 방식이고, 자바코드에서 별도의 쿼리를 날리지 않는 이상 알 수 있는 방법은 없으니까요.
insert를 하고 location을 반환할 때도 쓰일 수 있고요.

근데, 지금 다시 보니 여러개의 row를 업데이트 하는 상황이랑은 조금 안 맞는 것 같네요.
조금 더 고민해보고 적당한 방향으로 반영해보겠습니다.

} catch (final SQLException e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}

private Long extractId(final PreparedStatement pstmt) throws SQLException {
try (final ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
if (generatedKeys.next()) {
return generatedKeys.getLong(1);
}
throw new SQLException("id를 찾을 수 없습니다.");
}
}

private void setPreparedStatement(
final PreparedStatement pstmt,
final Object[] parameters
) throws SQLException {
for (int index = 1; index <= parameters.length; index++) {
pstmt.setObject(index, parameters[index - 1]);
}
}

public <T> T executeQuery(
final String sql,
final Mapper<T> mapper,
final Object... objects) {

try (final Connection connection = dataSource.getConnection();
final PreparedStatement pstmt = connection.prepareStatement(sql)) {
setPreparedStatement(pstmt, objects);
final ResultSet rs = pstmt.executeQuery();
Copy link
Member

Choose a reason for hiding this comment

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

ResultSetAutoCloseable를 상속받고 있다고 하네요.
ResultSet도 함께 try-with-resources를 적용해줘야 할 것 같아요!

image

Copy link
Member Author

Choose a reason for hiding this comment

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

ResultSet도 close를 해줘야하는 군요. 적용해볼게요!

log.debug("query : {}", sql);
if (rs.next()) {
return mapper.map(rs);
}
return null;
} catch (SQLException e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
}
9 changes: 9 additions & 0 deletions jdbc/src/main/java/org/springframework/jdbc/core/Mapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.springframework.jdbc.core;

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

public interface Mapper<T> {

T map(final ResultSet resultSet) throws SQLException;
}
5 changes: 0 additions & 5 deletions jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.springframework.jdbc.core;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.test_supporter.DataSourceConfig;
import org.springframework.jdbc.core.test_supporter.DatabasePopulatorUtils;
import org.springframework.jdbc.core.test_supporter.User;
import org.springframework.jdbc.core.test_supporter.UserDao;

class JdbcTemplateTest {
Copy link
Member

Choose a reason for hiding this comment

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

테스트까지! 꼼꼼하시군요 👍


private final JdbcTemplate jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance());
private final UserDao userDao = new UserDao(DataSourceConfig.getInstance());

@BeforeEach
void setUp() {
DatabasePopulatorUtils.execute(DataSourceConfig.getInstance());
}

@Test
@DisplayName("execute Query로 읽는 쿼리를 실행할 수 있다.")
void executeQuery() {
final User user = new User("hong-sile", "hong", "[email protected]");
userDao.insert(user);
final String sql = "select id, account, password, email from users where id = ?";
final Long id = 1L;

final User actual = jdbcTemplate.executeQuery(sql, (rs) ->
new User(
rs.getLong(1),
rs.getString(2),
rs.getString(3),
rs.getString(4)
)
, id);

assertAll(
() -> assertThat(id)
.isEqualTo(actual.getId()),
() -> assertThat(actual)
.usingRecursiveComparison()
.ignoringFields("id")
.isEqualTo(user)
);
}

@Test
@DisplayName("execute로 쓰는 쿼리를 실행할 수 있다.")
void execute() {
final User user = new User("hong-sile", "hong", "[email protected]");
final String sql = "insert into users (account, password, email) values (?, ?, ?)";

final Long id = jdbcTemplate.executeUpdate(
sql,
user.getAccount(), user.getPassword(), user.getEmail()
);

final User actual = userDao.findById(id);

assertAll(
() -> assertThat(id)
.isEqualTo(actual.getId()),
() -> assertThat(actual)
.usingRecursiveComparison()
.ignoringFields("id")
.isEqualTo(user)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.springframework.jdbc.core.test_supporter;

import java.util.Objects;
import org.h2.jdbcx.JdbcDataSource;

public class DataSourceConfig {

private static javax.sql.DataSource INSTANCE;

public static javax.sql.DataSource getInstance() {
if (Objects.isNull(INSTANCE)) {
INSTANCE = createJdbcDataSource();
}
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 DataSourceConfig() {}
}
Loading
Loading