Skip to content

User Guide

Myeonghyeon-Lee edited this page May 26, 2022 · 52 revisions

Contents

Spring Data JDBC Reference Documentation

  1. Spring Boot Starter Data JDBC Plus SQL
  2. Spring Boot Starter Data JDBC Plus Repository

1. Spring Boot Starter Data JDBC Plus SQL

Spring Data JDBC 를 사용하면서, 직접 SQL 을 작성할 때 도움이 되는 기능들을 제공합니다.

1.1. Gradle / Maven Dependency

  • Gradle
dependencies {
    implementation("com.navercorp.spring:spring-boot-starter-data-jdbc-plus-sql:2.3.4")
}
  • Maven
<dependency>
	<groupId>com.navercorp.spring</groupId>
	<artifactId>spring-boot-starter-data-jdbc-plus-sql</artifactId>
	<version>2.3.4</version>
</dependency>
  • Java Codes
@Table("n_order")
@Data
public class Order {
    @Id
    @Column("order_no")
    private Long orderNo;

    @Column("price")
    private long price;

    @Column("purchaser_no")
    private String purchaserNo;
}

public interface OrderRepository extends CrudRepository<Order, Long>, OrderRepositoryCustom {
}

public interface OrderRepositoryCustom {
    List<Order> findByPurchaserNo(String purchaserNo);
}

public class OrderRepositoryImpl extends JdbcRepositorySupport<Order> implements OrderRepositoryCustom {
	private final OrderSql sqls;

	public OrderRepositoryImpl(EntityJdbcProvider entityJdbcProvider) {
		super(Order.class, entityJdbcProvider);
		this.sql = super.sqls(OrderSql::new);
	}

	@Override
	public List<Order> findByPurchaserNo(String purchaserNo) {
		String sql = this.sql.selectByPurchaserNo();
		return find(sql, mapParameterSource()
			.addValue("purchaserNo", purchaserNo));
	}
}
  • Groovy codes for SQL
implementation("org.codehaus.groovy:groovy:${groovyVersion}")
class OrderSql extends SqlGeneratorSupport {

    String selectByPurchaserNo() {
        """
        SELECT ${sql.columns(Order)}
        FROM n_order
        WHERE purchaser_no = :purchaserNo
        """
    }
}

1.2. JdbcRepositorySupport, JdbcDaoSupport

  • Customizing Individual Repositories 로 SQL 실행 코드를 직접 작성할 때 필요한 코드 및 지원 메소드를 제공한다.
  • 내부적으로 JdbcOperations(NamedParameterJdbcTemplate) 을 사용합니다.
# JdbcRepositorySupport
public class OrderRepositoryImpl extends JdbcRepositorySupport<Order> implements OrderRepositoryCustom {
	private final OrderSql sqls;

	public OrderRepositoryImpl(EntityJdbcProvider entityJdbcProvider) {
		super(Order.class, entityJdbcProvider);
		this.sql = super.sqls(OrderSql::new);
	}

	@Override
	public List<Order> findByPurchaserNo(String purchaserNo) {
		String sql = this.sql.selectByPurchaserNo();
		return find(sql, mapParameterSource()
			.addValue("purchaserNo", purchaserNo));
	}
}

# JdbcDaoSupport
public class OrderDaoImpl extends JdbcDaoSupport implements OrderRepositoryCustom {
	private final OrderSql sqls;

	public OrderDaoImpl(EntityJdbcProvider entityJdbcProvider) {
		this.sql = super.sqls(OrderSql::new);
	}

	@Override
	public List<OrderDto> selectByPurchaserNo(String purchaserNo) {
		String sql = this.sql.selectByPurchaserNo();
		return select(sql, mapParameterSource()
			.addValue("purchaserNo", purchaserNo),
                        OrderDto.class);
	}
}

Repository 와 Dao 를 개념적으로 분리해서 사용하도록 각각 제공합니다. JdbcRepositorySupport 와 JdbcDaoSupport 가 제공하는 기능은 유사하지만, 구현 관점에서 차이가 있습니다.

