ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [김영한 스프링] 06. JdbcTemplate - SimpleJdbcInsert & 기능 정리
    Spring/스프링 DB 2편 - 데이터 접근 활용 기술 2024. 1. 9. 05:24

    JdbcTemplate - SimpleJdbcInsert

     

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

     

     

    JdbcTemplateItemRepositoryV3

    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.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 JdbcTemplate template;
        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;
        }
    
        @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();
            }
    
    
        }
    
        @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 변환 지원
        }
    }

    기본

    • JdbcTemplateItemRepositoryV2를 복사해서 JdbcTemplateItemRepositoryV3 만듦
    • JdbcTemplateItemRepositoryV3은 ItemRepository 인터페이스를 구현했다.
    • this.jdbcInsert = new SimpleJdbcInsert(dataSource) : 생성자를 보면 의존관계 주입은 dataSource를 받고 내부에서 SimpleJdbcInsert을 생성해서 가지고 있다. 스프링에서는 JdbcTemplate 관련 기능을 사용할 때 관례상 이 방법을 많이 사용한다.
      • 물론 SimpleJdbcInsert을 스프링 빈으로 직접 등록하고 주입받아도 된다.

     

     

    SimpleJdbcInsert

    this.jdbcInsert = new SimpleJdbcInsert(dataSource)
        .withTableName("item")
        .usingGeneratedKeyColumns("id");
    //    .usingColumns("item_name", "price", "quantity"); //생략 가능
    • withTableName : 데이터를 저장할 테이블 명을 지정한다.
    • usingGeneratedKeyColumns : key를 생성하는 PK 컬럼 명을 지정한다.
    • usingColumns : INSERT SQL에 사용할 컬럼을 지정한다. 특정 값만 저장하고 싶을 때 사용한다. 생략할 수 있다

     

    SimpleJdbcInsert는 생성 시점에 데이터베이스 테이블의 메타 데이터를 조회한다. 따라서 어떤 컬럼이 있는지 확인할 수 있으므로 usingColumns을 생략할 수 있다. 만약 특정 컬럼만 지정해서 저장하고 싶다면 usingColumns를 사용하면 된다.

     

    애플리케이션을 실행해 보면 SimpleJdbcInsert이 어떤 INSERT SQL을 만들어서 사용하는지 로그로 확인할 수 있다.

    DEBUG 39424 --- [ main] o.s.jdbc.core.simple.SimpleJdbcInsert : Compiled insert object: insert string is [INSERT INTO item (ITEM_NAME, PRICE, QUANTITY) VALUES(?, ?, ?)]

     

    save()

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

    public Item save(Item item) {
        SqlParameterSource param = new BeanPropertySqlParameterSource(item);
        Number key = jdbcInsert.executeAndReturnKey(param);
        item.setId(key.longValue());
        return item;
    }

     

     

    JdbcTemplateV3Config

    package hello.itemservice.config;
    
    import hello.itemservice.repository.ItemRepository;
    import hello.itemservice.repository.jdbctemplate.JdbcTemplateItemRepositoryV3;
    import hello.itemservice.service.ItemService;
    import hello.itemservice.service.ItemServiceV1;
    import lombok.RequiredArgsConstructor;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    
    @Configuration
    @RequiredArgsConstructor
    public class JdbcTemplateV3Config {
    
        private final DataSource dataSource;
    
        @Bean
        public ItemService itemService() {
            return new ItemServiceV1(itemRepository());
        }
    
        @Bean
        public ItemRepository itemRepository() {
            return new JdbcTemplateItemRepositoryV3(dataSource);
        }
    }

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

     

     

    ItemServiceApplication - 변경

    package hello.itemservice;
    
    import hello.itemservice.config.*;
    import hello.itemservice.repository.ItemRepository;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Import;
    import org.springframework.context.annotation.Profile;
    
    
    //@Import(MemoryConfig.class)
    //@Import(JdbcTemplateV1Config.class)
    //@Import(JdbcTemplateV2Config.class)
    @Import(JdbcTemplateV3Config.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);
    	}
    
    }

    • JdbcTemplateV3Config.class를 설정으로 사용하도록 변경되었다.
      • @Import(JdbcTemplateV2Config.class) -> @Import(JdbcTemplateV3Config.class)

     

     

    실행

     

     

    JdbcTemplate 기능 정리

     

    주요 기능

    JdbcTemplate이 제공하는 주요 기능은 다음과 같다.

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

     

    참고
    스토어드 프로시저를 사용하기 위한 SimpleJdbcCall에 대한 자세한 내용은 다음 스프링 공식 메뉴얼을 참고하자.
    https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbcsimple-jdbc-call-1

     

     

    JdbcTemplate 사용법 정리

    JdbcTemplate에 대한 사용법은 스프링 공식 메뉴얼에 자세히 소개되어 있다. 여기서는 스프링 공식 메뉴얼이 제공하는 예제를 통해 JdbcTemplate의 기능을 간단히 정리해 보자.

     

    참고
    스프링 JdbcTemplate 사용 방법 공식 메뉴얼
    https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbcJdbcTemplate

     

     

    조회

    단건 조회 - 숫자 조회

    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);
    }

    여러 로우를 조회할 때는 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));

     

     

    정리

    실무에서 가장 간단하고 실용적인 방법으로 SQL을 사용하려면 JdbcTemplate을 사용하면 된다.

    JPA와 같은 ORM 기술을 사용하면서 동시에 SQL을 직접 작성해야 할 때가 있는데, 그때도 JdbcTemplate을 함께 사용하면 된다.

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

     

    동적 쿼리 문제를 해결하면서 동시에 SQL도 편리하게 작성할 수 있게 도와주는 기술이 바로 MyBatis이다.

     

    참고
    JOOQ라는 기술도 동적쿼리 문제를 편리하게 해결해 주지만 사용자가 많지 않아서 강의에서 다루지는 않는다.

     

     

    출처 :  https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard

     

    스프링 DB 2편 - 데이터 접근 활용 기술 강의 - 인프런

    백엔드 개발에 필요한 DB 데이터 접근 기술을 활용하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔드 개발자

    www.inflearn.com

Designed by Tistory.