[QueryDSL] 동적 업데이트

반응형

Introduction

서비스 로직에서 특정 데이터를 업데이트, 즉 변경 하기 위해서 JPA 를 사용할 경우 일반적으로 DirtyChecking 을 통해 업데이트를 진행한다. 하지만 다수의 데이터를 일괄적으로 변경을 해야하는 경우에는 다수의 조회 및 업데이트 쿼리가 발생하기 때문에 성능적으로 좋지 않다. 그런 경우 JPQL 을 통해 업데이트 쿼리를 직접 작성하여 Bulk 업데이트를 진행하기도 하는데. Querydsl를 통해 문자열로 이루어진 JPQL 업데이트 쿼리의 Set 파라미터를 동적으로 업데이트를 진행할 수 있다.

Bulk update

1.JPQL

JPQL을 통한 기본적은 Bulk Update는 단순한 문자열 형태로 진행된다. 아래 코드는 Repository에서 나이와 유저아이디로 해당 유저들의 나이를 일괄 변경한다.

@Transactional
@Modifying
@Query("update Member m set m.age = :age where m.id in (:meberIds)")
void updateAgeByMemberIds(Integer age, List<Long> memberIds);

 

 

만약 나이뿐만이 아니라 주소가 달라질 경우 새로운 함수를 만들어야 한다. 이렇게 Set 해주는 파라미터의 갯수의 따라서 함수가 계속 추가적으로 쌓이는 불편함이 생긴다. 

@Transactional
@Modifying
@Query("update Member m set m.age = :age where m.id in (:meberIds)")
void updateAgeByMemberIds(Integer age, List<Long> memberIds);

@Transactional
@Modifying
@Query("update Member m set m.age = :age, m.address = :address where m.id in (:meberIds)")
void updateAgeAndAddressByMemberIds(Integer age, String address, List<Long> memberIds);

 

 

혹은 문자열 및 매개변수를 동적으로 변환시켜주는 로직을 직접 구성해주어야 한다. 

String queryString = "update Member m set";
if (newName != null) {
    queryString += " m.name = :newName";
}
if (newAge != null) {
    queryString += ", m.age = :newAge";
}
queryString += " where m.id = :id";

Query query = em.createQuery(queryString);

if (newName != null) {
    query.setParameter("newName", newName);
}
if (newAge != null) {
    query.setParameter("newAge", newAge);
}
query.setParameter("id", memberId);

int updatedCount = query.executeUpdate();

 

이는 반복적인 코드를 증가시키고, 가독성이 매우 떨어지며 단순 String 이기 때문에 띄어쓰기로 인한 식별 문제 등등 컴파일 단계에서 오류를 발견하기가 힘들다. 

2. QueryDSL

QueryDSL 은 컴파일 단계에서의 오류를 발견할 수 있도록 코드 레벨에서 문법적인 에러를 발견할 수 있으며, 여러가지 지원하는 기능들 가운데에는 동적으로 Query 를 Build 하는 기능을 지원한다. 
그 중에는 동적 검색(https://mokggang.tistory.com/69)뿐만이 아니라 위와 같은 불편한 동적 업데이트 쿼리 생성을 손쉽게 개발할 수 있도록 지원한다.

public void dynamicUpdateMember(MemberSearchCondition condition){
	
    JPAUpdateClause clause = new JPAUpdateClause(entityManager, member);

    if(StringUtils.hasText(condition.getName)){
    	clause.set(member.name, condition.getName);
    }
    if(condition.getAge!=null){
    	clause.set(member.age, condition.getAge);
    }
    ...
    
    clause.where(member.id.eq(condition.getId))
    
    clause.execute();
}

 

업데이트 할 파라미터를 set 해주기만 하면 되기 때문에 많은 양의 코드를 줄여준다. 

 

3. 확인

동적 업데이트를 지원하는 JPAUpdateClause.class를 확인해보면 JPQL 로 직접 동적 업데이트를 만드는 방식을 사용하여 QType객체를 대상으로 Query 를 생성해 execute 해주고 있다. 

@Override
public <T> JPAUpdateClause set(Path<T> path, T value) {
    if (value != null) {
        updates.put(path, Expressions.constant(value));
    } else {
        setNull(path);
    }
    return this;
}
    
    
@Override
public long execute() {
    JPQLSerializer serializer = new JPQLSerializer(templates, entityManager);
    serializer.serializeForUpdate(queryMixin.getMetadata(), updates);

    Query query = entityManager.createQuery(serializer.toString());
    if (lockMode != null) {
        query.setLockMode(lockMode);
    }
    JPAUtil.setConstants(query, serializer.getConstants(), queryMixin.getMetadata().getParams());
    return query.executeUpdate();
}


public void serializeForUpdate(QueryMetadata md, Map<Path<?>, Expression<?>> updates) {
    append(UPDATE);
    handleJoinTarget(md.getJoins().get(0));
    append(SET);
    boolean first = true;
    for (Map.Entry<Path<?>, Expression<?>> entry : updates.entrySet()) {
        if (!first) {
            append(", ");
        }
        handle(entry.getKey());
        append(" = ");
        handle(entry.getValue());
        first = false;
    }
    if (md.getWhere() != null) {
        append(WHERE).handle(md.getWhere());
    }
}

 

반응형