[Spring DB 2편] 2. JdbcTemplate

HJ·2023년 2월 2일
0

Spring DB 2편

목록 보기
2/11
post-custom-banner

김영한 님의 스프링 DB 2편 - 데이터 접근 활용 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard


1. JdbcTemplate

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

  • 템플릿 콜백 패턴을 사용해서 JDBC를 직접 사용할 때의 반복 작업을 대신 처리해준다

    • 커넥션 획득 / Statement 준비 및 실행 / 결과를 반복하도록 루프 실행 / 커넥션, Statement, ResultSet 종료

    • 트랜잭션을 위한 커넥션 동기화, 예외 발생 시 스프링 예외 변환기 실행

  • 개발자는 SQL을 작성하고, 전달할 파라미터를 정의, 응답 값을 매핑하기만 하면 된다

  • https://docs.spring.io/spring-framework/docs/current/reference/html/dataaccess.html#jdbc-JdbcTemplate




2. JdbcTemplate 사용하기

2-1. JdbcTemplate 생성

public class JdbcTemplateItemRepositoryV1 implements ItemRepository {

    private final JdbcTemplate template;

    public JdbcTemplateItemRepositoryV1(DataSource dataSource) {
        this.template = new JdbcTemplate(dataSource);
    }
    ...
}
  • JdbcTemplate 을 사용하기 위해 필드에 선언이 필요

  • JdbcTemplate 를 생성할 때 DataSource 를 받아야 한다

    • 커넥션 생성 등을 하기 때문에 DataSource가 필요
  • JdbcTemplateItemRepositoryV1 의 생성자에서 dataSource 를 외부에서 주입 받고 생성자 내부에서 JdbcTemplate 을 생성


2-2. 자동 등록 복습

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
  • application.properties에 위처럼 설정하면 스프링부트가 해당 설정을 사용해서 커넥션 풀과 DataSource , 트랜잭션 매니저를 스프링 빈으로 자동 등록

  • 자동 등록된 DataSourceJdbcTemplateItemRepositoryV1 의 생성자에 주입된다


2-3. 업데이트

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);
}
  • update() : 데이터를 변경할 때 사용 ( insert, update, delete )

  • sql의 ? 에 바인딩할 파라미터를 update() 에 순차적으로 적어준다


2-4. KeyHolder

public Item save(Item item) {

    String sql = "insert into item(item_name, price, quantity) values(?, ?, ?)";

    KeyHolder keyHolder = new GeneratedKeyHolder();
    template.update(connection -> {
        // 자동 증가 키
        PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
        ps.setString(1, item.getItemName());
        ...
        return ps;
    }, keyHolder);

    long key = keyHolder.getKey().longValue();
    item.setId(key);
    return item;
}
  • Item의 id 값은 DB에 저장할 때 DB에서 생성하도록 설정했다

  ➜ id 값 없이 Item 객체 생성 후 DB에서 생성된 값으로 id 값 설정이 필요

  • DB에 item을 저장한 후 생성된 id 값을 가져오기 위해 KeyHolder를 사용

  • KeyHolderconnection.prepareStatement(sql, new String[]{"id"}) 를 사용해서 id 를 지정해주면 INSERT 쿼리 실행 이후에 DB에서 생성된 ID 값을 조회할 수 있다


2-5. 단일 데이터 조회

public Optional<Item> findById(Long id) {

    String sql = "select id, item_name, price, quantity where id=?";

    try {
        Item item = template.queryForObject(sql, itemRowMapper(), id);
        return Optional.of(item);
    } catch (EmptyResultDataAccessException e) {
        return Optional.empty();
    }
}

private RowMapper<Item> itemRowMapper() {
    return( (rs, rowNum) -> {
        Item item = new Item();
        item.setId(rs.getLong("id"));
        ...
        return item;
    });
}
  • queryForObject() 를 통해 객체를 하나 가져올 수 있다

    • 반환 결과인 ResultSet 을 객체로 만드는 RowMapper 메서드가 필요
  • 결과가 없으면 EmptyResultDataAccessException 예외가 발생

  • 결과가 둘 이상이면 IncorrectResultSizeDataAccessException 예외가 발생

  • itemRowMapper() : 결과인 ResultSet 을 Item 객체로 바꾸는 메서드


2-6. RowMapper

  • 데이터베이스의 조회 결과인 ResultSet 을 객체로 변환할 때 RowMapper 를 사용

  • JdbcTemplate 이 자동으로 ResultSet을 반복하면서 rowMapper 를 실행

  • but> 결과를 객체로 매핑하는 코드는 직접 구현해야한다


2-7. 여러 데이터 조회

public List<Item> findAll(ItemSearchCond cond) {
    // 동적 쿼리 코드 생략
    ...
    return template.query(sql, itemRowMapper(), param.toArray());
}
  • query() : 결과가 하나 이상일 때 사용

  • 결과가 없으면 빈 컬렉션을 반환한다


2-8. 동적 쿼리 문제

  • findAll() 은 사용자가 검색하는 값에 따라서 실행하는 SQL이 동적으로 달려져야한다

    • ex> 검색 조건 없음, 상품명만으로 검색, 가격만으로 검색, 상품명과 가격으로 검색
  • 위의 상황을 고려하여 where 절을 넣는 경우, and 를 사용하는 경우 모두 계산해야하고 상황에 맞는 파라미터도 생성해야한다

  • 이처럼 고려해야될 사항이 너무 많아 JdbcTemplate 에서 동적 쿼리를 작성하기 어렵다

    • Mybatis 의 경우 동적 쿼리를 쉽게 작성할 수 있다