(1) 조회 실행 메소드 명

  • JdbcRepositorySupport 는 find 로 실행합니다.
  • JdbcDaoSupport 는 select 로 실행합니다.
  • 기능적인 차이는 없습니다.

(2) 반환 타입

  • JdbcRepositorySupport 는 Repository 에 대응되는 AggregateRoot(Entity) 타입을 기본 타입으로 사용합니다.
    • 생성자에서 AggregateRoot 타입을 설정합니다.
  • JdbcDaoSupport 는 다양한 QueryModel 을 반환할 수 있으므로, 기본 타입을 지정하지 않고 사용할 때 타입을 결정합니다.
# JdbcRepositorySupport
public List<Order> findByPurchaserNo(String purchaserNo) {
    String sql = this.sql.selectByPurchaserNo();
    return find(sql, mapParameterSource()
        .addValue("purchaserNo", purchaserNo));  // 생성자에서 설정한 Order 타입을 기본으로 사용한다.
}

# JdbcDaoSupport
public List<OrderDto> selectByPurchaserNo(String purchaserNo) {
    String sql = this.sql.selectByPurchaserNo();
    return select(sql, mapParameterSource()
        .addValue("purchaserNo", purchaserNo),
        OrderDto.class);  // Query 실행시 반환 타입을 전달한다.
}
  • JdbcRepositorySupport 를 사용하더라도, 다른 반환 타입을 전달할 수 있습니다.

(3) 조회 결과 매핑

  • JdbcRepositorySupport 는 AggregateResultSetExtractor 를 사용해서 조회 결과를 매핑합니다.
    • AggregateResultSetExtractor1 : N 조회 결과를 반환 타입(Aggregate) 결과에 매핑합니다.
  • JdbcDaoSupport 는 EntityRowMapper 를 사용해서 조회 결과를 매핑합니다.
    • EntityRowMapper 는 Row 단위 결과를 매핑합니다.

2가지 매핑 방식에 따라 작성해야 되는 SQL 에도 차이가 발생합니다.

JdbcRepositorySupport, JdbcDaoSupport 를 사용하더라도 다른 방식의 결과 매핑을 사용할 수 있습니다.

# JdbcRepositorySupport
public List<Order> findByPurchaserNo(String purchaserNo) {
    String sql = this.sql.selectByPurchaserNo();
    return find(sql, mapParameterSource()
        .addValue("purchaserNo", purchaserNo),
        this.getRowMapper());  // 조회 결과를 EntityRowMapper 로 매핑합니다.
}

# JdbcDaoSupport
public List<OrderDto> selectByPurchaserNo(String purchaserNo) {
    String sql = this.sql.selectByPurchaserNo();
    return select(sql, mapParameterSource()
        .addValue("purchaserNo", purchaserNo),
        this.getAggregateResultSetExtractor(OrderDto.class));  // 조회 결과를 AggregateResultSetExtractor 로 매핑합니다.
}

(4) 조회 결과를 AfterLoadEvent, AfterLoadCallback 로 발행 4.8. Entity Callbacks

  • JdbcRepositorySupport 는 조회 결과 Event 및 Callback 을 ApplicationEventPublisher, EntityCallbacks 에 발행합니다.

1.3. JdbcReactiveDaoSupport

  • JdbcDaoSupport 를 상속하고, Reactive(Flux) 조회 메소드를 제공한다.
  • JDBC Query 실행은 BLOCKING 이지만, Excel Download 와 같이 Asynchronous Stream 이 효과적일 때 사용할 수 있다.
  • CustomRepository 확장과 같이 사용하기 위해서는 spring-data-jdbc-plus-repositoryreactive-support 옵션이 활성화 되야 한다.Reactive Type Support

1.4. EntityRowMapper, AggregateResultSetExtractor

AggregateResultSetExtractor 와 EntityRowMapper 는 조회 결과 매핑 방식에 차이가 있으며, 연관관계가 존재할 경우 SQL 의 JOIN 구문도 이에 맞춰서 작성해야 합니다.

  • 1:1, 1:N 연관관계 엔티티
