[MySQL8.0] row constructor 여러 레코드를 서로 다른 값으로 업데이트하는 방법

2022. 12. 23. 18:51데이터베이스

 

 

개인 게시판 프로젝트를 진행하고 있습니다.

 

구현한 기술 요구사항 중, 각 게시글의 조회수를 요청마다 한 건씩 바로 업데이트 하는 것이 아니라 어딘가에 모아뒀다가 한번에 Update쿼리를 쏟아내는 방식으로 벌크 업데이트 처리를 한 구현이 있습니다.

 

이 상황에서, Update 쿼리를 한 트랜잭션에서 모아서 쏴주기는 하지만 단일 트랜잭션에 Update쿼리가 너무 많습니다.

 

그래서 인프런 JPA의 어머니에게 질문을 남겼던 기억이 있습니다.

아래 이미지는 당시에 답변받은 내용입니다.

7월 31일자 답변 https://www.inflearn.com/questions/609450/%EB%B2%8C%ED%81%AC-%EB%B0%B0%EC%B9%98-update-%EC%BF%BC%EB%A6%AC-%EA%B4%80%EB%A0%A8-%EC%A7%88%EB%AC%B8%EC%9E%85%EB%8B%88%EB%8B%A4

 

아.. 그렇구나.. 김영한님이 안된다면 안되는거구나 하고 그냥 넘겼었습니다.

For문 돌리면서 그냥 하나하나 update를 수행했습죠.

 

 

 


 

 

그런데 오늘 책을 읽다가 엄청난 것을 발견했습니다.

 

RealMySQL8.0 2권 164페이지

 

 

MySQL8.0 버전부터는 레코드 생성 문법으로 서로 다른값으로 레코드를 따로따로 업데이트 할 수 있다고 합니다!

 

 

 


 

 

 

적용해 보겠습니다

 

제 기존 코드는 이렇습니다

private void hitCountBulkUpdate(Map<Long, Long> postIdsAndHitCounts) {
    for (Long postId : postIdsAndHitCounts.keySet()) {
        postRepository.updateHitCountByPostId(postId, postIdsAndHitCounts.get(postId));
    }
}

@Modifying
@Query("update Post p set p.hitCount = p.hitCount + :hitCount where p.postId =:postId")
void updateHitCountByPostId(@Param("postId") Long postId, @Param("hitCount") Long hitCount);

 

상세 구현은 제쳐두고, `Map`에 담긴 `postId와 `hitCounts`를 for문을 돌며 update 메서드를 한번씩 호출하는 구조입니다.

 

이 코드를 수정해보겠습니다.

 

 

구현하다보니 제약사항이 세 가지 발생했습니다.

 

1. JPA에서 지원하지 않는 문법

2. Spring Data JDBC에서도 지원하지 않는 문법

3. H2과 호환되는 문법이 아니라서 Junit 테스트 불가 

 

그래서, 그냥 jdbc template를 사용하기로 했습니다.

작성해놓은 테스트가 돌아가지 않는건 좀 치명적이지만 학습용 프로젝트니 그냥 넘어갑니다.

 

 

build.gradle에 의존성을 추가합니다.

implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'

 

 

jdbc 레포지토리를 생성합니다. 

package com.zzangmin.gesipan.component.basiccrud.repository.jdbc;

import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@RequiredArgsConstructor
@Repository
public class PostJdbcRepository {

    private final JdbcTemplate jdbcTemplate;

    public int bulkUpdatePostHitCounts(String updateRows) {
        String SQL = "update post p1 "
                + " inner join "
                + " (values "
                + updateRows
                + " temptable_post (post_id, hit_count) "
                + " on p1.post_id = temptable_post.post_id "
                + " set p1.hit_count=p1.hit_count+temptable_post.hit_count";

		return jdbcTemplate.update(SQL);
    }

}

 

 

서비스단의 코드도 간략해집니다.

레포지토리의 update 메서드를 단 한번만 호출하면 되기 때문이지요~

    @Transactional
    @Scheduled(fixedRate = RedisKeyUtils.SCHEDULED_INCREASE_SECONDS, timeUnit = TimeUnit.SECONDS)
    public void scheduledIncreasePostHitCounts() {
        ...
        ...
        ...
        String updateRows = generateRowConstructorUpdateString(cursor);
        if (updateRows.isBlank()) {
            return;
        }

        postJdbcRepository.bulkUpdatePostHitCounts(updateRows);
    }

참 마음에 듭니다

 

 

 

updateRows는 내부적으로 어디서 만들어 넣어주는데, 최종적으로 생성되는 SQL은 다음과 같습니다.

update post p1  inner join  (values row(1,15), row(2,3)) temptable_post (post_id, hit_count)  on p1.post_id = temptable_post.post_id  set p1.hit_count=p1.hit_count+temptable_post.hit_count

세부 구현코드도 있긴 한데 보여드리기 부끄럽네요. 알아서... 잘.. 만들어보세요... stringbuilder... sb...sb..

 

 

여튼 이렇게 만들면 됩니다. 

끝~

 

삽질 오래했는데 허무해라 허참

 

row constructor 네 이놈..