4 분 소요


이름 지정 파라미터1

순서대로 바인딩

JdbcTemplate을 기본으로 사용하면 파라미터를 순서대로 바인딩 한다.

String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql,
        itemName,
        price,
        quantity,
        itemId);

itemName, price, quantity가 SQL에 있는 ? 순서대로 바인딩 된다.
따라서 순서만 잘 지키면 문제가 될 것은 없지만, 변경시점에 문제가 발생한다.

누군가 SQL 코드의 순서를 변경했다고 가정하면..

String sql = "update item set item_name=?, quantity=?, price=? where id=?";
template.update(sql,
        itemName,
        price,
        quantity,
        itemId);

이렇게 되면 다음과 같은 순서로 데이터가 바인딩 된다.
item_name=itemName, quantity=price, price=quantity

이처럼 파라미터를 순서대로 바인딩 하는 것은 편리하기는 하지만, 순서가 맞지 않아서 버그가 발생할 수도 있으므로 주의해서 사용해야 한다.

이름 지정 바인딩

JdbcTemplate은 이런 문제를 보완하기 위해 NamedParameterJdbcTemplate라는 이름을 지정해서 파라미터를 바인딩 하는 기능을 제공한다.

JdbcTemplateItemRepositoryV2

/**
 * NamedParameterJdbcTemplate
 * SqlParameterSource
 * - BeanPropertySqlParameterSource
 * - MapSqlParameterSource
 * Map
 *
 * BeanPropertyRowMapper
 *
 */
@Slf4j
@Repository
public class JdbcTemplateItemRepositoryV2 implements ItemRepository {

    private final NamedParameterJdbcTemplate template;

    public JdbcTemplateItemRepositoryV2(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)";

        SqlParameterSource param = new BeanPropertySqlParameterSource(item);
        KeyHolder keyHolder = new GeneratedKeyHolder();
        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, Object> 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 변환 지원
    }
}


이름 지정 파라미터2

이름 지정 파라미터
파라미터를 전달하려면 Map처럼 key, value 데이터 구조를 만들어서 전달해야 한다.
여기서 key:파라미터 이름, value는 해당 파라미터의 값이 된다.

이름 지정 바인딩에서 자주 사용하는 파라미터의 종류는 크게 3가지이다.

  • Map
  • SqlParameterSource
    • MapSqlParameterSource
    • BeanPropertySqlParameterSource

Map

단순히 Map 사용

Map<String, Object> param = Map.of("id", id);
Item item = template.queryForObject(sql, param, itemRowMapper());

MapSqlParameterSource

  • Map과 유사한데, SQL 타입을 지정할 수 있는 등 SQL에 좀 더 특화된 기능을 제공한다.
  • SqlParameterSource 인터페이스의 구현체이다.
  • MapSqlParameterSource는 메서드 체인을 통해 편리한 사용법도 제공한다.
SqlParameterSource param = new MapSqlParameterSource()
        .addValue("itemName", updateParam.getItemName())
        .addValue("price", updateParam.getPrice())
        .addValue("quantity", updateParam.getQuantity())
        .addValue("id", itemId); //이 부분이 별도로 필요하다.
        template.update(sql, param);

BeanPropertySqlParameterSource

  • 자동으로 파라미터 객체를 생성한다.
    • Ex) getXxx() -> xxx, getItemName() -> itemName
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
  • BeanPropertySqlParameterSource가 많은 것을 자동화 해주기 때문에 가장 좋아보이지만, 항상 사용할 수 있는 것은 아니다.
  • 예를 들어 update()에서는 SQL에 :id를 바인딩 해야 하는데, update()에서 사용하는 ItemUpdateDto에는 itemId가 없다. 따라서, BeanPropertySqlParameterSource를 사용할 수 없고, 대신에 MapSqlParameterSource를 사용했다.

BeanPropertyRowMapper

private RowMapper<Item> itemRowMapper() {
    return BeanPropertyRowMapper.newInstance(Item.class); //camel 변환 지원
}

BeanPropertyRowMapperResultSet의 결과를 받아서 Java bean 규약에 맞춰 데이터를 변환한다.
예를 들어, 데이터베이스에서 조회한 결과가 select id, price라고 하면 다음과 같은 코드를 작성해준다.

Item item = new Item();
item.setId(rs.getLong("id"));
item.setPrice(rs.getInt("price"));

별칭
select item_name의 경우 setItem_name()이라는 메서드가 없기 때문에 이런 경우에는 개발자가 조회 SQL을 다음과 같이 고치면 된다.
select item_name as itemName

관례의 불일치

자바 객체는 itemName처럼 중간에 낙타 봉이 올라와 있는 카멜(camelCase) 표기법을 사용한다.
반면에 관계형 데이터베이스에서는 주로 item_name처럼 중간에 언더스코어를 사용하는 snake_case표기법을 사용한다.

