Reputation: 1599
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.
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
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.
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
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