Skip to content

Instantly share code, notes, and snippets.

@isen-ng
Created August 10, 2018 03:18
Show Gist options
  • Save isen-ng/ab5a5a6317911452b219e139774f6da1 to your computer and use it in GitHub Desktop.
Save isen-ng/ab5a5a6317911452b219e139774f6da1 to your computer and use it in GitHub Desktop.
Spring JPA: Specifications and projection: https://jira.spring.io/browse/DATAJPA-1033
package foo
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.lang.Nullable;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
import javax.persistence.criteria.*;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.Attribute.PersistentAttributeType;
import javax.persistence.metamodel.Bindable;
import javax.persistence.metamodel.ManagedType;
import javax.persistence.metamodel.PluralAttribute;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Member;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static javax.persistence.metamodel.Attribute.PersistentAttributeType.*;
/**
* Methods copied wholesale from:
* https://github.com/spring-projects/spring-data-jpa/blob/master/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java
*
* Needed because `toExpressionRecursively` is marked as package private and thus cannot be used in
* `JPASpecificationProjectionImpl`
*/
public abstract class QueryUtils2 {
private static final Map<PersistentAttributeType, Class<? extends Annotation>> ASSOCIATION_TYPES;
static {
Map<PersistentAttributeType, Class<? extends Annotation>> persistentAttributeTypes = new HashMap<PersistentAttributeType, Class<? extends Annotation>>();
persistentAttributeTypes.put(ONE_TO_ONE, OneToOne.class);
persistentAttributeTypes.put(ONE_TO_MANY, null);
persistentAttributeTypes.put(MANY_TO_ONE, ManyToOne.class);
persistentAttributeTypes.put(MANY_TO_MANY, null);
persistentAttributeTypes.put(ELEMENT_COLLECTION, null);
ASSOCIATION_TYPES = Collections.unmodifiableMap(persistentAttributeTypes);
}
/**
* Private constructor to prevent instantiation.
*/
private QueryUtils2() {
}
@SuppressWarnings("unchecked")
static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property) {
Bindable<?> propertyPathModel;
Bindable<?> model = from.getModel();
String segment = property.getSegment();
if (model instanceof ManagedType) {
/*
* Required to keep support for EclipseLink 2.4.x. TODO: Remove once we drop that (probably Dijkstra M1)
* See: https://bugs.eclipse.org/bugs/show_bug.cgi?id=413892
*/
propertyPathModel = (Bindable<?>) ((ManagedType<?>) model).getAttribute(segment);
} else {
propertyPathModel = from.get(segment).getModel();
}
if (requiresJoin(propertyPathModel, model instanceof PluralAttribute, !property.hasNext())
&& !isAlreadyFetched(from, segment)) {
Join<?, ?> join = getOrCreateJoin(from, segment);
return (Expression<T>) (property.hasNext() ? toExpressionRecursively(join, property.next()) : join);
} else {
Path<Object> path = from.get(segment);
return (Expression<T>) (property.hasNext() ? toExpressionRecursively(path, property.next()) : path);
}
}
/**
* Returns whether the given {@code propertyPathModel} requires the creation of a join. This is the case if we find a
* optional association.
*
* @param propertyPathModel may be {@literal null}.
* @param isPluralAttribute is the attribute of Collection type?
* @param isLeafProperty is this the final property navigated by a {@link PropertyPath}?
* @return wether an outer join is to be used for integrating this attribute in a query.
*/
private static boolean requiresJoin(@Nullable Bindable<?> propertyPathModel, boolean isPluralAttribute,
boolean isLeafProperty) {
if (propertyPathModel == null && isPluralAttribute) {
return true;
}
if (!(propertyPathModel instanceof Attribute)) {
return false;
}
Attribute<?, ?> attribute = (Attribute<?, ?>) propertyPathModel;
if (!ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) {
return false;
}
if (isLeafProperty && !attribute.isCollection()) {
return false;
}
Class<? extends Annotation> associationAnnotation = ASSOCIATION_TYPES.get(attribute.getPersistentAttributeType());
if (associationAnnotation == null) {
return true;
}
Member member = attribute.getJavaMember();
if (!(member instanceof AnnotatedElement)) {
return true;
}
Annotation annotation = AnnotationUtils.getAnnotation((AnnotatedElement) member, associationAnnotation);
return annotation == null ? true : (boolean) AnnotationUtils.getValue(annotation, "optional");
}
static Expression<Object> toExpressionRecursively(Path<Object> path, PropertyPath property) {
Path<Object> result = path.get(property.getSegment());
return property.hasNext() ? toExpressionRecursively(result, property.next()) : result;
}
/**
* Returns an existing join for the given attribute if one already exists or creates a new one if not.
*
* @param from the {@link From} to get the current joins from.
* @param attribute the {@link Attribute} to look for in the current joins.
* @return will never be {@literal null}.
*/
private static Join<?, ?> getOrCreateJoin(From<?, ?> from, String attribute) {
for (Join<?, ?> join : from.getJoins()) {
boolean sameName = join.getAttribute().getName().equals(attribute);
if (sameName && join.getJoinType().equals(JoinType.LEFT)) {
return join;
}
}
return from.join(attribute, JoinType.LEFT);
}
/**
* Return whether the given {@link From} contains a fetch declaration for the attribute with the given name.
*
* @param from the {@link From} to check for fetches.
* @param attribute the attribute name to check.
* @return
*/
private static boolean isAlreadyFetched(From<?, ?> from, String attribute) {
for (Fetch<?, ?> fetch : from.getFetches()) {
boolean sameName = fetch.getAttribute().getName().equals(attribute);
if (sameName && fetch.getJoinType().equals(JoinType.LEFT)) {
return true;
}
}
return false;
}
}
package foo
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import javax.persistence.*;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Selection;
import java.beans.PropertyDescriptor;
import java.util.*;
import java.util.stream.Collectors;
import static com.treeboxsolutions.services.onuserservice.datasource.database.repositories.QueryUtils2.toExpressionRecursively;
import static org.springframework.data.jpa.repository.query.QueryUtils.toOrders;
public class JpaSpecificationProjectionImpl<T> implements JpaSpecificationProjection<T> {
@Autowired
private EntityManager mEntityManager;
private ProjectionFactory mProjectionFactory = new SpelAwareProxyProjectionFactory();
public JpaSpecificationProjectionImpl() {
// jpa requirements
}
@Override
public <P> Optional<P> findOneProjected(Specification<T> spec, Class<T> domainClass, Class<P> projectionClass) {
TypedQuery<Tuple> query = getQueryProjected(spec, domainClass, projectionClass, Sort.unsorted());
try {
Tuple result = query.getSingleResult();
return Optional.of(mapResult(result, projectionClass));
}
catch (NoResultException e) {
return Optional.empty();
}
}
@Override
public <P> List<P> findAllProjected(Specification<T> spec, Class<T> domainClass, Class<P> projectionClass) {
TypedQuery<Tuple> query = getQueryProjected(spec, domainClass, projectionClass, Sort.unsorted());
List<Tuple> results = query.getResultList();
return results.stream()
.map(result -> mapResult(result, projectionClass))
.collect(Collectors.toList());
}
private <P> TypedQuery<Tuple> getQueryProjected(Specification<T> spec, Class<T> domainClass, Class<P> projectionClass, Sort sort) {
CriteriaBuilder criteriaBuilder = mEntityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> tupleQuery = criteriaBuilder.createTupleQuery();
Root<T> root = tupleQuery.from(domainClass);
// Gathering selections from the projection class
// `toExpressionRecursively` would be imported from QueryUtils
Set<Selection<?>> selections = new HashSet<>();
List<PropertyDescriptor> inputProperties = mProjectionFactory.getProjectionInformation(projectionClass).getInputProperties();
for (PropertyDescriptor propertyDescriptor : inputProperties) {
String property = propertyDescriptor.getName();
PropertyPath path = PropertyPath.from(property, domainClass);
selections.add(toExpressionRecursively(root, path).alias(property));
}
// Select, restrict and order
tupleQuery.multiselect(new ArrayList<>(selections))
.where(spec.toPredicate(root, tupleQuery, criteriaBuilder))
.orderBy(toOrders(sort, root, criteriaBuilder));
return mEntityManager.createQuery(tupleQuery);
}
private <P> P mapResult(Tuple result, Class<P> projectionClass) {
Map<String, Object> mappedResult = new HashMap<>(result.getElements().size());
for (TupleElement<?> element : result.getElements()) {
String name = element.getAlias();
mappedResult.put(name, result.get(name));
}
return mProjectionFactory.createProjection(projectionClass, mappedResult);
}
}
package foo
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.lang.Nullable;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
import javax.persistence.criteria.*;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.Attribute.PersistentAttributeType;
import javax.persistence.metamodel.Bindable;
import javax.persistence.metamodel.ManagedType;
import javax.persistence.metamodel.PluralAttribute;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Member;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static javax.persistence.metamodel.Attribute.PersistentAttributeType.*;
/**
* Methods copied wholesale from:
* https://github.com/spring-projects/spring-data-jpa/blob/master/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java
*
* Needed because `toExpressionRecursively` is marked as package private and thus cannot be used in
* `JPASpecificationProjectionImpl`
*/
public abstract class QueryUtils2 {
private static final Map<PersistentAttributeType, Class<? extends Annotation>> ASSOCIATION_TYPES;
static {
Map<PersistentAttributeType, Class<? extends Annotation>> persistentAttributeTypes = new HashMap<PersistentAttributeType, Class<? extends Annotation>>();
persistentAttributeTypes.put(ONE_TO_ONE, OneToOne.class);
persistentAttributeTypes.put(ONE_TO_MANY, null);
persistentAttributeTypes.put(MANY_TO_ONE, ManyToOne.class);
persistentAttributeTypes.put(MANY_TO_MANY, null);
persistentAttributeTypes.put(ELEMENT_COLLECTION, null);
ASSOCIATION_TYPES = Collections.unmodifiableMap(persistentAttributeTypes);
}
/**
* Private constructor to prevent instantiation.
*/
private QueryUtils2() {
}
@SuppressWarnings("unchecked")
static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property) {
Bindable<?> propertyPathModel;
Bindable<?> model = from.getModel();
String segment = property.getSegment();
if (model instanceof ManagedType) {
/*
* Required to keep support for EclipseLink 2.4.x. TODO: Remove once we drop that (probably Dijkstra M1)
* See: https://bugs.eclipse.org/bugs/show_bug.cgi?id=413892
*/
propertyPathModel = (Bindable<?>) ((ManagedType<?>) model).getAttribute(segment);
} else {
propertyPathModel = from.get(segment).getModel();
}
if (requiresJoin(propertyPathModel, model instanceof PluralAttribute, !property.hasNext())
&& !isAlreadyFetched(from, segment)) {
Join<?, ?> join = getOrCreateJoin(from, segment);
return (Expression<T>) (property.hasNext() ? toExpressionRecursively(join, property.next()) : join);
} else {
Path<Object> path = from.get(segment);
return (Expression<T>) (property.hasNext() ? toExpressionRecursively(path, property.next()) : path);
}
}
/**
* Returns whether the given {@code propertyPathModel} requires the creation of a join. This is the case if we find a
* optional association.
*
* @param propertyPathModel may be {@literal null}.
* @param isPluralAttribute is the attribute of Collection type?
* @param isLeafProperty is this the final property navigated by a {@link PropertyPath}?
* @return wether an outer join is to be used for integrating this attribute in a query.
*/
private static boolean requiresJoin(@Nullable Bindable<?> propertyPathModel, boolean isPluralAttribute,
boolean isLeafProperty) {
if (propertyPathModel == null && isPluralAttribute) {
return true;
}
if (!(propertyPathModel instanceof Attribute)) {
return false;
}
Attribute<?, ?> attribute = (Attribute<?, ?>) propertyPathModel;
if (!ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) {
return false;
}
if (isLeafProperty && !attribute.isCollection()) {
return false;
}
Class<? extends Annotation> associationAnnotation = ASSOCIATION_TYPES.get(attribute.getPersistentAttributeType());
if (associationAnnotation == null) {
return true;
}
Member member = attribute.getJavaMember();
if (!(member instanceof AnnotatedElement)) {
return true;
}
Annotation annotation = AnnotationUtils.getAnnotation((AnnotatedElement) member, associationAnnotation);
return annotation == null ? true : (boolean) AnnotationUtils.getValue(annotation, "optional");
}
static Expression<Object> toExpressionRecursively(Path<Object> path, PropertyPath property) {
Path<Object> result = path.get(property.getSegment());
return property.hasNext() ? toExpressionRecursively(result, property.next()) : result;
}
/**
* Returns an existing join for the given attribute if one already exists or creates a new one if not.
*
* @param from the {@link From} to get the current joins from.
* @param attribute the {@link Attribute} to look for in the current joins.
* @return will never be {@literal null}.
*/
private static Join<?, ?> getOrCreateJoin(From<?, ?> from, String attribute) {
for (Join<?, ?> join : from.getJoins()) {
boolean sameName = join.getAttribute().getName().equals(attribute);
if (sameName && join.getJoinType().equals(JoinType.LEFT)) {
return join;
}
}
return from.join(attribute, JoinType.LEFT);
}
/**
* Return whether the given {@link From} contains a fetch declaration for the attribute with the given name.
*
* @param from the {@link From} to check for fetches.
* @param attribute the attribute name to check.
* @return
*/
private static boolean isAlreadyFetched(From<?, ?> from, String attribute) {
for (Fetch<?, ?> fetch : from.getFetches()) {
boolean sameName = fetch.getAttribute().getName().equals(attribute);
if (sameName && fetch.getJoinType().equals(JoinType.LEFT)) {
return true;
}
}
return false;
}
}
@isen-ng
Copy link
Author

isen-ng commented Aug 10, 2018

Use by

public interface IMyRepository extends Repository<MyEntity, String>, JpaSpecificationProjection<MyEntity> {
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment