DB 쿼리 플랜 캐시와 별개로, Hibernate는 별도의 쿼리 플랜 캐시를 관리해요.
JPQL 과 HQL, Criteria API는 SQM을 통해 실제 SQL로 변환돼요.
SQM은 Semantic Query Model
entity query parser의 역할을하고, JPQL and Criteria API를 둘다 처리할 수 있어요.
JPQL은 Java Persistence Query Language
테이블 대신 엔티티 객체를 대상으로 쿼리를 작성해요.
HQL은 Hibernate Query Language
JPQL과 매우 유사한 형태에요. JPA의 구현체인 Hibernate 프레임워크에서 사용하는 쿼리 언어에요.
Criteria API는
CriteriaQuery
를 통해 동적으로 자바 코드를 작성할 수 있고, 컴파일타임에 오류를 감지할 수 있어요. QueryDSL과 동일한 특징을 가지고 있지만, QuryDSL이 더 가독성이 좋고, QueryDSL은 HQL로 변환되는 특징이 있어요
return queryFactory
.selectFrom(person)
.where(person.firstName.eq(firstName), person.lastName.eq(lastName))
.fetch();
select person
from Person person
where person.firstName = ?1 and person.lastName = ?2
select
p1_0.first_name,
p1_0.id,
p1_0.last_name
from
person p1_0
where
p1_0.first_name='Joseph'
and p1_0.last_name='Jeong'
'Tina' , 'Jeong' 으로 파라미터 조건만 바꾸니 2번의 쿼리플랜캐시 생성은 생략됐어요.
기존의 쿼리플랜캐시를 사용하는 것을 관찰할 수 있었어요.
밑의 QueryInterpretationCacheStandardImpl.resolveSelectQueryPlan
메소드 참고
isQueryPlanCacheable
쿼리 플랜캐싱이 가능한지 체크public class QuerySqmImpl{
...
@Override
public boolean isQueryPlanCacheable() {
return CRITERIA_HQL_STRING.equals( hql )
// For criteria queries, query plan caching requires an explicit opt-in
? getQueryOptions().getQueryPlanCachingEnabled() == Boolean.TRUE
: super.isQueryPlanCacheable();
}
public class SqmSelectionQueryImpl<R> extends AbstractSelectionQuery<R>
implements SqmSelectionQueryImplementor<R>, InterpretationsKeySource {
private SelectQueryPlan<R> resolveSelectQueryPlan() {
final QueryInterpretationCache.Key cacheKey = createInterpretationsKey( this );
if ( cacheKey != null ) {
return getSession().getFactory().getQueryEngine().getInterpretationCache()
.resolveSelectQueryPlan( cacheKey, this::buildSelectQueryPlan ); // 여기 걸림
}
else {
return buildSelectQueryPlan();
}
}
public class QueryInterpretationCacheStandardImpl implements QueryInterpretationCache {
private final BoundedConcurrentHashMap<Key, QueryPlan> queryPlanCache;
private final BoundedConcurrentHashMap<Object, HqlInterpretation> hqlInterpretationCache;
@Override
public <R> SelectQueryPlan<R> resolveSelectQueryPlan(
// 캐시된 쿼리 플랜이 있으면 반환
final SelectQueryPlan<R> cached = (SelectQueryPlan<R>) queryPlanCache.get( key );
if ( cached != null ) {
if ( stats ) {
statistics.queryPlanCacheHit( key.getQueryString() );
}
return cached;
}
// 아니면 쿼리 플랜 생성
...
final SelectQueryPlan<R> plan = creator.get(); // creater는 QuerySqmImpl
queryPlanCache.put( key.prepareForStore(), plan );
return plan;
}
}
private SelectQueryPlan<R> buildSelectQueryPlan() { // 아까 메소드 참조 this::buildSelectQueryPlan로 여기 들어옴
final SqmSelectStatement<R>[] concreteSqmStatements = QuerySplitter.split(
(SqmSelectStatement<R>) getSqmStatement(),
getSession().getFactory()
);
if ( concreteSqmStatements.length > 1 ) {
return buildAggregatedSelectQueryPlan( concreteSqmStatements );
}
else { // 여기 걸림
return buildConcreteSelectQueryPlan( concreteSqmStatements[0], getResultType(), getQueryOptions() );
}
}
private <T> SelectQueryPlan<T> buildConcreteSelectQueryPlan(
SqmSelectStatement<?> concreteSqmStatement,
Class<T> resultType,
QueryOptions queryOptions) {
return new ConcreteSqmSelectQueryPlan<>(
concreteSqmStatement,
getQueryString(),
getDomainParameterXref(),
resultType,
tupleMetadata,
queryOptions
);
}
ConcreteSqmSelectQueryPlan
생성protected List<R> doList() {
final List<R> list = resolveSelectQueryPlan()
.performList( executionContextForDoList( containsCollectionFetches, hasLimit, needsDistinct ) ); // 실제 Entity 객체 리스트가 담김
}
public class ConcreteSqmSelectQueryPlan<R> implements SelectQueryPlan<R> {
....
@Override
public List<R> performList(DomainQueryExecutionContext executionContext) {
if ( executionContext.getQueryOptions().getEffectiveLimit().getMaxRowsJpa() == 0 ) {
return Collections.emptyList();
}
return withCacheableSqmInterpretation( executionContext, null, listInterpreter ); // withCacheableSqmInterpretation에서 buildCacheableSqmInterpretation 호출
}
private static CacheableSqmInterpretation buildCacheableSqmInterpretation(
final SqmTranslation<SelectStatement> sqmInterpretation =
sessionFactory.getQueryEngine().getSqmTranslatorFactory()
.createSelectTranslator(
sqm,
executionContext.getQueryOptions(),
domainParameterXref,
executionContext.getQueryParameterBindings(),
executionContext.getSession().getLoadQueryInfluencers(),
sessionFactory,
true
)
.translate();
// DBMS 종류별로 SqlAstTranslator 구현체가 있고 실질적인 sql을 생성
final SqlAstTranslator<JdbcOperationQuerySelect> selectTranslator =
sessionFactory.getJdbcServices().getJdbcEnvironment().getSqlAstTranslatorFactory()
.buildSelectTranslator(
sessionFactory,
sqmInterpretation.getSqlAst()
);
Hibernate에서 동일한 쿼리에 대해 실행 계획이 달라지는 문제는 주로 파라미터의 변동성 때문에 발생해요.
특히 IN
절과 같은 변동적인 파라미터를 사용할 때 주의
파라미터 변동성:
IN
절에 사용되는 파라미터 리스트의 크기가 변동적일 경우, Hibernate는 각각의 다른 파라미터 리스트에 대해 새로운 쿼리 플랜을 생성.파라미터 패딩 옵션 켜기:
hibernate.query.in_clause_parameter_padding
속성을 사용하여 IN
절의 파라미터 크기를 고정하기. -> 동일한 크기의 파라미터 리스트를 사용하여 쿼리 플랜의 재사용성을 높일수 있어요.protected Map<String, Object> jpaProperties() {
Map<String, Object> props = new HashMap<>();
props.put("hibernate.session_factory.interceptor", interceptor);
// 쿼리 플랜 최적화 설정
props.put("hibernate.query.in_clause_parameter_padding", true);
// execution plan cache 사이즈 하향
props.put("hibernate.query.plan_cache_max_size", 256);
props.put("hibernate.query.plan_parameter_metadata_max_size", 16);
return props;
}
메모리 오류가 발생하면 2의 제곱기준으로 맞추어 하향조정하기
출처: