0. JdbcTemplate 소개와 설정

  • SQL을 직접 사용하는 경우에 스프링이 제공하는 JdbcTemplate은 아주 좋은 선택지다. JdbcTemplate 은 JDBC를 매우 편리하게 사용할 수 있게 도와준다.

장점

설정의 편리함

  • JdbcTemplate은 spring-jdbc 라이브러리에 포함되어 있는데, 이 라이브러리는 스프링으로 JDBC를 사용할 때 기본으로 사용되는 라이브러리이다. 그리고 별도의 복잡한 설정 없이 바로 사용할 수 있다.

반복 문제 해결

  • JdbcTemplate은 템플릿 콜백 패턴을 사용해서, JDBC를 직접 사용할 때 발생하는 대부분의 반복 작업을 대신 처리해준다.
  • 개발자는 SQL을 작성하고, 전달할 파리미터를 정의하고, 응답 값을 매핑하기만 하면 된다.
  • 우리가 생각할 수 있는 대부분의 반복 작업을 대신 처리해준다.
    • 커넥션 획득
    • statement 를 준비하고 실행
    • 결과를 반복하도록 루프를 실행
    • 커넥션 종료, statement , resultset 종료
    • 트랜잭션 다루기 위한 커넥션 동기화
    • 예외 발생시 스프링 예외 변환기 실행

단점

  • 동적 SQL을 해결하기 어렵다.

설정

  • build.gradle 에 다음 설정 추가

    • //JdbcTemplate 추가
      implementation 'org.springframework.boot:spring-boot-starter-jdbc'
      //H2 데이터베이스 추가
      runtimeOnly 'com.h2database:h2'
      
  • 진행하기 전에 먼저 H2 데이터베이스에 item 테이블을 생성해야 한다.

    • drop table if exists item CASCADE;
      create table item
      (
          id bigint generated by default as identity,
          item_name varchar(10),
          price integer,
          quantity integer,
          primary key (id)
      );
      

1. JdbcTemplate 적용1 - 기본

JdbcTemplateItemRepositoryV1

  • package hello.itemservice.repository.jdbcTemplate;
      
    import hello.itemservice.domain.Item;
    import hello.itemservice.repository.ItemRepository;
    import hello.itemservice.repository.ItemSearchCond;
    import hello.itemservice.repository.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.stereotype.Service;
    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;
      
    @Slf4j
    public class JdbcTemplateItemRepositoryV1 implements ItemRepository {
      
        private final JdbcTemplate template;
      
        public JdbcTemplateItemRepositoryV1(DataSource dataSource) {
            this.template = new JdbcTemplate(dataSource);
        }
      
      
        @Override
        public Item save(Item item) {
            String sql = "insert into item(item_name, price, quantity) values (?,?,?)";
      
            //db 에서 만들어주는 key 를 사용하기 위해 불러옴
            KeyHolder keyHolder = new GeneratedKeyHolder();
            template.update((connection) -> {
                //자동 증가 키
                PreparedStatement preparedStatement = connection.prepareStatement(sql, new String[]{"id"});
                preparedStatement.setString(1, item.getItemName());
                preparedStatement.setInt(2, item.getPrice());
                preparedStatement.setInt(3, item.getQuantity());
                return preparedStatement;
            }, 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{
                //결과가 null 이면 항상 예외가 터짐(EmptyResultDataAccessException)
                //따라서 ofNullable 이 아닌 of 메서드를 사용
                Item item = template.queryForObject(sql, itemRowMapper(), id);
                return Optional.of(item);
            }catch (EmptyResultDataAccessException e){
                return Optional.empty();
            }
        }
      
        @Override
        public List<Item> findAll(ItemSearchCond cond) {
            String itemName = cond.getItemName();
            Integer maxPrice = cond.getMaxPrice();
      
            String sql = "select id, item_name, price, quantity from item";
      
            //동적 쿼리
            if (StringUtils.hasText(itemName) || maxPrice != null) {
                sql += " where";
            }
            boolean andFlag = false;
      
            List<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());
        }
      
        private RowMapper<Item> itemRowMapper() {
            return ((resultSet, rowNum) -> {
                Item item = new Item();
                item.setId(resultSet.getLong("id"));
                item.setItemName(resultSet.getString("item_name"));
                item.setPrice(resultSet.getInt("price"));
                item.setQuantity(resultSet.getInt("quantity"));
                return item;
            });
        }
    }
    
    • this.template = new JdbcTemplate(dataSource)
    • JdbcTemplate 은 데이터소스( dataSource )가 필요하다.
    • JdbcTemplateItemRepositoryV1() 생성자를 보면 dataSource 를 의존 관계 주입 받고 생성자 내부에서 JdbcTemplate 을 생성한다.
    • 스프링에서는 JdbcTemplate 을 사용할 때 관례상 이 방법을 많이 사용한다. 물론 JdbcTemplate 을 스프링 빈으로 직접 등록하고 주입받아도 된다.