이 부분을 관례로 많이 사용하다 보니 BeamPropertyRowMapper는 언더스코어 표기법을 카멜로 자동 변환해준다.
따라서, select item_name으로 조회해도 setItemName()에 문제 없이 값이 들어간다.

정리하면 snake_case는 자동으로 해결되니 그냥 두고, 컬럼 이름과 객체 이름이 완전히 다른 경우에는 조회 SQL에서 별칭을 사용하면 된다.


SimpleJdbcInsert

JdbcTemplate은 INSERT SQL을 직접 작성하지 않아도 되도록 SimpleJdbcInsert라는 편리한 기능을 제공한다.

JdbcTemplateItemRepositoryV3

/**
 * SimpleJdbcInsert
 */
@Slf4j
@Repository
public class JdbcTemplateItemRepositoryV3 implements ItemRepository {

    private final NamedParameterJdbcTemplate template;
    private final SimpleJdbcInsert jdbcInsert;

    public JdbcTemplateItemRepositoryV3(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) {
        SqlParameterSource param = new BeanPropertySqlParameterSource(item);
        
        Number key = jdbcInsert.executeAndReturnKey(param);
        item.setId(key.longValue());
        return item;
    }

    ...
}
  • withTableName: 데이터를 저장할 테이블 명을 지정
  • usingGeneratedkeyColumns: key를 생성하는 PK 컬럼 명을 지정
  • usingColumns: INSERT SQL에 사용할 컬럼을 지정.
    • 특정 값만 저장하고 싶을 때 사용하며 생략할 수 있다.

SimpleJdbcInsert는 생성 시점에 데이터베이스 테이블의 메타 데이터를 조회한다. 따라서, 어떤 컬럼이 있는지 확인할 수 있으므로 usingColumns를 생략할 수 있다.

save()

jdbcInsert.executeAndReturnKey(param)을 사용해서 INSERT SQL을 실행하고, 생성된 키 값도 매우 편리하게 조회할 수 있다.


JdbcTemplate 기능 정리

주요 기능

  • JdbcTemplate
    • 순서 기반 파라미터 바인딩을 지원한다.
  • NamedParameterJdbcTemplate
    • 이름 기반 파라미터 바인딩을 지원한다.(권장)
  • SimpleJdbcInsert
    • INSERT SQL을 편리하게 사용할 수 있다.

사용법 정리

JdbcTemplate에 대한 사용법은 스프링 공식 메뉴얼에 자세히 소개되어 있다.

[참고]
JdbcTemplate 사용 방법 공식 메뉴얼

조회

단건 조회

숫자 조회

int rowCount = jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
  • 하나의 로우을 조회할 때는 queryForObject()를 사용하면 된다.
  • 조회 대상이 객체가 아니라 단순 데이터 하나라면 타입을 Integer.class, String.class와 같이 지정해주면 된다.

숫자 조회, 파라미터 바인딩

int countOfActorsNamedJoe = jdbcTemplate.queryForObject(
        "select count(*) from t_actor where first_name = ?", Integer.class, "Joe");

문자 조회

String lastName = jdbcTemplate.queryForObject(
        "select last_name from t_actor where id = ?",
        String.class, 1212L);

객체 조회

Actor actor = jdbcTemplate.queryForObject(
        "select first_name, last_name from t_actor where id = ?",
        (resultSet, rowNum) -> {
            Actor newActor = new Actor();
            newActor.setFirstName(resultSet.getString("first_name"));
            newActor.setLastName(resultSet.getString("last_name"));
            return newActor;
        }, 1212L);
  • 객체 하나를 조회.
  • 결과를 객체로 매핑해야 하므로 RowMapper를 사용해야 한다.

목록 조회

객체

List<Actor> actors = jdbcTemplate.query(
        "select first_name, last_name from t_actor",
        (resultSet, rowNum) -> {
            Actor actor = new Actor();
            actor.setFirstName(resultSet.getString("first_name"));
            actor.setLastName(resultSet.getString("last_name"));
            return actor;
        });
  • 여러 로우을 조회할 때는 query()를 사용하면 된다.

RowMapper 분리

private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
    Actor actor = new Actor();
    actor.setFirstName(resultSet.getString("first_name"));
    actor.setLastName(resultSet.getString("last_name"));
    return actor;
};

public List<Actor> findAllActors() {
    return this.jdbcTemplate.query("select first_name, last_name from t_actor", actorRowMapper);
}

RowMapper를 분리하면 여러 곳에서 재사용 할 수 있다.

기타 기능

  • 임의의 SQL을 실행할 때는 execute()를 사용하면 된다.
  • 테이블을 생성하는 DDL에 사용할 수 있다.

DDL

jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");


<출처 : 인프런 - 스프링 DB 2편 : 데이터 접근 활용 기술(김영한)>

댓글남기기