public class Users {
private Long id;
private String name;
private int balance;
public Users(String name, int balance) {
this.name = name;
this.balance = balance;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public int getBalance() {
return balance;
}
}
MySQL 드라이버는 의존성으로 있다.
DriverManager로 커넥션을 얻어서 Insert 요청을 처리하면 되겠다.
public class UsersRepository {
public Users save(Users users) {
Connection connection = null;
PreparedStatement ps = null;
try {
connection = DriverManager.getConnection(dbConfig.getUrl(), dbConfig.getUsername(), dbConfig.getPassword());
ps = connection.prepareStatement("""
Insert Into Users (name, balance)
values
(? ,?)
""");
ps.setString(1, users.getName());
ps.setInt(2, users.getBalance());
boolean result = ps.execute();
if(!result)
throw new IllegalStateException("User Insert 실패");
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
try {
if(ps != null)
ps.close();
if(connection != null)
connection.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
return null;
}
}
문제점 투성이다.
execute() 메서드는 범용 메서드기 때문에 Insert Select 등등 모든 쿼리를 처리한다. 지금은 Insert만 처리할 것이니 executeUpdate()를 사용하는게 맞다
ps.execute()의 반환값은 ResultSet이 있다면 true인 것이다. 그렇기 때문에 저장 과정에서 항상 예외가 발생한다.
지금 코드에서 삽입된 User의 ID를 알 방법이 없다.
finally에서 close의 try-catch를 하나로 뭉뚱그렸는데 ps만 닫히고, connection은 닫히지 않을 수 있다. 따로 try-catch를 해주는게 맞다.
Users를 반환하지 않고 있다.
단순하다. execute를 executeUpdate로 변환해주면된다.
/*
제거
boolean result = ps.execute();
if(!result)
throw new IllegalStateException("User Insert 실패");
*/
// 추가
ps.executeUpdate();
참고) executeUpdate()의 반환값은 영향을 받은 row의 개수이다.
참고) https://www.baeldung.com/jdbc-get-insert-id
prepareStatement 호출 시에 RETURN_GENERATED_KEYS 플래그를 전달해주고 ps.getGeneratedKeys()로 값을 가져오면 된다.
ps = conn.prepareStatement("""
Insert Into Users (name, balance)
values
(? ,?)
""", Statement.RETURN_GENERATED_KEYS); // 플레그 전달 - 생성된 ID 반환
ps.setString(1, users.getName());
ps.setInt(2, users.getBalance());
ps.executeUpdate();
rs = ps.getGeneratedKeys();
boolean hasId = rs.next();
long id = rs.getLong(1); // id 반환
try(Connection conn = DriverManager.getConnection(dbConfig.getUrl(), dbConfig.getUsername(), dbConfig.getPassword());
PreparedStatement ps = conn.prepareStatement("""
Insert Into Users (name, balance) values (? ,?)
""", Statement.RETURN_GENERATED_KEYS); // 플레그 전달 - 생성된 ID 반환
) {
ps.setString(1, users.getName());
ps.setInt(2, users.getBalance());
ps.executeUpdate();
try(ResultSet rs = ps.getGeneratedKeys()) {
boolean hasId = rs.next();
long id = rs.getLong(1); // id 반환
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
이 부분은 JPA처럼 리플렉션으로 기존 객체에 ID 박아넣어 반환하도록 했다.
// protected Users() {} -- Users 코드에 기본 생성자가 있어야 한다. 추가해준다.
public class MyEntityIdInjector {
public static void injectId(Object entity, Long id) {
try {
Field field = entity.getClass().getDeclaredField("id");
field.setAccessible(true);
field.set(entity, id);
} catch (Exception e) {
throw new IllegalStateException("id 주입 실패", e);
}
}
}
위 클래스를 작성하고 다음과 같이 파라미터로 받은 users에 id를 박아넣고 반환했다.
try(ResultSet rs = ps.getGeneratedKeys()) {
boolean hasId = rs.next();
long id = rs.getLong(1); // id 반환
MyEntityIdInjector.injectId(users, id);
return users;
}
다음 테스트 코드가 정상 통과했다.
package com.example.my_spring_server.users.infra;
import com.example.my_spring_server.MySQLConfig;
import com.example.my_spring_server.users.domain.Users;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class UsersRepositoryTest {
UsersRepository repository = new UsersRepository(new MySQLConfig());
@Test
void Users_저장하기() {
Users result = repository.save(new Users("아무개", 10_000));
assertNotNull(result.getId());
assertEquals(result.getId(), repository.findById(result.getId()).getId());
assertEquals("아무개", result.getName());
assertEquals(10_000, result.getBalance());
}
}
findById도 구현을 했지만 생략
package com.example.my_spring_server.users.infra;
import com.example.my_spring_server.DBConfig;
import com.example.my_spring_server.jpa.MyEntityIdInjector;
import com.example.my_spring_server.users.domain.Users;
import java.sql.*;
public class UsersRepository {
private final DBConfig dbConfig;
public UsersRepository(DBConfig dbConfig) {
this.dbConfig = dbConfig;
}
public Users save(Users users) {
try(Connection conn = DriverManager.getConnection(dbConfig.getUrl(), dbConfig.getUsername(), dbConfig.getPassword());
PreparedStatement ps = conn.prepareStatement("""
Insert Into Users (name, balance)
values
(? ,?)
""", Statement.RETURN_GENERATED_KEYS); // 플레그 전달 - 생성된 ID 반환
) {
ps.setString(1, users.getName());
ps.setInt(2, users.getBalance());
ps.executeUpdate();
try(ResultSet rs = ps.getGeneratedKeys()) {
boolean hasId = rs.next();
long id = rs.getLong(1); // id 반환
MyEntityIdInjector.injectId(users, id);
return users;
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
close랑 쓰잘때기 없는 예외 처리가 너무 많다.
DB 처리하는 곳에서 모두 이런 흐름의 코드의 반복이 일어날 것 같다.