Save

  • @Override
    public Item save(Item item) {
        String sql = "insert into item(item_name, price, quantity) values (?,?,?)";
      
        //db 에서 만들어주는 key 를 사용하기 위해 불러옴
        KeyHolder keyHolder = new GeneratedKeyHolder();
        template.update((connection) -> {
            //자동 증가 키
            PreparedStatement preparedStatement = connection.prepareStatement(sql, new String[]{"id"});
            preparedStatement.setString(1, item.getItemName());
            preparedStatement.setInt(2, item.getPrice());
            preparedStatement.setInt(3, item.getQuantity());
            return preparedStatement;
        }, keyHolder);
      
        long key = keyHolder.getKey().longValue();
        item.setId(key);
      
        return item;
    }
    
    • template.update() : 데이터를 변경할 때는 update() 를 사용하면 된다.
      • INSERT , UPDATE , DELETE SQL에 사용한다.
      • template.update() 의 반환 값은 int 인데, 영향 받은 로우 수를 반환한다.\

return 을 받기 위해 memberId 를 받아야 한다.

  • 데이터를 저장할 때 PK 생성에 identity (auto increment) 방식을 사용하기 때문에, PK인 ID 값을 개발자가 직접 지정하는 것이 아니라 비워두고 저장해야 한다. 그러면 데이터베이스가 PK인 ID를 대신 생성해준다.
  • 제는 이렇게 데이터베이스가 대신 생성해주는 PK ID 값은 데이터베이스가 생성하기 때문에, 데이터베이스에 INSERT가 완료 되어야 생성된 PK ID 값을 확인할 수 있다.
  • KeyHolder 와 connection.prepareStatement(sql, new String[]{“id”}) 를 사용해서 id 를 지정해주면 INSERT 쿼리 실행 이후에 데이터베이스에서 생성된 ID 값을 조회할 수 있다.
  • 물론 데이터베이스에서 생성된 ID 값을 조회하는 것은 순수 JDBC로도 가능하지만, 코드가 훨씬 더 복잡하다.
  • 참고로 뒤에서 설명하겠지만 JdbcTemplate이 제공하는 SimpleJdbcInsert 라는 훨씬 편리한 기능이 있으므로 대략 이렇게 사용한다 정도로만 알아두면 된다.

update()

  • @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);
        }
    
    • template.update() : 데이터를 변경할 때는 update() 를 사용하면 된다.
      • ”?” 에 바인딩할 파라미터를 순서대로 전달하면 된다.
      • 반환 값은 해당 쿼리의 영향을 받은 로우 수 이다. 여기서는 where id=? 를 지정했기 때문에 영향 받은 로우수는 최대 1개이다.