@Table("n_board")
public class Board {
    @Id
    private Long id;
    
    private String name;

    @Column("board_id")
    private Audit audit;    // 1:1 관계 (FK Column `n_audit.board_id`)

    @MappedCollection(idColumn = "board_id", keyColumn = "board_index")
    private List<Post> posts = new ArrayList<>();    // 1:N 관계 (FK Column `n_post.board_id`, Order By Column `n_post.board_index`)
}

@Table("n_audit")
public class Audit {
    @Id
    private Long id;

    private String name;
}

@Table("n_post")
public class Post {
   @Id
   private Long id;

   private String title;

   private String content;
}
  • 1:1 관계(n_board -> b_audit)는 JOIN 하더라도, 기준 테이블 "n_board" 의 ROW 결과가 중복되지 않습니다.
  • 1:N 관계(n_board -> n_post)는 JOIN 했을 때, n_post 의 연결 갯수만큼 기준 테이블 "n_board" 의 ROW 결과가 중복됩니다.

    (연관관계 데이터가 없을 수도 있다면, LEFT OUTER JOIN 으로 작성해야 합니다.)

1.4.1. EntityRowMapper

Spring Data JDBC 의 CrudRepository 에서 사용하는 RowMapper 입니다.

  • EntityRowMapper 는 SQL 조회 결과를 ROW 단위로 1:1 연관관계까지 매핑합니다.

  • 1:N 연관관계는 추가 SQL 을 실행(EAGER Fetch)해서 결과를 매핑합니다.

  • SQL 을 직접 작성할 때, 1:1 연관관계까지 SELECT 구문과 FROM 구문(JOIN 포함)을 작성해줘야 합니다.

  • 실행 SQL 작성

SELECT n_board.id AS id, n_board.name AS name, audit.id AS audit_id, audit.name AS audit_name
FROM n_board
LEFT OUTER JOIN n_audit AS audit
WHERE id = :id
  • n_post 과의 1:N 연관관계는 추가 SQL 이 자동으로 실행되면서 결과가 매핑됩니다.
SELECT n_post.id AS id, n_post.title AS title, n_post.content AS content
FROM n_post
WHERE n_post.board_id = :board_id
ORDER BY board_index
  • Spring Data JDBC CrudRepository 동작과 동일하며, N+1 쿼리가 실행될 수 있습니다.

1.4.2. AggregateResultSetExtractor

SELECT 조회 결과 중 1:N 관계 결과까지 Grouping 해서 결과 객체에 매핑합니다.

  • @EntityGraph 와 비슷하게 EAGER Fetch 없이 한번에 조회한 결과를 매핑합니다.
  • 1:N LEFT OUTER JOIN 결과를 1:N 결과 객체에 Grouping 해서 매핑합니다.
Board Audit Post
Board 1 Audit 1 Post 1
Board 1 Audit 1 Post 2
Board 1 Audit 1 Post 3
Board 2 Audit 2 Post 4
Board 2 Audit 2 Post 5
# 매핑 결과 
1. Board 1 / Audit 1 / Post 1, Post 2
2. Board 2 / Audit 2 / Post 4, Post 5
  • AggregateResultSetExtractor 를 사용하기 위해서 Grouping Entity 는 @Id 컬럼 필드는 필수입니다.

1.5. SqlGeneratorSupport (SqlAware)

SQL 작성을 지원하는 SqlProvider 를 주입 받아서 제공합니다. SqlProvidercolumns, tables, aggregateColumns, aggregateTables 메소드를 제공한다.

  • JdbcRepositorySupport, JdbcDaoSupport 의 sqls 메소드를 사용하여, SqlProvider 를 주입 받을 수 있습니다.
public class BoardRepositoryImpl extends JdbcRepositorySupport<Board> implements BoardRepositoryCustom {
	private final BoardSql sqls;

	public BoardRepositoryImpl(EntityJdbcProvider entityJdbcProvider) {
		super(Board.class, entityJdbcProvider);
		this.sql = super.sqls(BoardSql::new);    // BoardSql 생성 및 SqlProvider 객체 주입
	}
}

