package org.prgms;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.UUID;
public class JdbcCustomerRepository {
private static final Logger logger = LoggerFactory.getLogger(JdbcCustomerRepository.class);
public static void main(String[] args) {
try (
//CONNECTION
var connection = DriverManager.getConnection("jdbc:mysql://localhost:3307/kdt", "root", "1234");
//STATEMENT 객체를 통해 query를 날린다.
var statement = connection.createStatement();
//RESULT_SET 조회된 결과를 받아온다.
var resultSet = statement.executeQuery("select * from customers"); //resultSet 조회된 결과를 가져옴
) {
while (resultSet.next()) {
var name = resultSet.getString("name");
var customerId = UUID.nameUUIDFromBytes(resultSet.getBytes("customer_id"));
var createdAt = resultSet.getTimestamp("created_at").toLocalDateTime();
logger.info("customer id -> {}, name -> {}, createdAt -> {}", customerId, name, createdAt);
}
} catch (SQLException throwables) {
logger.error("Got error while closing connection", throwables);
}
}
}
JDBC 템플릿 이용(CustomerJdbcRepository)
package org.prgms.customer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import javax.sql.DataSource;
import java.nio.ByteBuffer;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Repository
public class CustomerJdbcRepository implements CustomerRepository {
private static final Logger logger = LoggerFactory.getLogger(CustomerJdbcRepository.class);
private final DataSource dataSource;
private final JdbcTemplate jdbcTemplate;
private static final RowMapper<Customer> customerRowMapper = (resultSet, i) -> {
var customerName = resultSet.getString("name");
var customerEmail = resultSet.getString("email");
var customerId = toUUID(resultSet.getBytes("customer_id"));
var createdAt = resultSet.getTimestamp("created_at").toLocalDateTime();
return new Customer(customerId, customerName, customerEmail, createdAt);
};
public CustomerJdbcRepository(DataSource dataSource, JdbcTemplate jdbcTemplate) {
this.dataSource = dataSource;
this.jdbcTemplate = jdbcTemplate;
}
@Override
public Customer insert(Customer customer) {
var update = jdbcTemplate.update("INSERT INTO customers(customer_id, name, email, created_at) VALUES (UUID_TO_BIN(?),?,?,? )",
customer.getCustomerId().toString().getBytes(),
customer.getName(),
customer.getEmail(),
Timestamp.valueOf(customer.getCreatedAt()));
if (update != 1) {
throw new RuntimeException("Nothing was inserted");
}
return customer;
}
@Override
public Customer update(Customer customer) {
var update = jdbcTemplate.update("UPDATE customers SET name = ?, email = ? , last_login_at =? WHERE customer_id = UUID_TO_BIN(?)",
customer.getName(),
customer.getEmail(),
customer.getLastLoginAt() == null ? null : Timestamp.valueOf(customer.getLastLoginAt()),
customer.getCustomerId().toString().getBytes()
);
if (update != 1) {
throw new RuntimeException("Nothing was updated");
}
return customer;
}
@Override
public int count() {
return jdbcTemplate.queryForObject("select count(*) from customers", Integer.class);
}
@Override
public List<Customer> findAll() {
return jdbcTemplate.query("select * from customers", customerRowMapper); //query List반환
}
@Override
public Optional<Customer> findById(UUID customerId) {
try {
return Optional.ofNullable(
jdbcTemplate.queryForObject("select * from customers where customer_id = UUID_TO_BIN(?)",
customerRowMapper, customerId.toString().getBytes()));
} catch (EmptyResultDataAccessException e) {
logger.error("Got empty result", e);
return Optional.empty();
}
// queryForObject는 단 한건의 오브젝트 반환 , preparedStatement가 만들어짐. (sql injection 방지)
// Optional로 처리하기 위해 try catch문 사용. 없을 경우 -> optional.empty()
}
@Override
public Optional<Customer> findByName(String name) {
try {
return Optional.ofNullable(jdbcTemplate.queryForObject("select * from customers where name = ?",
customerRowMapper,
name));
} catch (EmptyResultDataAccessException e) {
logger.error("Got empty result", e);
return Optional.empty();
}
}
@Override
public Optional<Customer> findByEmail(String email) {
try {
return Optional.ofNullable(jdbcTemplate.queryForObject("select * from customers where email = ?",
customerRowMapper,
email));
} catch (EmptyResultDataAccessException e) {
logger.error("Got empty result", e);
return Optional.empty();
}
}
@Override
public void deleteAll() {
jdbcTemplate.update("DELETE FROM customers");
}
static UUID toUUID(byte[] bytes) {
var byteBuffer = ByteBuffer.wrap(bytes);
return new UUID(byteBuffer.getLong(), byteBuffer.getLong());
}
}
JDBC Unit Test (CustomerJdbcRepositoryTest.java)
package org.prgms.customer;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import javax.sql.DataSource;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
@SpringJUnitConfig
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CustomerJdbcRepositoryTest {
@Configuration
@ComponentScan(
basePackages = {"org.prgms.customer"}
)
static class config {
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://localhost:3307/kdt")
.username("root")
.password("1234")
.type(HikariDataSource.class)
.build();
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
@Autowired
CustomerJdbcRepository customerJdbcRepository;
@Autowired
DataSource dataSource;
Customer newCustomer;
@BeforeAll
void setup() {
newCustomer = new Customer(UUID.randomUUID(), "test-user", " test@gmail.com", LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
customerJdbcRepository.deleteAll();
}
@Test
@Order(1)
public void testHikariConnectionPool() {
assertThat(dataSource.getClass().getName(), is("com.zaxxer.hikari.HikariDataSource"));
}
@Test
@Order(2)
@DisplayName("고객을 추가할 수 있다. ")
public void testInsert() {
customerJdbcRepository.insert(newCustomer);
System.out.println("newCustomer =>" + newCustomer.getCustomerId());
var retrievedCustomer = customerJdbcRepository.findById(newCustomer.getCustomerId());
assertThat(retrievedCustomer.isEmpty(), is(false));
}
@Test
@Order(3)
@DisplayName("전체 고객을 조회할 수 있다")
public void testFindAll() {
var customers = customerJdbcRepository.findAll();
assertThat(customers.isEmpty(), is(false));
}
@Test
@Order(4)
@DisplayName("이름으로 고객을 조회할 수 있다. ")
public void testFindByName() {
var customer = customerJdbcRepository.findByName(newCustomer.getName());
assertThat(customer.isEmpty(), is(false));
var unknown = customerJdbcRepository.findByName("unknown");
assertThat(unknown.isEmpty(), is(true));
}
@Test
@Order(5)
@DisplayName("이메일로 고객을 조회할 수 있다. ")
public void testFindByEmail() {
var customer = customerJdbcRepository.findByEmail(newCustomer.getEmail());
assertThat(customer.isEmpty(), is(false));
var unknown = customerJdbcRepository.findByEmail("unknown@gmail.com");
assertThat(unknown.isEmpty(), is(true));
}
@Test
@Order(6)
@DisplayName("고객을 수정할 수 있다.")
public void testUpdate() {
newCustomer.changeName("updated-user");
customerJdbcRepository.update(newCustomer);
var all = customerJdbcRepository.findAll();
assertThat(all, hasSize(1));
var retrievedCustomer = customerJdbcRepository.findById(newCustomer.getCustomerId());
assertThat(retrievedCustomer.isEmpty(), is(false));
}
}
위와 같이 테스트를 하게 되면 직접 DB에 연동하게 되는 것이다!
DB를 직접 연동하지 않을 경우 Embedded Database 를 사용하면 된다.
Embedded Mysql
<dependency>
<groupId>com.wix</groupId>
<artifactId>wix-embedded-mysql</artifactId>
<version>4.6.1</version>
<scope>test</scope>
</dependency>
https://github.com/wix/wix-embedded-mysql
Wix가 MySQL 8.0 이상에서 window를 지원하지 않는 문제가 있습니다. 따라서 mysql5.7로 테스트 해보면 될듯한데요.
# Mysql 8+
WHERE uuid = UUID_TO_BIN('77dea2ad-3c8c-40c6-a278-7cf1a1ac9384');
# 이전버전
WHERE uuid = UNHEX(REPLACE('77dea2ad-3c8c-40c6-a278-7cf1a1ac9384', '-', ''));
# Mysql 8+
SELECT BIN_TO_UUID(uuid) AS uuid
FROM my_table;
# 이전버전
SELECT HEX(uuid) AS uuid
FROM my_table;
package org.prgms.customer;
import com.wix.mysql.EmbeddedMysql;
import com.wix.mysql.config.Charset;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import javax.sql.DataSource;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static com.wix.mysql.EmbeddedMysql.anEmbeddedMysql;
import static com.wix.mysql.ScriptResolver.classPathScript;
import static com.wix.mysql.distribution.Version.v5_7_10;
import static com.wix.mysql.config.MysqldConfig.aMysqldConfig;
@SpringJUnitConfig
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CustomerJdbcRepositoryTest {
@Configuration
@ComponentScan(
basePackages = {"org.prgms.customer"}
)
static class config {
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://localhost:2215/test-order_mgmt")
.username("test")
.password("test1234!")
.type(HikariDataSource.class)
.build();
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
@Autowired
CustomerJdbcRepository customerJdbcRepository;
@Autowired
DataSource dataSource;
Customer newCustomer;
EmbeddedMysql embeddedMysql;
@BeforeAll
void setup() {
newCustomer = new Customer(UUID.randomUUID(), "test-user", " test@gmail.com", LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
var mysqlConfig = aMysqldConfig(v5_7_10)
.withCharset(Charset.UTF8)
.withPort(2215)
.withUser("test","test1234!")
.build();
embeddedMysql = anEmbeddedMysql(mysqlConfig)
.addSchema("test-order_mgmt",classPathScript("schema.sql"))
.start();
// customerJdbcRepository.deleteAll();
}
@AfterAll
void cleanup(){
embeddedMysql.stop();
}
@Test
@Order(1)
@Disabled
public void testHikariConnectionPool() {
assertThat(dataSource.getClass().getName(), is("com.zaxxer.hikari.HikariDataSource"));
}
@Test
@Order(2)
@DisplayName("고객을 추가할 수 있다. ")
public void testInsert() {
customerJdbcRepository.insert(newCustomer);
System.out.println("newCustomer =>" + newCustomer.getCustomerId());
var retrievedCustomer = customerJdbcRepository.findById(newCustomer.getCustomerId());
assertThat(retrievedCustomer.isEmpty(), is(false));
}
@Test
@Order(3)
@DisplayName("전체 고객을 조회할 수 있다")
public void testFindAll() {
var customers = customerJdbcRepository.findAll();
assertThat(customers.isEmpty(), is(false));
}
@Test
@Order(4)
@DisplayName("이름으로 고객을 조회할 수 있다. ")
public void testFindByName() {
var customer = customerJdbcRepository.findByName(newCustomer.getName());
assertThat(customer.isEmpty(), is(false));
var unknown = customerJdbcRepository.findByName("unknown");
assertThat(unknown.isEmpty(), is(true));
}
@Test
@Order(5)
@DisplayName("이메일로 고객을 조회할 수 있다. ")
public void testFindByEmail() {
var customer = customerJdbcRepository.findByEmail(newCustomer.getEmail());
assertThat(customer.isEmpty(), is(false));
var unknown = customerJdbcRepository.findByEmail("unknown@gmail.com");
assertThat(unknown.isEmpty(), is(true));
}
@Test
@Order(6)
@DisplayName("고객을 수정할 수 있다.")
public void testUpdate() {
newCustomer.changeName("updated-user");
customerJdbcRepository.update(newCustomer);
var all = customerJdbcRepository.findAll();
assertThat(all, hasSize(1));
var retrievedCustomer = customerJdbcRepository.findById(newCustomer.getCustomerId());
assertThat(retrievedCustomer.isEmpty(), is(false));
}
}
BeforeAll로 테스트 동작전에 미리 setup을 하면 된다.
@BeforeAll
void setup() {
newCustomer = new Customer(UUID.randomUUID(), "test-user", " test@gmail.com", LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
var mysqlConfig = aMysqldConfig(v5_7_10)
.withCharset(Charset.UTF8)
.withPort(2215)
.withUser("test","test1234!")
.build();
embeddedMysql = anEmbeddedMysql(mysqlConfig)
.addSchema("test-order_mgmt",classPathScript("schema.sql"))
.start();
// customerJdbcRepository.deleteAll();
}