findById()

  • @Override
    public Optional<Item> findById(Long id) {
        String sql = "select id, item_name, price, quantity from item where id = ?";
        try{
            //결과가 null 이면 항상 예외가 터짐(EmptyResultDataAccessException)
            //따라서 ofNullable 이 아닌 of 메서드를 사용
            Item item = template.queryForObject(sql, itemRowMapper(), id);
            return Optional.of(item);
        }catch (EmptyResultDataAccessException e){
            return Optional.empty();
        }
    }
    
  • template.queryForObject()

    • 결과 로우가 하나일 때 사용한다.
    • RowMapper 는 데이터베이스의 반환 결과인 ResultSet 을 객체로 변환한다.
    • 결과가 없으면 EmptyResultDataAccessException 예외가 발생한다.
    • 결과가 둘 이상이면 IncorrectResultSizeDataAccessException 예외가 발생한다

queryForObject() 인터페이스 정의

  • <T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args) throws DataAccessException;
    

findAll()

  • @Override
    public List<Item> findAll(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();
      
        String sql = "select id, item_name, price, quantity from item";
      
        //동적 쿼리
        if (StringUtils.hasText(itemName) || maxPrice != null) {
            sql += " where";
        }
        boolean andFlag = false;
      
        List<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());
    }
    
    • template.query()
      • 결과가 하나 이상일 때 사용한다.
      • RowMapper 는 데이터베이스의 반환 결과인 ResultSet 을 객체로 변환한다.
      • 결과가 없으면 빈 컬렉션을 반환한다.
      • 동적 쿼리에 대한 부분은 바로 다음에 다룬다.

query() 인터페이스 정의

  • <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) throws DataAccessException;
    

itemRowMapper()

  • private RowMapper<Item> itemRowMapper() {
        return ((resultSet, rowNum) -> {
            Item item = new Item();
            item.setId(resultSet.getLong("id"));
            item.setItemName(resultSet.getString("item_name"));
            item.setPrice(resultSet.getInt("price"));
            item.setQuantity(resultSet.getInt("quantity"));
            return item;
        });
    }
    
  • 데이터베이스의 조회 결과를 객체로 변환할 때 사용한다.

  • JDBC를 직접 사용할 때 ResultSet 를 사용했던 부분을 떠올리면 된다.

  • 차이가 있다면 다음과 같이 JdbcTemplate이 다음과 같은 루프를 돌려주고, 개발자는 RowMapper 를 구현해서 그 내부 코드만 채운다고 이해하면 된다.

    • while(resultSet  끝날  까지) {
      	rowMapper(rs, rowNum)
      }
      

2. JdbcTemplate 적용2 - 동적 쿼리 문제

  • 결과를 검색하는 findAll() 에서 어려운 부분은 사용자가 검색하는 값에 따라서 실행하는 SQL이 동적으로 달려져야 한다는 점이다.
    • 검색 조건이 없을 떄 : select id, item_name, price, quantity from item
    • 상품명( itemName )으로 검색 : select id, item_name, price, quantity from item where item_name like concat('%',?,'%')
    • 최대 가격( maxPrice )으로 검색 : select id, item_name, price, quantity from item where price <= ?
    • 둘 다 검색 : select id, item_name, price, quantity from item where item_name like concat('%',?,'%') and price <= ?
    • 결과적으로 4가지 상황에 따른 SQL을 동적으로 생성해야 하고, 떤 경우에는 where 를 앞에 넣고 어떤 경우에는 and 를 넣어야 하는지 등을 모두 계산해야 한다.

즉 매우 복잡하고, 오류가 날 가능성이 높다는 말

3. JdbcTemplate 적용3 - 구성과 실행

  • 일단 실제 코드가 동작하도록 구성하고 실행해보자

JdbcTemplateV1Config

  • JdbcTemplateItemRepositoryV1 를 사용하기 위해 dataSource 를 DI

  • package hello.itemservice.config;
      
    import javax.sql.DataSource;
      
    @Configuration
    @RequiredArgsConstructor
    public class JdbcTemplateV1Config {
      
        private final DataSource dataSource;
      
        @Bean
        public ItemService itemService() {
            return new ItemServiceV1(itemRepository());
        }
      
        @Bean
        public ItemRepository itemRepository() {
            return new JdbcTemplateItemRepositoryV1(dataSource);
        }
    }
    