3. NamedParameterJdbcTemplate

3-1. 필요성

  • 2-3 번의 update() 를 보면 파라미터가 순서대로 sql의 ? 에 바인딩된다

  • but> 파라미터가 많아지면 순서를 잘못 넣어 심각한 문제가 발생할 수도 있다

   NamedParameterJdbcTemplate라는 이름을 지정해서 파라미터를 바인딩하는 기능 제공


3-2. 생성

public class JdbcTemplateItemRepositoryV2 implements ItemRepository {

    private final NamedParameterJdbcTemplate template;

    public JdbcTemplateItemRepositoryV2(DataSource dataSource) {
        this.template = new NamedParameterJdbcTemplate(dataSource);
    }
}
  • JdbcTemplate ➜ NamedParameterJdbcTemplate

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

// 변경 전 SQL
String sql = "update item set item_name = ?, price=?, quantity=? where id=?";


// 변경 후 SQL
String sql = "update item set item_name = :itemName, price=:price, quantity=:quantity where id=:id";


// 변경 전 호출
template.update(sql, updateParam.getItemName(), updateParam.getPrice(), updateParam.getQuantity(), itemId);


// 변경 후 호출
template.update(sql, param);
  • SQL의 ?:파라미터이름 형태로 변경

  • 메서드를 호출할 때 값을 직접 전달하지 않고 값을 가진 파라미터를 전달

  • 파라미터를 전달하려면 Map 처럼 key, value 데이터 구조를 만들어서 전달해야한다

  • key는 :파라미터이름으로 지정한 파라미터 이름, value는 해당 파라미터의 값

  • 이름 지정 바인딩에서 사용하는 파라미터

    • Map 객체를 직접 사용

    • SqlParameterSource : 인터페이스

      • MapSqlParameterSource

      • BeanPropertySqlParameterSource


3-4. Map 객체 사용

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

3-5. MapSqlParameterSource

SqlParameterSource param = new MapSqlParameterSource()
        .addValue("itemName", updateParam.getItemName())
        ...
        .addValue("id", itemId);

template.update(sql, param);
  • SqlParameterSource 의 구현체

  • Map과 유사한데 SQL 타입을 지정할 수 있는 등 SQL에 특화

  • 메서드 체인을 통해 편리하게 사용할 수 있다


3-6. BeanPropertySqlParameterSource

SqlParameterSource param = new BeanPropertySqlParameterSource(item);

template.update(sql, param);
  • SqlParameterSource 의 구현체

  • 자바 빈 프로퍼티 규약을 통해 자동으로 파라미터 객체를 생성

  • ex> getItemName()ItemName

    • ItemName을 key로, getItemName() 실행 결과를 value로 가짐

3-7. BeanPropertyRowMapper

private RowMapper<Item> itemRowMapper() {
    return BeanPropertyRowMapper.newInstance(Item.class);   // camel 변환 지원
}
  • ResultSet 의 결과를 받아서 자바 빈 규약에 맞춰 데이터를 변환해준다

  • ex> select id, price from item

    • DB에서 조회한 결과 이름으로 setId(), setPrice() 처럼 자바 빈 프로퍼티 규약에 맞는 메서드를 호출

  • DB의 칼럼 이름과 객체의 필드 이름이 다른 경우 별칭 as 를 활용한다

    • ex> select item_name인 경우 setItem_name() 메서드가 없는 문제

    • select item_name as itemName 처럼 별칭을 활용하면 된다

  • camel 변환 지원

    • 자바는 camelCase 표기법을 사용, 관계형 DB는 snake_case 를 사용

    • BeanPropertyRowMapper 는 snake_case 표기법을 camelCase 로 자동으로 변환해준다

    • 따라서 item_name 으로 조회해도 자동으로 setItemName() 에 값이 들어간다




4. SimpleJdbcInsert

4-1. SimpleJdbcInsert 생성

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"); // 생략 가능 
    }
}
  • insert 쿼리를 직접 작성하지 않도록 SimpleJdbcInsert 라는 기능을 제공

  • withTableName() : 테이블명

  • usingGeneratedKeyColumns() : 자동으로 생성되는 PK 가 있는 경우 추가

  • usingColumns() : 어떤 칼럼을 사용하는지

  • SimpleJdbcInsert 는 생성 시점에 dataSource 를 통해서 테이블명을 가지고 DB에서 메타데이터를 읽기 때문에 어떤 칼럼이 있는지 알 수 있어서 usingColumns() 은 생략 가능하다


4-2. 활용

public Item save(Item item) {

    SqlParameterSource param = new BeanPropertySqlParameterSource(item);

    Number key = jdbcInsert.executeAndReturnKey(param);

    item.setId(key.longValue());

    return item;
}



5. 참고

  • execute() : DDL 과 같은 임의의 SQL을 실행할 때 사용

  • update("call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",Long.valueOf(unionId));
    : 스토어드 프로시저 호출

  • SimpleJdbcCall : 스토어드 프로시저를 편리하게 호출할 수 있다

post-custom-banner

0개의 댓글