Skip to content

Commit

Permalink
Merge pull request #218 from croz-ltd/feature_supportSortingBySubclas…
Browse files Browse the repository at this point in the history
…sProperties

Add support for sorting by subclass properties
  • Loading branch information
jzrilic authored Aug 22, 2024
2 parents d62c7ce + e0036c0 commit b0b6ff9
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import net.croz.nrich.search.util.PathResolvingUtil;
import net.croz.nrich.search.util.ProjectionListResolverUtil;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.query.QueryUtils;
import org.springframework.data.jpa.repository.query.NrichQueryUtils;
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
Expand Down Expand Up @@ -101,7 +101,7 @@ public <R, P> CriteriaQuery<P> buildQuery(R request, SearchConfiguration<T, P, R
query.distinct(searchConfiguration.isDistinct());

if (sort != null && sort.isSorted()) {
query.orderBy(QueryUtils.toOrders(sort, root, criteriaBuilder));
query.orderBy(NrichQueryUtils.toOrders(sort, root, criteriaBuilder));
}

return query;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.springframework.data.jpa.repository.query;

import org.hibernate.metamodel.model.domain.EntityDomainType;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ConcurrentReferenceHashMap;

import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.From;
import jakarta.persistence.criteria.Order;
import jakarta.persistence.metamodel.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
* Adds support for sorting by subclass properties. Although Hibernate supports sorting by subclass properties, Spring will throw an exception.
*/
public final class NrichQueryUtils {

private static final Map<PropertyPathKey, PropertyPath> PROPERTY_PATH_CACHE = new ConcurrentReferenceHashMap<>();

private NrichQueryUtils() {
}

public static List<Order> toOrders(Sort sort, From<?, ?> from, CriteriaBuilder cb) {
if (sort.isUnsorted()) {
return Collections.emptyList();
}

Assert.notNull(from, "From must not be null");
Assert.notNull(cb, "CriteriaBuilder must not be null");

List<jakarta.persistence.criteria.Order> orders = new ArrayList<>();

for (org.springframework.data.domain.Sort.Order order : sort) {
orders.add(toJpaOrder(order, from, cb));
}

return orders;
}

private static jakarta.persistence.criteria.Order toJpaOrder(Sort.Order order, From<?, ?> from, CriteriaBuilder cb) {
String propertyName = order.getProperty();

PropertyPath property = PROPERTY_PATH_CACHE.computeIfAbsent(new PropertyPathKey(propertyName, from.getJavaType()), key -> resolvePropertyPath(propertyName, from));
Expression<?> expression = QueryUtils.toExpressionRecursively(from, property);

if (order.isIgnoreCase() && String.class.equals(expression.getJavaType())) {
@SuppressWarnings("unchecked")
Expression<String> upper = cb.lower((Expression<String>) expression);
return order.isAscending() ? cb.asc(upper) : cb.desc(upper);
}

return order.isAscending() ? cb.asc(expression) : cb.desc(expression);
}

private static PropertyPath resolvePropertyPath(String property, From<?, ?> from) {
PropertyReferenceException originalException;
try {
return PropertyPath.from(property, from.getJavaType());
}
catch (PropertyReferenceException exception) {
originalException = exception;
}

List<? extends Class<?>> subtypeList = resolveSubtypeList(from);
if (!CollectionUtils.isEmpty(subtypeList)) {
for (Class<?> subtype : subtypeList) {
try {
return PropertyPath.from(property, subtype);
}
catch (PropertyReferenceException exception) {
// ignored
}
}
}

throw originalException;
}

private static List<? extends Class<?>> resolveSubtypeList(From<?, ?> from) {
if (from.getModel() instanceof EntityDomainType<?> entityDomainType) {
return entityDomainType.getSubTypes().stream()
.map(Type::getJavaType)
.toList();
}

return Collections.emptyList();
}

record PropertyPathKey(String property, Class<?> type) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,20 @@ void shouldSortEntities() {
assertThat(results.get(0).getAge()).isEqualTo(28);
}

@Test
void shouldSortBySubclassProperty() {
// given
generateTestSubEntityList(entityManager);
Map<String, Object> requestMap = Map.of("subName", "subName");

// when
List<TestEntity> results = executeQuery(requestMap, SearchConfiguration.emptyConfiguration(), Sort.by(Sort.Order.desc("subName")));

// then
assertThat(results).isNotEmpty();
assertThat(results).extracting("subName").containsExactly("subName2", "subName1", "subName0");
}

@Test
void shouldSortByJoinedEntity() {
// given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,10 @@
import net.croz.nrich.search.repository.stub.TestEntityWithEmbeddedId;
import net.croz.nrich.search.repository.stub.TestNestedEntity;
import net.croz.nrich.search.repository.stub.TestStringSearchEntity;
import net.croz.nrich.search.repository.stub.TestSubEntity;

import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.JoinType;

import net.croz.nrich.search.repository.stub.TestSubEntity;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
Expand Down Expand Up @@ -75,7 +73,7 @@ public static List<TestEntity> generateListForSearch(EntityManager entityManager
public static List<TestStringSearchEntity> generateListForStringSearch(EntityManager entityManager) {
LocalDate date = LocalDate.parse("01.01.1970", DateTimeFormatter.ofPattern("dd.MM.yyyy"));
List<TestStringSearchEntity> testEntityList = IntStream.range(0, 5)
.mapToObj(value -> createTestStringSearchEntity("name " + value, 50 + value, date.plus(value, ChronoUnit.DAYS)))
.mapToObj(value -> createTestStringSearchEntity("name " + value, 50 + value, date.plusDays(value)))
.collect(Collectors.toList());

testEntityList.forEach(entityManager::persist);
Expand Down

0 comments on commit b0b6ff9

Please sign in to comment.