I’m currently evaluating the potential use of Spring Boot GraphQL for future web projects. So far, I’ve had positive experiences with filtering, pagination, and selects/joins. However, I haven’t found a way to apply sorting when using pagination.
Here's the Repository
@NoRepositoryBean
public interface BaseRepository<T, ID> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T>, QueryByExampleExecutor<T>
{
}
@GraphQlRepository
public interface UserRepository extends BaseRepository<User, Long>
{
}
Here's the schema.graphqls
type Query {
users(
first: Int,
after: String,
last: Int,
before: String,
user: UserInput
): UserConnection
user(id: ID!): User
}
input UserInput {
username: String
firstname: String
lastname: String
}
type User {
id: ID!
username: String!
firstname: String
lastname: String
roles: [Role!]
}
type Role {
id: ID!
name: String!
}
And here's the query
query {
users(
user: {
firstname: "Max"
},
first: 20,
) {
edges {
node {
id
username
firstname
lastname
roles {
id
name
}
}
}
pageInfo {
hasPreviousPage
hasNextPage
startCursor
endCursor
}
}
}
I'm looking for something like this:
query {
users(
user: {
firstname: "Max"
},
first: 20,
orderBy: {
lastname: DESC
}
) {
...
}
}
Thank you!
My solution is to register a custom data fetcher where I can access the query and add the sort. There a more than one possible way to achieve this. For me the most convinient way was to just implement QueryByExampleBuilderCustomizer and replace the Builder in customize
@NoRepositoryBean
public interface BaseRepository<T, ID> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T>, QueryByExampleExecutor<T>, QueryByExampleBuilderCustomizer<T>
{
@Override
default Builder<T, ?> customize(Builder<T, ?> builder)
{
return CustomBuilder.create(this);
}
}
To reuse spring boot stuff this has to be in package org.springframework.graphql.data.query. I know using internal stuff is not a good idea in the long run, but i'm hoping spring boot will add sorting to pagination in future releases since it is commonly needed although the specification does not explicitly specify it but definetly does not forbid it.
public class CustomBuilder<T, R> extends QueryByExampleDataFetcher.Builder<T, R>
{
public static <T, R> CustomBuilder<T, R> create(QueryByExampleExecutor<T> executor) {
Class<R> domainType = RepositoryUtils.getDomainType(executor);
return new CustomBuilder<>(executor, domainType);
}
private final QueryByExampleExecutor<T> executor;
private final TypeInformation<T> domainType;
private final Class<R> resultType;
@SuppressWarnings("unchecked")
CustomBuilder(QueryByExampleExecutor<T> executor, Class<R> domainType)
{
super(executor, domainType);
this.executor = executor;
this.domainType = TypeInformation.of((Class<T>) domainType);
this.resultType = domainType;
}
@Override
public DataFetcher<Iterable<R>> scrollable() {
return new SortableScrollableEntityFetcher<>(
this.executor, this.domainType, this.resultType,
RepositoryUtils.defaultCursorStrategy(),
RepositoryUtils.defaultScrollCount(),
RepositoryUtils.defaultScrollPosition(),
Sort.unsorted());
}
static class SortableScrollableEntityFetcher<T, R>
extends QueryByExampleDataFetcher<T> implements SelfDescribingDataFetcher<Iterable<R>> {
private final QueryByExampleExecutor<T> executor;
private final Class<R> resultType;
private final Sort sort;
private final CursorStrategy<ScrollPosition> cursorStrategy;
private final int defaultCount;
private final Function<Boolean, ScrollPosition> defaultPosition;
private final ResolvableType scrollableResultType;
SortableScrollableEntityFetcher(
QueryByExampleExecutor<T> executor,
TypeInformation<T> domainType,
Class<R> resultType,
CursorStrategy<ScrollPosition> cursorStrategy,
int defaultCount,
Function<Boolean, ScrollPosition> defaultPosition,
Sort sort
) {
super(domainType);
this.executor = executor;
this.resultType = resultType;
this.sort = sort;
this.cursorStrategy = cursorStrategy;
this.defaultCount = defaultCount;
this.defaultPosition = defaultPosition;
this.scrollableResultType = ResolvableType.forClassWithGenerics(Window.class, resultType);
}
@Override
public ResolvableType getReturnType() {
return ResolvableType.forClassWithGenerics(Iterable.class, this.scrollableResultType);
}
@Override
@SuppressWarnings("unchecked")
public Iterable<R> get(DataFetchingEnvironment env) throws BindException {
return this.executor.findBy(buildExample(env), (query) -> {
FluentQuery.FetchableFluentQuery<R> queryToUse = (FluentQuery.FetchableFluentQuery<R>) query;
if (this.sort.isSorted()) {
queryToUse = queryToUse.sortBy(this.sort);
}
if (requiresProjection(this.resultType)) {
queryToUse = queryToUse.as(this.resultType);
}
else {
queryToUse = queryToUse.project(buildPropertyPaths(env.getSelectionSet(), this.resultType));
}
return getResult(queryToUse, env);
});
}
protected Iterable<R> getResult(FluentQuery.FetchableFluentQuery<R> queryToUse, DataFetchingEnvironment env) {
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy);
int count = range.count().orElse(this.defaultCount);
ScrollPosition position = (range.position().isPresent() ?
range.position().get() : this.defaultPosition.apply(range.forward()));
Sort sort = fromJson(env.getArgument("sort"));
return queryToUse.limit(count).sortBy(sort).scroll(position);
}
@SuppressWarnings("unchecked")
private static Sort fromJson(Object json) {
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map = mapper.convertValue(json, Map.class);
Object ordersObj = map.get("orders");
if (!(ordersObj instanceof List<?> orders)) {
return Sort.unsorted();
}
List<Sort.Order> orderList = orders.stream()
.filter(o -> o instanceof Map<?, ?>)
.map(o -> (Map<String, Object>) o)
.map(entry -> new Sort.Order(
Sort.Direction.fromOptionalString((String) entry.getOrDefault("direction", "ASC")).orElse(Sort.Direction.ASC),
(String) entry.get("property")
))
.toList();
return Sort.by(orderList);
}
}
}