Created
August 10, 2018 03:18
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Use by