ItemServiceApplication - 변경

  • //@Import(MemoryConfig.class)
    @Import(JdbcTemplateV1Config.class)
    @SpringBootApplication(scanBasePackages = "hello.itemservice.web")
    public class ItemServiceApplication {}
    

데이터베이스 접근 설정

  • src/main/resources/application.properties

  • spring.profiles.active=local
    spring.datasource.url=jdbc:h2:tcp://localhost/~/test
    spring.datasource.username=sa
    
    • 이렇게 설정만 하면 스프링 부트가 해당 설정을 사용해서 커넥션 풀과 DataSource , 트랜잭션 매니저를 스프링 빈으로 자동 등록한다.

로그 추가

  • dbcTemplate이 실행하는 SQL 로그를 확인하려면 application.properties 에 다음을 추가하면 된다. main , test 설정이 분리되어 있기 때문에 둘다 확인하려면 두 곳에 모두 추가해야 한다.

  • #jdbcTemplate sql log
    logging.level.org.springframework.jdbc=debug
    
    • Executing prepared SQL statement [select id, item_name, price, quantity from item] 처럼 출력된다.

4. JdbcTemplate - 이름 지정 파라미터 1(NamedParameterJdbcTemplate)

JdbcTemplate 문제점

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

  • String sql = "update item set item_name=?, price=?, quantity=? where id=?";
    template.update(sql,
     itemName,
     price,
     quantity,
     itemId);
    
    • 여기서는 itemName , price , quantity 가 SQL에 있는 ? 에 순서대로 바인딩 된다. 따라서 순서만 잘 지키면 문제가 될 것은 없다. 그런데 문제는 변경시점에 발생한다.
    • 누군가가 sql문을 = "update item set item_name=?, quantity=?, price=? where id=?"; 이렇게 변경한다면 심각한 문제가 발생한다.

개발을 할 때는 코드를 몇줄 줄이는 편리함도 중요하지만, 모호함을 제거해서 코드를 명확하게 만드는 것이 유지보수 관점에서 매우 중요하다.

이름 지정 바인딩(NamedParameterJdbcTemplate)

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

JdbcTemplateItemRepositoryV2

  • ? 대신 :itemName 과 같이 사용한다.

  • package hello.itemservice.repository.jdbcTemplate;
      
    import ...;
      
    /**
     * NamedParameterJdbcTemplate 사용
     * SqlParameterSource
     * - BeanPropertySqlParameterSource
     * - MapSqlParameterSource
     * Map
     *
     * BeanPropertyRowMapper
     *
     */
    @Slf4j
    public class JdbcTemplateItemRepositoryV2 implements ItemRepository {
      
      
        private final NamedParameterJdbcTemplate template;
      
        //JdbcTemplateItemRepositoryV2 생성자를 보면 의존관계 주입은 dataSource 를 받고 내부에서 NamedParameterJdbcTemplate 을 생성해서 가지고 있다. 스프링에서는 JdbcTemplate 관련 기능을 사용할 때 관례상 이 방법을 많이 사용한다.
        public JdbcTemplateItemRepositoryV2(DataSource dataSource) {
            this.template = new NamedParameterJdbcTemplate(dataSource);
        }
      
      
        @Override
        public Item save(Item item) {
            //? 대신 :itemName 과 같이 사용한다.
            String sql = "insert into item(item_name, price, quantity) " +
                    "values (:itemName, :price, :quantity)";
      
            //Item 의 필드이름대로 parameter 를 넘긴다.
            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{
                  
                //이렇게도 쓸 수 있다. 1개이므로 맵으로 만들어서 넘긴다.
                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();
            }
        }
      
        @Override
        public List<Item> findAll(ItemSearchCond cond) {
            String itemName = cond.getItemName();
            Integer maxPrice = cond.getMaxPrice();
      
            SqlParameterSource 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());
        }
      
        private RowMapper<Item> itemRowMapper() {
            return BeanPropertyRowMapper.newInstance(Item.class); //camel 변환 지원
        }
    }
    

