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의 제곱기준으로 맞추어 하향조정하기
출처: