Robin Hermans
Robin Hermans

Reputation: 1599

How to use natural sort with Spring Data Jpa

I have a table column that I want to order on. The problem is that the column value contains both numbers and text. For example, the result is now ordered like this.

1. group one
10. group ten
11. group eleven
2. group two

But I'd like the result to be ordered naturally, like this

1. group one
2. group two
10. group ten
11. group eleven

When looking at the Spring configuration I can't seem to find an option that allows you to do this. I use the Spring Pageable class to set my order field and direction, with an additional use of JPA specifications. The method itself returns a Page with the first 20 results by default.

I don't believe that Oracle supports Natural ordering out of the box so I have a stored procedure for that. Running the query using this procedure I get the desired result.

select ... from group order by NATURALSORT(group.name) asc

As you might expect I'd like to use that procedure by wrapping it around every ordered column that contains text. While maintaining to use pageables/pages and specifications. The research I done this far points me to a solution that might include

But I didn't seem to find a method that allows me to set the order using native SQL. The only way I found to set the order was by calling orderBy and including an Order object.

So in general.

  1. Is there a way to globally enable natural ordering when using Spring Data Jpa, hibernate and the Oracle database
  2. If not, how can I wrap a single order by column with my stored procedure while still being able to use the Page findAll(Pageable, Specifications) method?

Thanks in advance.

Upvotes: 2

Views: 3893

Answers (2)

Robin Hermans
Robin Hermans

Reputation: 1599

After some digging into the source code of both Spring JPA and Hibernate I managed to find a solution to my problem. I'm pretty sure this isn't a nice way to solve it, but it's the only one I could find.

I ended up implementing a wrapper for the 'order by' part of the query by extending the SingularAttributePath class. This class has a render method that generates the string which gets inserted into the actual query. My implementation looks like this

@Override
public String render(RenderingContext renderingContext) {
  String render = super.render(renderingContext);
  render = "MYPACKAGE.NSORT(" + render + ")";
  return render;
}

Next I extended the Order conversion functionality in the SimpleJpaRepository class. By default this is done by calling QueryUtils.toOrders(sort, root, builder). But since the method calling it was impossible to override I ended up calling the toOrder method myself and altering the result to my liking.

This means replacing all orders in the result by my custom implementation of the SingularAttributePath class. As an extra I extended the Sort class which is used by the Pageable class to have control over what gets wrapped and what doesn't (called NaturalOrder). But I'll get to that in a second. My implementation looks close to this (some checks are left out)

// Call the original method to convert the orders
List<Order> orders = QueryUtils.toOrders(sort, root, builder);
for (Order order : orders) {
  // Fetch the original order object from the sort for comparing
  SingularAttributePath orderExpression = (SingularAttributePath) order.getExpression();
  Sort.Order originalOrder = sort.getOrderFor(orderExpression.getAttribute().getName());
  // Check if the original order object is instantiated from my custom order class
  // Also check if the the order should be natural
  if (originalOrder instanceof NaturalSort.NaturalOrderm && ((NaturalSort.NaturalOrder) originalOrder).isNatural()){
    // replace the order with the custom class
    Order newOrder = new OrderImpl(new NaturalSingularAttributePathImpl(builder, expression.getJavaType(), expression.getPathSource(), expression.getAttribute()));
    resultList.add(newOrder);
  }else{
    resultList.add(order);
  }
}
return resultList;

The return list then gets added to the query by calling query.orderBy(resultlist). That's it for the back-end.

In order to control the wrap condition I also extended the Sort class used by the Pageable (mentioned this a few lines back). The only functionality I wanted to add was to have 4 types in the Direction enum.

  • ASC (default ascending)
  • DESC (default descending)
  • NASC (normal ascending)
  • NDESC (normal descending)

The last two values only act as placeholders. They set the isNatural boolean (variable of the extended Order class) which gets used in the condition. At the time they are converted to query they are mapped back to their default variants.

public Direction getNativeDirection() {
  if (this == NaturalDirection.NASC)
    return Direction.ASC;
  if (this == NaturalDirection.NDESC)
    return Direction.DESC;
  return Direction.fromString(String.valueOf(this));
}

Lastly I replaced the SortHandlerMethodArgumentResolver used by the PageableHandlerMethodArgumentResolver. The only thing this does is creating instances of my NaturalSort class and passing them into the Pageable object , instead of the default Sort class.

In the end I'am able to call the same REST endpoint, while the result differs in the way it's sorted.

Default Sorting
/api/v1/items?page=0&size=20&sort=name,asc
Natural Sorting
/api/v1/items?page=0&size=20&sort=name,nasc

I hope this solution can help those who have the same or a derived problem regarding natural sort and spring JPA. If you have any question or improvements ,please let me know.

Upvotes: 3

Dragan Bozanovic
Dragan Bozanovic

Reputation: 23552

I am not aware of any such feature. You could however store the numbers in a separate column, and then order by that column, which should give a better sorting performance as an additional benefit.

Upvotes: 1

Related Questions