JPA를 사용할 경우 Entity의 Id 생성 전략이 IDENTITY일 경우 INSERT 구문이 실행될 때마다 SELECT 구문이 실행된다.
1개 row 작업의 경우 JPA를 사용함에 있어 큰 이슈가 없지만 대량의 배치성 작업의 경우 성능에 영향을 미친다.
이를 위해서 jdbcTemplate을 이용하여 jdbc의 prepareStatement와 rewriteBathchedStatements 옵션을 사용하여 성능 개선을 이룰수 있다.
그러나 다른 테이블의 데이터를 읽어서 새로운 테이블로 데이터를 migration 하는 경우 대량의 데이터를 한번에 읽을 수 없으므로 일반적으로 페이징 기법을 사용한다.
그러나 jdbc에서 페이지네이션(pagination) 쿼리를 만들기 귀찮아 jpa에서 PageRequest를 사용하여 데이터를 읽어 jdbc로 batchUpdate 하도록 구현하였다.
1. dependency 추가
dependencies {
runtimeOnly 'mysql:mysql-connector-java'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
}
2. application.yml 설정
spring:
jpa:
hibernate:
ddl-auto: none
show-sql: false
generate-ddl: false
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
properties:
hibernate:
format-sql: false
datasource:
username: {USER_NAME}
password: {USER_PASSWD}
driver-class-name: com.mysql.cj.jdbc.driver
url: {JDBC_URL: jdbc:mysql://******?autoReconnect=true&useUnicode=true&charsetEncoding=UTF-8}
hikari:
pool-name: SampleHikariPool
3. JpaConfig 작성
@Configuration
@EntityScan(basePackages={"ENTITY_PACKAGE_PATH"})
@EnableJpaRepositories(basePackages={"REPOSITORY_PACKAGE_PATH"})
public class JpaConfig {
// 생략
}
4. Entity 생성
@Getter
@ToString
@Entity
@Table(name="TABLE_NAME")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgConstructor
public class SampleEntity {
@Id
@GenerateValue(strategy = GenerationType.IDENTITY)
@Column(name = "TABLE_ID_COL")
private long id;
@Column(name = "name")
private String name;
// 생략
@Builder
public SampleEntity(String name, ...) {
this.name = name;
// 생략
}
}
5. JpaRepository 생성 (읽기 전용)
public interface SampleJpaRepository extends JpaRepository<SampleEntity, Long> {
// Page의 경우 totalPage와 totalElements를 구하기 위해 select 구문이 2번 실행된다.
// 띠라서 select 구문을 1번만 실행하려면 Slice 또는 List를 사용한다.
// List로 반환하여도 좋으나 작업의 끝을 알기 위해서 Slice로 선택하였다.
Slice<SampleEntity> findAll(Pageable pageable);
// 생략
}
6. JdbcRepository 생성 (쓰기 전용)
@Repository
@RequriedArgsConstructor
public class SampleJdbcReposiotry {
private final JdbcTemplate jdbcTemplate;
// rewriteBatchStatements=true 로 할 경우 INSERT... VALUES 를 사용하여야한다
// (VALUE 사용할 경우 단건씩 insert 실행됨)
// example : INSERT INTO `database`.`table` (COL1, COL2, ...) VALUES (?, ?, ...)
// 방식 1 : 가장 코드가 간결함
public void batchInsert(String insertQuery, List<SampleEntity> entities) {
jdbcTemplate.batchUpdate(insertQuery, entities, entities.size(), (ps, argument) -> {
ps.setLong(1, argument.getId());
ps.setString(2, argument.getName());
// 생략
});
}
// 방식 2 : BatchPreparedStatementSetter 사용
public void batchInsertOtherWay(String insertQuery, List<SampleEntity> entities) {
jdbcTemplate.batchUpdate(String insertQuery, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
SampleEntity entity = entities.get(i);
ps.setLong(1, entity.getId());
ps.setString(2, entity.getName());
// 생략
}
@Override
public int getBatchSize() {
return entities.size();
}
});
}
// 이외에도 ParameterPreparedStatementSetter 등으로 구현할 수 있다
}
7. service 구현
@Service
@ReuqiredArgsConstructor
public class SampleService {
private final SampleJpaRepository sampleJpaRepository;
private final SampleJdbcRepository sampleJdbcRepository;
public void dataSelectAndInsert() {
int page = 0;
int pageSize = 100;
boolean hasNext = true;
while(true) {
// Pageable 구현, 필요시 Sort 추가하면 됨
PageRequest pageRequest = PageRequest.of(page, pageSize);
// 주어진 페이지 적용된 select 실행
Slice<SampleEntity> entitySlice = sampleJpaRepository.findAll(pageRequest);
// data 개수가 0이면 while 종료
if(entitySlice.getContent().isEmpty()) {
break;
}
// 가져온 data insert 실행
sampleJdbcRepository.batchInsert(insertQuery, entitySlice.getContent());
// 마지막 페이지일 경우 hasNext=false 이므로 while 종료
hasNext = entitySlice.hasNext();
// 다음 페이징을 위한 세팅
page++;
}
}
}
8. 확인
// application.yml > jdbc-url 부분(jdbc:mysql://****?****)에 다음 파라미터를 추가한다
&rewriteBatchedStatements=true&profileSQL=true&logger=Slf4JLogger
'SpringBoot > JPA' 카테고리의 다른 글
[Enum] AttributeConverter 구현 (0) | 2022.04.12 |
---|