서비스를 개발하다보면 쿼리에서 비트 연산이 필요한 경우가 종종 발생합니다.
이 때 MyBatis를 사용한다면 문제가 되지 않지만, JPA와 QueryDSL을 사용하고 있다면 이들이 비트 연산 기능을 제공하지 않고 있기 때문에 문제가 발생합니다.
이번 포스팅에서는 제가 MySQL + JPA + QueryDSL 환경에서 비트연산을 구현하는 과정과 결과를 소개하려고 합니다.
예시 상황
요구사항
- 사용자는 자신이 관심있는 취미를 여러개 선택해 저장할 수 있습니다.
- 관리자는 특정 취미를 선택한 사용자 목록을 조회할 수 있습니다.
샘플 코드
취미 옵션(HobbyType)
@Getter
@AllArgsConstructor
public enum HobbyType {
READING(1),
SPORTS(2),
MUSIC(4),
MOVIE(8),
GAME(16),
PLAY(32),
CHAT(64),
DRINK(128);
private int bitValue;
}
사용자 엔티티(User)
public class User {
private long id;
private String name;
private int hobby;
public void setHobbies(List<HobbyType> hobbies) {
this.hobby = hobbies.stream()
.mapToInt(HobbyType::getBitValue)
.sum();
}
}
그거 @NamedQuery 로 쿼리 직접 짜면 되는거 아닌가요?
아닙니다. 😭
JPQL에서 & 문자를 사용한 연산을 지원하지 않기 때문에 아래와 같은 오류가 발생합니다.
InvalidDataAccessApiUsageException: org.hibernate.QueryException: unexpected char: '&'
그럼 ExpressionUtils 쓰면 되는거 아닌가요?
안됩니다. 🤯
ExpressionUtils를 사용할 경우 아래와 같은 코드가 나올 텐데요.
ExpressionUtils.predicateTemplate("({0} & {1}) > {2}", QUser.user.hobby, Expressions.asNumber(hobbies), Expressions.asNumber(0));
JPQL에서 & 문자를 사용한 연산을 지원하지 않기 때문에 아래와 같은 오류가 발생합니다.
InvalidDataAccessApiUsageException: org.hibernate.QueryException: unexpected char: '&'
그렇담 어떻게 해야할까요? 🤔🤔🤔
해결 방법
해결 방법을 요약하면 아래와 같습니다.
- Hibernate의
Dialect
에서 제공하는registerFunction
을 통해 비트 연산 함수를 등록합니다. - 애플리케이션의 spring.jpa.database-platform 에 앞서 함수를 등록한 클래스를 명시해줍니다.
- QueryDsl의 Expressions를 통해 앞서 등록한 함수를 호출해 비트 연산을 수행합니다.
이제 아래 내용을 통해 자세히 알아보도록 하겠습니다.
비트 연산 함수 등록
ANSI SQL 외에 각 DBMS 마다 각자의 문법과 함수가 존재하는데, 이러한 표준 외 DBMS 벤더별 기능을 방언(Dialect)라고 합니다.
JPA는 특정 DBMS에 종속되지 않으며 직접 SQL을 작성하고 실행하기 때문에 Dialect 설정을 통해 DBMS에 맞는 쿼리를 생성합니다.
Dialect에 대한 자세한 내용은 아래 포스트를 참조하시면 되겠습니다.
예시 상황에서는 MySQL을 사용하기 때문에 아래와 같이 Dialect가 지정되어 있는데요.
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
먼저 비트연산 함수를 등록하기 위한 커스텀 Dialect 클래스를 만들어 줍니다. 왜냐구요? Hibernate에서 이미 구현한 클래스를 우리가 수정할 수는 없잖아요...
해당 클래스는 MySQL8Dialect 클래스를 상속하도록 만들어줍니다. (DB 버전에 맞는 클래스를 상속하면됩니다.)
public class MySQLDialect extends MySQL8Dialect {
public MySQLDialect() {
super();
}
}
그리고 비트 연산 함수를 만들어줍시다.
public class MySQLDialect extends MySQL8Dialect {
public MySQLDialect() {
super();
registerFunction("bitand", new SQLFunctionTemplate(IntegerType.INSTANCE, "(?1 & ?2)"));
}
}
이 bitand 함수는 이제 첫번째 인자와 두번째 인자로 bit AND 연산을 수행하고 그 결과를 IntegerType으로 리턴해줍니다.
spring.jpa.database-platform 에 커스텀 Dialect 클래스 등록
application.properties 에서 아래와 같이 앞서 만든 커스텀 Dialect 클래스를 등록해줍니다.
주의할 점은 패키지명까지 모두 작성해줘야 한다는 점!
spring.jpa.database-platform=com.tistory.logical-code.utility.dialect.MySQLDialect
QueryDsl에서 함수 호출하기
QueryDsl을 사용하는 Repository 클래스에서 bitand 함수를 호출해 결과를 Integer 타입으로 받도록 하겠습니다.
private NumberTemplate<Integer> getBitAndTemplate(int hobbies) {
return Expressions.numberTemplate(Integer.class, "function('bitand', {0}, {1})", QUser.user.hobby, hobbies);
}
이제 이 메서드를 호출하면 user 엔티티의 hobby 필드와 파라미터로 bit AND 연산을 수행하고 그 결과를 Integer로 반환할 것입니다.
전체 SELECT 쿼리를 한 번 볼까요?
public Page<User> getUsersByHobbie(int hobbies, Pageable pageable) {
List<User> users = getUserQuery(hobbies)
.select(user)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
Long totalCount = getUserQuery(hobbies)
.select(user.count())
.fetchOne()
.orElse(0L);
return new PageImpl<>(users, pageable, totalCount);
}
private JPAQuery<?> getUserQuery(int hobbies) {
return queryFactory.from(user)
.where(
getBitAndTemplate(hobbies).gt(0)
);
}
private NumberTemplate<Integer> getBitAndTemplate(int hobbies) {
return Expressions.numberTemplate(Integer.class, "function('bitand', {0}, {1})", QUser.user.hobby, hobbies);
}
이렇게 registerFunction 을 사용해 JPA에서 비트 연산을 구현해보았습니다 :)
'🌱 SPRING > JPA' 카테고리의 다른 글
[Spring Data JPA] Auditing에 ZonedDateTime 사용하기 (0) | 2021.05.27 |
---|---|
[JPA] 즉시 로딩/지연 로딩 (0) | 2020.07.29 |
[JPA] 프록시 (1) | 2020.07.29 |
[JPA] 상속관계 매핑 (0) | 2020.07.19 |
[JPA] 다양한 연관관계 매핑 (0) | 2020.07.19 |