Avoid using Hibernate APIs to cache query results as the API changes in Hibernate 6

Closes #16332
This commit is contained in:
Alexander Schwartz 2023-01-11 10:46:46 +01:00 committed by Hynek Mlnařík
parent 5ddb79cbe6
commit e9e6b73bd2
2 changed files with 51 additions and 48 deletions

View file

@ -16,6 +16,7 @@
*/ */
package org.keycloak.models.map.storage.jpa; package org.keycloak.models.map.storage.jpa;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -25,7 +26,6 @@ import java.util.UUID;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.LockModeType; import javax.persistence.LockModeType;
import javax.persistence.Parameter;
import javax.persistence.PersistenceException; import javax.persistence.PersistenceException;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaBuilder;
@ -38,11 +38,9 @@ import javax.persistence.criteria.Selection;
import org.hibernate.Session; import org.hibernate.Session;
import org.hibernate.internal.SessionImpl; import org.hibernate.internal.SessionImpl;
import org.hibernate.query.spi.QueryImplementor;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.ExpirableEntity; import org.keycloak.models.map.common.ExpirableEntity;
import org.keycloak.models.map.common.StringKeyConverter; import org.keycloak.models.map.common.StringKeyConverter;
@ -134,6 +132,26 @@ public abstract class JpaMapKeycloakTransaction<RE extends JpaRootEntity, E exte
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public Stream<E> read(QueryParameters<M> queryParameters) { public Stream<E> read(QueryParameters<M> queryParameters) {
Map<QueryCacheKey, List<RE>> cache = getQueryCache();
QueryCacheKey queryCacheKey = new QueryCacheKey(queryParameters, modelType);
if (!LockObjectsForModification.isEnabled(this.session, modelType)) {
List<RE> previousResult = cache.get(queryCacheKey);
SessionImpl session = (SessionImpl) em.unwrap(Session.class);
// only do dirty checking if there is a previously cached result that would match the query
if (previousResult != null) {
// if the session is dirty, data has been modified, and the cache must not be used
// check if there are queued actions already, as this allows us to skip the expensive dirty check
if (!session.getActionQueue().areInsertionsOrDeletionsQueued() && session.getActionQueue().numberOfUpdates() == 0 && session.getActionQueue().numberOfCollectionUpdates() == 0 &&
!session.isDirty()) {
logger.tracef("tx %d: cache hit for %s/%s%s", hashCode(), queryParameters, queryCacheKey, getShortStackTrace());
return closing(previousResult.stream()).map(this::mapToEntityDelegateUnique);
} else {
logger.tracef("tx %d: cache ignored due to dirty session", hashCode());
}
}
}
logger.tracef("tx %d: cache miss for %s/%s%s", hashCode(), queryParameters, queryCacheKey, getShortStackTrace());
JpaModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder() JpaModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(createJpaModelCriteriaBuilder()); .flashToModelCriteriaBuilder(createJpaModelCriteriaBuilder());
@ -170,27 +188,6 @@ public abstract class JpaMapKeycloakTransaction<RE extends JpaRootEntity, E exte
TypedQuery<RE> emQuery = paginateQuery(em.createQuery(query), queryParameters.getOffset(), queryParameters.getLimit()); TypedQuery<RE> emQuery = paginateQuery(em.createQuery(query), queryParameters.getOffset(), queryParameters.getLimit());
Map<QueryCacheKey, List<RE>> cache = getQueryCache();
QueryCacheKey queryCacheKey = new QueryCacheKey(emQuery, modelType);
if (!LockObjectsForModification.isEnabled(this.session, modelType)) {
List<RE> previousResult = cache.get(queryCacheKey);
//noinspection resource
SessionImpl session = (SessionImpl) em.unwrap(Session.class);
// only do dirty checking if there is a previously cached result that would match the query
if (previousResult != null) {
// if the session is dirty, data has been modified, and the cache must not be used
// check if there are queued actions already, as this allows us to skip the expensive dirty check
if (!session.getActionQueue().areInsertionsOrDeletionsQueued() && session.getActionQueue().numberOfUpdates() == 0 && session.getActionQueue().numberOfCollectionUpdates() == 0 &&
!session.isDirty()) {
logger.tracef("tx %d: cache hit for %s/%s%s", hashCode(), queryParameters, queryCacheKey, getShortStackTrace());
return closing(previousResult.stream()).map(this::mapToEntityDelegateUnique);
} else {
logger.tracef("tx %d: cache ignored due to dirty session", hashCode());
}
}
}
logger.tracef("tx %d: cache miss for %s/%s%s", hashCode(), queryParameters, queryCacheKey, getShortStackTrace());
if (LockObjectsForModification.isEnabled(session, modelType)) { if (LockObjectsForModification.isEnabled(session, modelType)) {
emQuery = emQuery.setLockMode(LockModeType.PESSIMISTIC_WRITE); emQuery = emQuery.setLockMode(LockModeType.PESSIMISTIC_WRITE);
} }
@ -209,11 +206,10 @@ public abstract class JpaMapKeycloakTransaction<RE extends JpaRootEntity, E exte
} }
private Map<QueryCacheKey, List<RE>> getQueryCache() { private Map<QueryCacheKey, List<RE>> getQueryCache() {
//noinspection resource,unchecked //noinspection unchecked
Map<QueryCacheKey, List<RE>> cache = (Map<QueryCacheKey, List<RE>>) em.unwrap(Session.class).getProperties().get(JPA_MAP_CACHE); Map<QueryCacheKey, List<RE>> cache = (Map<QueryCacheKey, List<RE>>) em.unwrap(Session.class).getProperties().get(JPA_MAP_CACHE);
if (cache == null) { if (cache == null) {
cache = new HashMap<>(); cache = new HashMap<>();
//noinspection resource
em.unwrap(Session.class).setProperty(JPA_MAP_CACHE, cache); em.unwrap(Session.class).setProperty(JPA_MAP_CACHE, cache);
} }
return cache; return cache;
@ -345,24 +341,17 @@ public abstract class JpaMapKeycloakTransaction<RE extends JpaRootEntity, E exte
private static class QueryCacheKey { private static class QueryCacheKey {
private final String queryString; private final String queryString;
private final Integer queryMaxResults; private final Integer queryLimit;
private final Integer queryFirstResult; private final Integer queryOffset;
private final HashMap<String, Object> queryParameters;
private final Class<?> modelType; private final Class<?> modelType;
private final List<? extends QueryParameters.OrderBy<?>> queryOrderBy;
public QueryCacheKey(TypedQuery<?> emQuery, Class<?> modelType) { public QueryCacheKey(QueryParameters<?> query, Class<?> modelType) {
// copy over all fields from the query that relevant for caching // copy over all fields from the query that relevant for caching
QueryImplementor<?> query = emQuery.unwrap(QueryImplementor.class); this.queryString = query.getModelCriteriaBuilder().toString();
this.queryString = query.getQueryString(); this.queryLimit = query.getLimit();
this.queryParameters = new HashMap<>(); this.queryOffset = query.getOffset();
for (Parameter<?> parameter : query.getParameters()) { this.queryOrderBy = new ArrayList<>(query.getOrderBy());
if (parameter.getName() == null) {
throw new ModelException("Can't prepare query for caching as parameter doesn't have a name");
}
this.queryParameters.put(parameter.getName(), query.getParameterValue(parameter.getName()));
}
this.queryMaxResults = emQuery.getMaxResults();
this.queryFirstResult = emQuery.getFirstResult();
this.modelType = modelType; this.modelType = modelType;
} }
@ -372,24 +361,24 @@ public abstract class JpaMapKeycloakTransaction<RE extends JpaRootEntity, E exte
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
QueryCacheKey that = (QueryCacheKey) o; QueryCacheKey that = (QueryCacheKey) o;
return Objects.equals(queryString, that.queryString) return Objects.equals(queryString, that.queryString)
&& Objects.equals(queryMaxResults, that.queryMaxResults) && Objects.equals(queryLimit, that.queryLimit)
&& Objects.equals(queryFirstResult, that.queryFirstResult) && Objects.equals(queryOffset, that.queryOffset)
&& Objects.equals(queryParameters, that.queryParameters) && Objects.equals(queryOrderBy, that.queryOrderBy)
&& Objects.equals(modelType, that.modelType); && Objects.equals(modelType, that.modelType);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(queryString, queryMaxResults, queryFirstResult, queryParameters, modelType); return Objects.hash(queryString, queryLimit, queryOffset, queryOrderBy, modelType);
} }
@Override @Override
public String toString() { public String toString() {
return "QueryCacheKey{" + return "QueryCacheKey{" +
"queryString='" + queryString + '\'' + "queryString='" + queryString + '\'' +
", queryMaxResults=" + queryMaxResults + ", queryMaxResults=" + queryLimit +
", queryFirstResult=" + queryFirstResult + ", queryFirstResult=" + queryOffset +
", queryParameters=" + queryParameters + ", queryOrderBy=" + queryOrderBy +
", modelType=" + modelType.getName() + ", modelType=" + modelType.getName() +
'}'; '}';
} }

View file

@ -5,6 +5,7 @@ import org.keycloak.storage.SearchableModelField;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects;
import static org.keycloak.models.map.storage.QueryParameters.Order.ASCENDING; import static org.keycloak.models.map.storage.QueryParameters.Order.ASCENDING;
@ -146,5 +147,18 @@ public class QueryParameters<M> {
public Order getOrder() { public Order getOrder() {
return order; return order;
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OrderBy<?> orderBy = (OrderBy<?>) o;
return Objects.equals(modelField, orderBy.modelField) && order == orderBy.order;
}
@Override
public int hashCode() {
return Objects.hash(modelField, order);
}
} }
} }