SQL 작성 클래스는 Groovy 나 Kotlin 과 같이 MultiLine String 을 지원하는 언어의 도움을 받을 수 있다.

Java 13 JEP 355: Text Blocks(Preview), Java 14 JEP 368: Text Blocks(Second Preview) 를 활용할 수도 있다.

# Groovy
class BoardSql extends SqlGeneratorSupport {

    /**
    *    SELECT n_board.id AS id, n_board.name AS name, audit.id AS audit_id, audit.name AS audit_name
    *    FROM n_board
    *    LEFT OUTER JOIN n_audit AS audit
    *    WHERE name = :name
    */
    String selectByName() {
        """
        SELECT ${sql.columns(Board)} 
        FROM ${sql.tables(Board)}
        WHERE name = :name
        """
    }

    /**
    *    SELECT n_board.id AS id, n_board.name AS name, audit.id AS audit_id, audit.name AS audit_name, post.id AS post_id, post.title AS post_title, post.content AS post_content
    *    FROM n_board
    *    LEFT OUTER JOIN n_audit AS audit
    *    LEFT OUTER JOIN n_post AS post
    *    WHERE name = :name
    */
    String selectAggregateByName() {
        """
        SELECT ${sql.aggregateColumns(Board)} 
        FROM ${sql.aggregateTables(Board)}
        WHERE name = :name
        """
    }
}
  • ${sql.columns(Board)}: 1:1 연관관계에 해당하는 SELECT Column 구문을 출력한다. (EntityRowMapper 에 대응)
  • ${sql.tables(Board)}: 1:1 연관관계에 해당하는 FROM JOIN 구문을 출력한다. (EntityRowMapper 에 대응)
  • ${sql.aggregateColumns(Board)}: 1:N 연관관계를 포함한 SELECT Column 구문을 출력한다. (AggregateResultSetExtractor 에 대응)
  • ${sql.aggregateTables(Board)}: 1:N 연관관계를 포함한 FROM JOIN 구문을 출력한다. (AggregateResultSetExtractor 에 대응)

1.6. SqlParameterSource

  • JdbcOperations 에서 SQL 파라미터를 바인딩하깅 위해 SqlParameterSource 를 제공해야 합니다.
  • SqlParameterSource 는 beanParameterSource, mapParameterSource, entityParameterSource, compositeSqlParameterSource 를 제공합니다.
# JdbcRepositorySupport
public List<Order> find(OrderCriteria criteria) {
    String sql = this.sql.select();
    return find(sql, beanParameterSource(criteria));  // beanParameterSource
}

public List<Order> findByPurchaserNo(String purchaserNo) {
    String sql = this.sql.selectByPurchaserNo();
    return find(sql, mapParameterSource()
        .addValue("purchaserNo", purchaserNo));  // mapParameterSource
}

public List<Order> findByExample(Order order) {
    String sql = this.sql.select();
    return find(sql, entityParameterSource(order));  // entityParameterSource
}

