JdbcTemplate
SQL을 직접 사용하는 경우에 유용하다.
장점
설정이 편리하다.
코드의 반복 해결
단점
동적인 SQL을 생성하기 어렵다
package hello.itemservice.infrastructure.persistence.database.repository;
import hello.itemservice.domain.Item;
import hello.itemservice.domain.ItemRepository;
import hello.itemservice.domain.ItemSearchCond;
import hello.itemservice.domain.ItemUpdateDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
import java.sql.PreparedStatement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* JDBC Template
*/
@Slf4j
public class JdbcTemplateItemRepositoryV1 implements ItemRepository {
private final JdbcTemplate template;
public JdbcTemplateItemRepositoryV1(final DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
@Override
public Item save(Item item) {
String sql = "insert into item(item_name, price, quantity) values (?, ?, ?)";
// DB에서 생성해준 값 받아오기 위해서는 필요함
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(con -> {
// 자동 증가 키
PreparedStatement ps = con.prepareStatement(sql, new String[]{"id"});
ps.setString(1, item.getItemName());
ps.setInt(2, item.getPrice());
ps.setInt(3, item.getQuantity());
return ps;
}, keyHolder);
long key = keyHolder.getKey().longValue();
item.setId(key);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql,
updateParam.getItemName(),
updateParam.getPrice(),
updateParam.getQuantity(),
itemId);
}
@Override
public Optional<Item> findById(Long id) {
String sql = "select id, item_name, price, quantity from item where id = ?";
try {
Item item = template.queryForObject(sql, itemRowMapper(), id);
// queryForObject는 결과가 없으면 예외가 터진다.
return Optional.of(item);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
private RowMapper<Item> itemRowMapper() {
return ((rs, number) -> {
Item item = new Item();
item.setId(rs.getLong("id"));
item.setItemName(rs.getString("item_name"));
item.setPrice(rs.getInt("price"));
item.setQuantity(rs.getInt("quantity"));
return item;
});
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
Integer maxPrice = cond.getMaxPrice();
String itemName = cond.getItemName();
String sql = "select id, item_name, price, quantity from item";
// 동적 쿼리
if (StringUtils.hasText(itemName) || maxPrice != null) {
sql += " where";
}
boolean andFlag = false;
ArrayList<Object> param = new ArrayList<>();
if (StringUtils.hasText(itemName)) {
sql += " item_name like concat('%', ?, '%')";
param.add(itemName);
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
sql += " and";
}
sql += " price <= ?";
param.add(maxPrice);
}
log.info("sql={}", sql);
return template.query(sql, itemRowMapper(), param.toArray());
}
}
NamedParameterJdbcTemplate
기존 JdbcTemplate은 파라미터를 순서대로 바인딩 하였으나 이는 이후 데이터가 수정되거나 추가되는 경우 순서가 바뀌면 문제가 될 수 있다.
이러한 문제를 손쉽게 해결하기 위해 NamedParameterJdbcTemplate를 만들어, 이름을 지정하여 파라미터를 바인하는 기능을 제공한다.
package hello.itemservice.infrastructure.persistence.database.repository;
import hello.itemservice.domain.Item;
import hello.itemservice.domain.ItemRepository;
import hello.itemservice.domain.ItemSearchCond;
import hello.itemservice.domain.ItemUpdateDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
import java.sql.PreparedStatement;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* NamedParameterJdbcTemplate
*
* 파라미터를 전달하리면 Map 처럼 key와 value 데이터 구조를 만들어서 전달해야 한다.
* 여기서 key는 :파라미터이름 으로 지정한 파라미터의 이름이고, value는 해당 파라미터의 값이다.
*
* Parameter 로 가능한 것들
* - SqlParameterSource
* - BeanPropertySqlParameterSource
* - MapSqlParameterSource
*
* - Map
*
* RowMapper로 BeanPropertyRowMapper 사용 가
*/
@Slf4j
public class JdbcTemplateItemRepositoryV2 implements ItemRepository {
private final NamedParameterJdbcTemplate template;
public JdbcTemplateItemRepositoryV2(final DataSource dataSource) {
this.template = new NamedParameterJdbcTemplate(dataSource);
}
@Override
public Item save(Item item) {
String sql = "insert into item(item_name, price, quantity) " +
"values (:itemName, :price, :quantity)";
KeyHolder keyHolder = new GeneratedKeyHolder();
// Item을 가지고 프로퍼티 생성
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
template.update(sql, param, keyHolder);
long key = keyHolder.getKey().longValue();
item.setId(key);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
String sql = "update item set " +
"item_name=:itemName, " +
"price=:price, " +
"quantity=:quantity " +
"where id=:id";
SqlParameterSource param = new MapSqlParameterSource()
.addValue("itemName", updateParam.getItemName())
.addValue("price", updateParam.getPrice())
.addValue("quantity", updateParam.getQuantity())
.addValue("id", itemId);
template.update(sql, param);
}
@Override
public Optional<Item> findById(Long id) {
String sql = "select id, item_name, price, quantity from item where id = :id";
try {
Map<String, Long> param = Map.of("id", id);
Item item = template.queryForObject(sql, param, itemRowMapper());
return Optional.of(item);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class); // Camel 변환 지원 (itemName -> item_name)
// 이것도 가능은 하다.
// return ((rs, number) -> {
// Item item = new Item();
// item.setId(rs.getLong("id"));
// item.setItemName(rs.getString("item_name"));
// item.setPrice(rs.getInt("price"));
// item.setQuantity(rs.getInt("quantity"));
// return item;
// });
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
Integer maxPrice = cond.getMaxPrice();
String itemName = cond.getItemName();
BeanPropertySqlParameterSource param = new BeanPropertySqlParameterSource(cond);
String sql = "select id, item_name, price, quantity from item";
// 동적 쿼리
if (StringUtils.hasText(itemName) || maxPrice != null) {
sql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
sql += " item_name like concat('%', :itemName, '%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
sql += " and";
}
sql += " price <= :maxPrice";
}
log.info("sql={}", sql);
return template.query(sql, param, itemRowMapper());
}
}
BeanPropertyRowMapper는 ResultSet의 결과를 받아서 자바빈 규약에 맞추어 데이터를 변환한다.
그러나 데이터베이스 컬럼 이름과 객체 이름이 완전히 다른 경우, 예를 들어 데이터베이스에는 member_name이라고 되어 있는데, 객체에서는 username이라고 되어 있다면 별칭을 사용하여 다음과 같이 해결할 수 있다.
select member_name as username
그러나 데이터베이스에는 item_name이라고 되어있으나, 객체에서는 itemName이라고 되어있는 경우, 이는 각각 데이터베이스의 관례와 자바의 관례에 의한 것이므로, 이는 RowMapper에서 알아서 변환하여 매핑해준다. (스네이크 -> 카멜)
JdbcTemplate - SimpleJdbcInsert
JdbcTemplate은 INSERT SQL을 직접 작성하지 않아도 되도록 SimpleJdbcTemplate라는 기능을 제공한다.
package hello.itemservice.infrastructure.persistence.database.repository;
import hello.itemservice.domain.Item;
import hello.itemservice.domain.ItemRepository;
import hello.itemservice.domain.ItemSearchCond;
import hello.itemservice.domain.ItemUpdateDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* SimpleJdbcInsert
*/
@Slf4j
public class JdbcTemplateItemRepositoryV3 implements ItemRepository {
private final NamedParameterJdbcTemplate template;
private final SimpleJdbcInsert jdbcInsert;
public JdbcTemplateItemRepositoryV3(final DataSource dataSource) {
this.template = new NamedParameterJdbcTemplate(dataSource);
this.jdbcInsert = new SimpleJdbcInsert(dataSource)
.withTableName("item")
.usingGeneratedKeyColumns("id");
// .usingColumns("item_name", "price", "quantity"); 생략 가능!
}
@Override
public Item save(Item item) {
BeanPropertySqlParameterSource param = new BeanPropertySqlParameterSource(item);
Number key = jdbcInsert.executeAndReturnKey(param);
item.setId(key.longValue());
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
String sql = "update item set " +
"item_name=:itemName, " +
"price=:price, " +
"quantity=:quantity " +
"where id=:id";
SqlParameterSource param = new MapSqlParameterSource()
.addValue("itemName", updateParam.getItemName())
.addValue("price", updateParam.getPrice())
.addValue("quantity", updateParam.getQuantity())
.addValue("id", itemId);
template.update(sql, param);
}
@Override
public Optional<Item> findById(Long id) {
String sql = "select id, item_name, price, quantity from item where id = :id";
try {
Map<String, Long> param = Map.of("id", id);
Item item = template.queryForObject(sql, param, itemRowMapper());
return Optional.of(item);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class); // Camel 변환 지원 (itemName -> item_name)
// 이것도 가능은 하다.
// return ((rs, number) -> {
// Item item = new Item();
// item.setId(rs.getLong("id"));
// item.setItemName(rs.getString("item_name"));
// item.setPrice(rs.getInt("price"));
// item.setQuantity(rs.getInt("quantity"));
// return item;
// });
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
Integer maxPrice = cond.getMaxPrice();
String itemName = cond.getItemName();
BeanPropertySqlParameterSource param = new BeanPropertySqlParameterSource(cond);
String sql = "select id, item_name, price, quantity from item";
// 동적 쿼리
if (StringUtils.hasText(itemName) || maxPrice != null) {
sql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
sql += " item_name like concat('%', :itemName, '%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
sql += " and";
}
sql += " price <= :maxPrice";
}
log.info("sql={}", sql);
return template.query(sql, param, itemRowMapper());
}
}
insert 외에는 변경되는 것이 없다.
MyBatis
JdbcTemplate보다 더욱 많은 기능을 제공하는 SQL Mapper이다.
기본적으로 JdbcTemplate이 제공하는 대부분의 기능을 제공한다.
JdbcTemplate와 비교해서 MyBatis는 SQL을 XML에 편리하게 작성할 수 있고, 또 동적 쿼리를 매우 편리하게 작성할 수 있다.
두 기술의 차이를 알아보자.
JdbcTemplate
String sql = "update item " +
"set item_name=:itemName, price=:price, quantity=:quantity " +
"where id=:id";
MyBatis
<update id="update">
update item
set item_name=#{itemName},
price=#{price},
quantity=#{quantity}
where id = #{id}
</update>
JdbcTemplate
String sql = "select id, item_name, price, quantity from item"; //동적 쿼리
if (StringUtils.hasText(itemName) || maxPrice != null) {
sql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
sql += " item_name like concat('%',:itemName,'%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
sql += " and";
}
sql += " price <= :maxPrice";
}
log.info("sql={}", sql);
return template.query(sql, param, itemRowMapper());
MyBatis
<select id="findAll" resultType="Item">
select id, item_name, price, quantity
from item
<where>
<if test="itemName != null and itemName != ''">
and item_name like concat('%',#{itemName},'%')
</if>
<if test="maxPrice != null">
and price <= #{maxPrice}
</if>
</where>
</select>
단점으로는 MyBatis는 사용하기 위해 약간의 설정이 필요하다는 것이다.
build.gralde 추가
// MyBatis 추가
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.3.0'
application.properties 추가
# MyBatis 여러 패키지 지정하는 경우 , ; 로 구분 가능
mybatis.type-aliases-package=hello.itemservice.domain
mybatis.configuration.map-underscore-to-camel-case=true
logging.level.hello.itemservice.infrastructure.database.repository.mybatis=trace
@Mapper
public interface ItemMapper {
/**
* 파라미터가 하나 넘어가는 경우 @Param 안써줘도 된다.
*/
void save(Item item);
void update(@Param("id") Long id,
@Param("updateParam") ItemUpdateDto updateParam);
List<Item> findAll(ItemSearchCond itemSearchCond);
Optional<Item> findById(Long id);
}
src/main/resources/hello/itemservice/repository/mybatis/ItemMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="hello.itemservice.infrastructure.persistence.database.mybatis.ItemMapper">
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into item (item_name, price, quantity)
values (#{itemName}, #{price}, #{quantity})
</insert>
<update id="update">
update item
set item_name=#{updateParam.itemName},
price=#{updateParam.price},
quantity=#{updateParam.quantity}
where id = #{id}
</update>
<select id="findById" resultType="Item">
select id, item_name, price, quantity
from item
where id=#{id}
</select>
<select id="findAll" resultType="Item">
select id, item_name, price, quantity
from item
<where>
<if test="itemName != null and itemName != ''">
and item_name like concat('%', #{itemName}, '%')
</if>
<if test="maxPrice != null">
<!-- and price <= #{maxPrice} 에서 < 가 오류-->
and price <= #{maxPrice}
</if>
</where>
</select>
</mapper>
참고 - XML 파일 경로 수정하기
- XML 파일을 원하는 위치에 두고 싶으면 application.properties 에 다음과 같이 설정하면 된다.
mybatis.mapper-locations=classpath:mapper/**/*.xml
이렇게 하면 resources/mapper 를 포함한 그 하위 폴더에 있는 XML을 XML 매핑 파일로 인식한다.
이경우 파일 이름은 자유롭게 설정해도 된다.
참고로 테스트의 application.properties 파일도 함께 수정해야 테스트를 실행할 때 인식할 수 있다.
'모각코 > 2022 동계 모각코 : 미남과 야수' 카테고리의 다른 글
[모각코] 2022 동계 모각코 3회차 결과 (0) | 2023.01.12 |
---|---|
[모각코] 2022 동계 모각코 3회차 목표 (0) | 2023.01.12 |
[모각코] 2022 동계 모각코 2회차 목표 (0) | 2023.01.05 |
[모각코] 2022 동계 모각코 1회차 결과 (0) | 2022.12.29 |
[모각코] 2022 동계 모각코 1회차 목표 (0) | 2022.12.29 |