JPA MySql Random 조회 (1) - order by rand()
Table of contents
서문
요구사항 개발 중 데이터를 랜덤으로 조회할 필요가 생겼다.
DB는 MySql을 사용하고 있다. Mysql은 rand()를 통해 랜덤 기능을 제공하며 데이터를 랜덤하게 조회하기 위해선 공식문서에도 나와있듯이 SELECT * FROM tbl_name ORDER BY RAND();
와 같이 사용하면 된다.
애플리케이션 서버에서 이를 어떻게 구현할 지가 고민이었고 Spring Boot 2.7.5, JPA, QueryDsl
을 사용중이다.
JPQL 작성
Mysql의 rand()
는 jpql이나 hql에서 지원해주는 함수가 아니다. (Oracle 문서, Hibernate 문서)
하지만 Hibernate 문서에도 나와있듯이 각 DB의 Dialect가 각자의 네이티브 함수들을 많이 등록해놨다. logging.level.org.hibernate.HQL_FUNCTIONS=debug
로 로그 레벨을 설정하고 스프링을 실행시키면 rand()
가 그 중에 하나임을 확인 할 수 있다.
구현
# Spring Data Jpa @Query 사용
@Query("select member from Member member order by rand()")
List<Member> getRandomMembers();
# Entity Manager 사용
List<Member> resultList = entityManager.createQuery(
"SELECT m " +
"FROM Member m " +
"ORDER BY rand()", Member.class)
.getResultList();
rand()
를 직접 호출해주면 된다.
Hibernate 개발에 참여했던 Vlad Mihalcea도 블로그에서 이 방법을 언급했다. (링크)
QueryDsl
QueryDsl에서도 NumberExpressions.random()
을 통해 랜덤 함수 호출이 가능하지만 Mysql을 사용할 경우random()
이란 함수를 찾을 수 없다고 예외가 던져진다. Querydsl 깃허브를 가보면 해당 이슈를 볼 수 있다. (링크)
의아했던 점은 SQLTemplates
에 이미 rand()
가 정의되었고 MySQLTemplates
는 SQLTemplates
을 상속받고 있기 때문에 rand()
가 동작해야 할 것 같은데 그렇지 않다는 점이다.
// QueryDsl 코드 일부
package com.querydsl.jpa.impl;
public class JPAQueryFactory implements JPQLQueryFactory {
public JPAQueryFactory(final EntityManager entityManager) {
this.entityManager = () -> entityManager;
this.templates = null;
}
@Override
public JPAQuery<?> query() {
if (templates != null) {
return new JPAQuery<Void>(entityManager.get(), templates);
} else {
return new JPAQuery<Void>(entityManager.get());
}
}
}
package com.querydsl.jpa.impl;
public class JPAQuery<T> extends AbstractJPAQuery<T, JPAQuery<T>> {
public JPAQuery(EntityManager em) {
super(em, JPAProvider.getTemplates(em), new DefaultQueryMetadata());
}
}
package com.querydsl.jpa;
public class JPQLTemplates extends Templates {
}
이는 JPAQueryFactory
를 생성할 때 JPQLTemplates
을 파라미터로 넘기지 않으면 JPAProvider
와 EntityManager
를 통해 JPQLTemplates
가 설정되기 때문이다.
JPQLTemplates
는 Templates
를 상속받고 있기 때문에 위에서 볼 수 있듯이 랜덤 함수가random()
으로 설정되며 JPQLTemplates
를 상속받은 여타 클래스들도 랜덤과 관련된 설정은 없기에 기본 설정이 유지된다. JPAQueryFactory
은 JPQL을 위한 클래스이고 MySql 전용이 아니기에 어찌보면 당연한 것이라는 생각도 든다. 내 프로젝트에선 Hibernate5Templates
가 사용되었다.
public class MySqlJpqlTemplates extends Hibernate5Templates {
protected MySqlJpqlTemplates() {
super();
add(Ops.MathOps.RANDOM, "rand()");
}
}
// 사용
QMember memberPath = new QMember("member");
List<Member> fetch1 = new JPASQLQuery<>(entityManager, MySQLTemplates.DEFAULT)
.select(memberPath)
.from(memberPath)
.orderBy(NumberExpression.random().asc())
.fetch();
List<Member> fetch2 = new JPAQueryFactory(new MySqlJpqlTemplates(), entityManager)
.select(member)
.from(member)
.orderBy(NumberExpression.random().asc())
.fetch();
List<Member> fetch3 = new JPAQuery<>(entityManager, new MySqlJpqlTemplates())
.select(member)
.from(member)
.orderBy(NumberExpression.random().asc())
.fetch();
// 생성 쿼리
Hibernate: select m1_0.id from member m1_0 order by rand()
JPAQueryFactory
나 JPAQuery
생성자 중에 JPQLTemplates
를 받는 생성자가 있으므로 위와 같이 JPQLTemplates
를 상속받아 새 클래스를 정의해 사용하면 rand()가 호출되는걸 확인할 수 있다. 혹은 JPASQLQuery
에 MySQLTemplates.DEFAULT
를 넘겨줘도 된다.
package com.querydsl.core.types.dsl;
public final class Expressions {
// 생략
public static <T extends Number & Comparable<?>> NumberTemplate<T> numberTemplate(Class<? extends T> cl,
String template, Object... args) {
return numberTemplate(cl, createTemplate(template), Arrays.asList(args));
}
// 생략
private static Template createTemplate(String template) {
return TemplateFactory.DEFAULT.create(template);
}
// 생략
}
// 사용
List<Member> members = repository.query(query -> query
.select(member)
.from(member)
.orderBy(Expressions.numberTemplate(Double.class, "function('rand')").asc())
.fetch());
아니면 더 간단하게 orderBy()
에
Expressions.numberTemplate(Double.class, "function('rand')")
를 넘겨 호출해도 된다.
NumberExpressions.random()
와 뭐가 다르길래 다르게 동작할까
함수명에서도 알 수 있듯이 Expressions.numberTemplate()
는 새 템플릿을 등록하는 함수다. 주석에도 Create a new Template expression
로 적혀있다. 내부 구현을 보면TemplateFactory.DEFAULT.create(template)
를 호출해 함수를 템플릿에 등록하는걸 알 수 있다.
원래 방언에 등록된 함수만 호출할 수 있는지 알았는데 Hibernate 6으로 오면서 변경된 것 같다고 한다.
order by rand()의 성능 이슈
Mysql의 order by rand()
는 인덱스를 타지 않고 테이블 풀스캔을 한 후 랜덤으로 정렬을 하기 때문에 다량의 데이터를 다룰 때 성능 이슈가 있을 수 있다.
대량의 데이터가 아니라면 order by rand()
사용도 괜찮다고 생각한다.
현재의 요구사항은 소수의 데이터를 위한 임시적인 기능이니 해당 방식을 채택했지만 만약 대량의 데이터를 다뤄야 한다면 다른 방법을 찾아야 할 것이다.
다른 방법들은 추후 글을 통해 정리해볼 예정이다
Subscribe to my newsletter
Read articles from JaeHun Kim directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by