5. JdbcTemplate - 이름 지정 파라미터 2

  • 파라미터를 전달하려면 Map 처럼 key , value 데이터 구조를 만들어서 전달해야 한다.
  • 여기서 key 는 ‘:파리이터이름’ 으로 지정한, 파라미터의 이름이고 , value 는 해당 파라미터의 값이 된다.
  • 다음 코드를 보면 이렇게 만든 파라미터( param )를 전달하는 것을 확인할 수 있다.
    • template.update(sql, param, keyHolder);

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

  • Map
  • SqlParameterSource
  • MapSqlParameterSource
  • BeanPropertySqlParameterSource

Map

  • 단순히 Map 을 사용한다.

  • findById() 코드에서 확인할 수 있다.

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

MapSqlParameterSource

  • Map 과 유사한데, SQL 타입을 지정할 수 있는 등 SQL에 좀 더 특화된 기능을 제공한다.

  • SqlParameterSource 인터페이스의 구현체이다.

  • MapSqlParameterSource 는 메서드 체인을 통해 편리한 사용법도 제공한다.

  • update() 코드에서 확인할 수 있다.

    • SqlParameterSource param = new MapSqlParameterSource()
          .addValue("itemName", updateParam.getItemName())
          .addValue("price", updateParam.getPrice())
          .addValue("quantity", updateParam.getQuantity())
          .addValue("id", itemId); //이 부분이 별도로 필요하다.
      template.update(sql, param);
      

BeanPropertySqlParameterSource

  • 자바빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성한다.
    • 예) ( getXxx() -> xxx, getItemName() -> itemName )
      • key=itemName, value=상품명 값
      • key=price, value=가격 값
  • SqlParameterSource 인터페이스의 구현체이다.
  • save() , findAll() 코드에서 확인할 수 있다.
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
  • 여기서 보면 BeanPropertySqlParameterSource 가 많은 것을 자동화 해주기 때문에 가장 좋아보이지만, BeanPropertySqlParameterSource 를 항상 사용할 수 있는 것은 아니다.
  • 예를 들어서 update() 에서는 SQL에 :id 를 바인딩 해야 하는데, update() 에서 사용하는 ItemUpdateDto 에는 itemId 가 없다. 따라서 BeanPropertySqlParameterSource 를 사용할 수 없고, 대신에 MapSqlParameterSource 를 사용했다.

BeanPropertyRowMapper

  • private RowMapper<Item> itemRowMapper() {
        return BeanPropertyRowMapper.newInstance(Item.class); //camel 변환 지원
    }
    
    • BeanPropertyRowMapper 는 ResultSet 의 결과를 받아서 자바빈 규약에 맞추어 데이터를 변환한다. 예를 들어서 데이터베이스에서 조회한 결과가 select id, price 라고 하면 다음과 같은 코드를 작성해준다. (실제로는 리플렉션 같은 기능을 사용한다.)

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

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

6. JdbcTemplate - 이름 지정 파라미터 3

  • 이름 지정 파라미터를 사용하도록 구성하고 실행해보자.

JdbcTemplateV2Config

  • 앞서 개발한 JdbcTemplateItemRepositoryV2 를 사용하도록 스프링 빈에 등록한다.

  • package hello.itemservice.config;
      
    import ...;
      
    import javax.sql.DataSource;
      
    @Configuration
    @RequiredArgsConstructor
    public class JdbcTemplateV2Config {
      
        private final DataSource dataSource;
      
        @Bean
        public ItemService itemService() {
            return new ItemServiceV1(itemRepository());
        }
      
        @Bean
        public ItemRepository itemRepository() {
            return new JdbcTemplateItemRepositoryV2(dataSource);
        }
    }
    