public List<Order> findByPurchaserNo(String purchaserNo, OrderCriteria criteria) {
    String sql = this.sql.selectExample;
    return find(sql, compositeSqlParameterSource(
        mapParameterSource().addValue("purchaserNo", purchaserNo),
        beanParameterSource(criteria)
    );  // compositeSqlParameterSource
}
  • beanParameterSource: parameter 객체의 getter 를 사용해서 binding 할 수 있다.
  • mapParameterSource: map 에 key/value 를 파라미터로 전달해서 binding 할 수 있다.
  • entityParameterSource: Spring Data JDBC 의 매핑 정보를 사용해서 파라미터 binding 할 수 있다. (@Column)
  • compositeSqlParameterSource: 복합 SqlParameterSource 를 조합한 파라미터로 전달해서 binding 할 수 있다.

1.7. SingleValueSelectTrait

count 결과와 같이 단일 컬럼 결과를 매핑하는 JdbcOperations 호출할 때 SingleValueSelectTrait 을 사용할 수 있습니다.

public class OrderRepositoryImpl extends JdbcRepositorySupport<Order> 
    implements OrderRepositoryCustom, SingleValueSelectTrait {
	private final OrderSql sqls;

	public OrderRepositoryImpl(EntityJdbcProvider entityJdbcProvider) {
		super(Order.class, entityJdbcProvider);
		this.sql = super.sqls(OrderSql::new);
	}

	@Override
	public Long countByPurchaserNo(String purchaserNo) {
		String sql = this.sql.countByPurchaserNo();
		return selectSingleValue(sql, mapParameterSource()
			.addValue("purchaserNo", purchaserNo),
                       Long.class);
	}
}
class OrderSql extends SqlGeneratorSupport {
    String countByPurchaserNo() {
        """
        SELECT count(id)
        FROM n_order
        WHERE purchaser_no = :purchaserNo
        """
    }

1.8. SqlParameterSourceFactory

SqlParameterSource (beanParameterSource, mapParameterSource, entityParameterSource) 를 생성한다. DefaultSqlParameterSourceFactory (Default) 와 EntityConvertibleSqlParameterSourceFactory 가 제공된다. SqlParameterSourceFactory 에 등록된 Converter 등의 설정은 Spring Data JDBC CrudRepository 와 별도로 설정됩니다.

1.8.1 DefaultSqlParameterSourceFactory

  • 기본 JDBC Parameter 컨버팅 전략 사용 (Default 설정)
  • 생성한 ParameterSource 는 JdbcDriver 의 Type Converting 전략에 의존한다.

1.8.2 EntityConvertibleSqlParameterSourceFactory

  • 몇가지 타입에 대한 ParameterSource Type Converter 등록 지원
@Configuration
public class JdbcConfig extends JdbcPlusSqlConfiguration {
    @Bean
    @Override
    public SqlParameterSourceFactory sqlParameterSourceFactory(
        JdbcMappingContext jdbcMappingContext, JdbcConverter jdbcConverter, Dialect dialect) {

	return new EntityConvertibleSqlParameterSourceFactory(
		this.parameterSourceConverter(),
                jdbcMappingContext,
                jdbcConverter,
                dialect.getIdentifierProcessing());
    }

    private ConvertibleParameterSourceFactory parameterSourceConverter() {
        JdbcParameterSourceConverter converter = new DefaultJdbcParameterSourceConverter();
        ConvertibleParameterSourceFactory parameterSourceFactory = new ConvertibleParameterSourceFactory(converter, null);
        parameterSourceFactory.setPaddingIterableParam(true);
        return parameterSourceFactory;
    }
}

1.8.3 ConvertibleParameterSourceFactory

JdbcParameterSourceConverter, FallbackParameterSource, PaddingIterable 설정을 적용한 SqlParameterSource 를 생성한다.

  • JdbcParameterSourceConverter: ParameterSource Converting 에 적용할 Converter 를 등록한다.
  • FallbackParameterSource: SQL Binding 에 필요한 Parameter 가 ParameterSource 에 존재하지 않을 때 처리할 전략을 주입한다.
  • PaddingIterable: Iterable(List, Set, Collection) 파라미터 바인딩시 SQL Parsing 비용을 줄이기 위해 바인딩 파라미터 갯수를 균일하게 조정한다. 특히 WHERE IN 조건에 주로 사용된다.
    • parameterSourceFactory.setPaddingIterableParam(true); 로 padding 설정을 활성화 할 수 있다.
    • this.setPaddingIterableBoundaries(...); 로 padding scope 패턴을 지정할 수 있다.

    default boundaries: new int[]{0, 1, 2, 3, 4, 8, 16, 32, 50, 100, 200, 300, 500, 1000, 1500, 2000}

SELECT *
FROM n_order
WHERE id in (:ids)
mapParameterSource()
    .add("ids", Arrays.asList("1", "2", "3", "4", "5", "6"));

-->

SELECT *
FROM n_order
WEHERE id IN (?, ?, ?, ?, ?, ?, ?, ?)

-->

SELECT *
FROM n_order
WEHERE id IN ("1", "2", "3", "4", "5", "6", "6", "6")

Spring Data JDBC CrudRepository 에는 적용되지 않습니다.

1.8.4 JdbcParameterSourceConverter (DefaultJdbcParameterSourceConverter)

Default Converter 가 내장되어 있으며, 추가 타입 Converter, Unwrapper 를 등록할 수 있다.

  • Default Converter

    • InstantParameterTypeConverter: Instant 타입을 Date 로 변환한다.
    • LocalDateTimeParameterTypeConverter: LocalDateTime 타입을 Date 로 변환한다.
    • LocalDateParameterTypeConverter: LocalDate 타입을 Date 로 변환한다.
    • ZonedDateTimeParameterTypeConverter: ZonedDateTime 타입을 Date 로 변환한다.
    • UuidToStringTypeConverter: UUID 타입을 String 으로 변환한다. (VARCHAR(36))
    • EnumToNameConverter: ENUM 타입을 name 으로 변환한다.
  • Unwrapper

    • AggregateReference 와 같이 Wrapping 된 값을 Unwrapping 할 수 있는 Unwrapper 를 등록할 수 있다.
    • Unwrapper 가 적용되면, Unwrapping 한 결과를 Converter 로 한번 더 변환 동작할 수 있다.

Converter 와 Unwrapper 는 정확한 타입에만 매칭되서 적용된다. 매칭 조건을 직접 작성할 필요가 있다면, ConditionalConverter 와 ConditionalUnwrapper 를 등록할 수 있다. matches 메소드를 구현하면 조건에 맞는 Converter 와 Unwrapper 가 선택된다. Spring Data JDBC CrudRepository 에는 적용되지 않습니다.

2. Spring Boot Starter Data JDBC Plus Repository

  • Spring Data JDBC 의 CrudRepository 에 확장 및 추가 기능을 제공합니다.

2.1. JdbcRepository

  • Spring Data JDBC 의 CrudRepository 는 save 메소드를 제공 합니다. (merge)
  • @Id 생성 전략에 따라 insert 를 직접 호출할 필요가 있습니다.
  • insert / update 를 직접 호출할 때 JdbcRepository 를 상속해서 기능을 제공할 수 있습니다.
  • Spring Data JDBC 에서는 insert / update 메소드를 직접 제공할 계획이 없습니다. DATAJDBC-282
  • spring-data-jdbc-plus-repository dependency 를 가지면, Entity 에 @Table 을 선언해야 합니다.
  • Gradle
dependencies {
    implementation("com.navercorp.spring:spring-boot-starter-data-jdbc-plus-repository:2.3.4")
}
  • Maven
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>com.navercorp.spring:spring-boot-starter-data-jdbc-plus-repository</artifactId>
	<version>2.3.4</version>
</dependency>
  • Java Codes
@Table("n_order")
@Data
public class Order {
    @Id
    @Column("order_no")
    private Long orderNo;

    @Column("price")
    private long price;

    @Column("purchaser_no")
    private String purchaserNo;
}

public interface OrderRepository extends JdbcRepository<Order, Long> {
}

@Service
public class OrderService {
    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }

    public Order save(Order order) {
        return this.repository.save(order);
    }

    // JdbcRepository 추가된 insert / update 메소드를 직접 사용
    public Order insert(Order order) {
        return this.repository.insert(order);
    }
  
    public Order update(Order order) {
        return this.repository.update(order);
    }
}

2.2. Reactive Type Support

  • Spring Data JDBC 에서는 CrudRepository 확장 메소드 반환 타입으로 Reactive(Flux, Mono) 타입을 허용하지 않는다.
  • 간단한 설정으로 Reactive(Flux, Mono) 타입을 반환타입으로 가지는 확장 메소드를 선언할 수 있도록 지원한다.
spring:
  data:
    jdbc:
      plus:
        repositories:
          reactive-support: true
public interface OrderRepository extends CrudRepository<Order, Long>, OrderRepositoryCustom {
}

public interface OrderRepositoryCustom {
    Flux<Order> findOrders(String purchaserId);
}