ItemServiceApplication - 변경

  • JdbcTemplateV2Config.class 를 설정으로 사용하도록 변경되었다.
    • @Import(JdbcTemplateV1Config.class) -> @Import(JdbcTemplateV2Config.class)
  • package hello.itemservice;
      
    import ...;
      
    @Import(JdbcTemplateV2Config.class)
    @SpringBootApplication(scanBasePackages = "hello.itemservice.web")
    public class ItemServiceApplication {
      
    	public static void main(String[] args) {
    		SpringApplication.run(ItemServiceApplication.class, args);
    	}
      
    	@Bean
    	@Profile("local")
    	public TestDataInit testDataInit(ItemRepository itemRepository) {
    		return new TestDataInit(itemRepository);
    	}
    }
    

7. JdbcTemplate - SimpleJdbcInsert

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

JdbcTemplateItemRepositoryV3

save 만 적용가능하다.

  • 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;
        }
    
    • SimpleJdbcInsert 를 주입받는다.

    • 이때 table 이름, keyColume 등을 넘긴다.

      • withTableName : 데이터를 저장할 테이블 명을 지정한다.

      • usingGeneratedKeyColumns : key 를 생성하는 PK 컬럼 명을 지정한다

      • this.jdbcInsert = new SimpleJdbcInsert(dataSource)
                        .withTableName("item")
                        .usingGeneratedKeyColumns("id");
        
    • Columns 도 넘길 수 있는데, 생략가능하다.

      • usingColumns : INSERT SQL에 사용할 컬럼을 지정한다. 특정 값만 저장하고 싶을 때 사용한다. 생략할 수 있다.

Config, ItemServiceApplication 설정 변경

  • V3 -> V4 로 변경

8. JdbcTemplate 기능 정리

주요 기능

  • JdbcTemplate : 순서 기반 파라미터 바인딩을 지원한다.
  • NamedParameterJdbcTemplate : 이름 기반 파라미터 바인딩을 지원한다. (권장)
  • SimpleJdbcInsert : INSERT SQL을 편리하게 사용할 수 있다.
  • SimpleJdbcCall : 스토어드 프로시저를 편리하게 호출할 수 있다.

조회

단건 조회 - 숫자 조회

  • 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 를 사용해야 한다. 여기서는 람다를 사용했다.

목록 조회 - 객체(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);
    }
    
    • 여러 로우를 조회할 때는 query() 를 사용하면 된다. 결과를 리스트로 반환한다.
    • 여기서는 RowMapper 를 분리했다. 이렇게 하면 여러 곳에서 재사용 할 수 있다.

변경(INSERT, UPDATE, DELETE)

  • 데이터를 변경할 때는 jdbcTemplate.update() 를 사용하면 된다. 참고로 int 반환값을 반환하는데, SQL 실행 결과에 영향받은 로우 수를 반환한다.

등록

  • jdbcTemplate.update(
     "insert into t_actor (first_name, last_name) values (?, ?)",
     "Leonor", "Watling");
    

수정

  • jdbcTemplate.update(
     "update t_actor set last_name = ? where id = ?",
     "Banjo", 5276L);
    

삭제

  • jdbcTemplate.update(
     "delete from t_actor where id = ?",
     Long.valueOf(actorId));
    

기타기능

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

DDL

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

스토어드 프로시저 호출

  • jdbcTemplate.update(
        "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
        Long.valueOf(unionId));
    

그런데 JdbcTemplate의 최대 단점이 있는데, 바로 동적 쿼리 문제를 해결하지 못한다는 점이다. 그리고 SQL을 자바 코드로 작성하기 때문에 SQL 라인이 코드를 넘어갈 때 마다 문자 더하기를 해주어야 하는 단점도 있다. 동적 쿼리 문제를 해결하면서 동시에 SQL도 편리하게 작성할 수 있게 도와주는 기술이 바로 MyBatis 이다.

댓글남기기