DataProvider, Spring Data JPA and query by example

Getting started with Vaadin 8, I hacked up a little prototype that I think is quite neat.

Spring Data JPA has a feature called “Query By Example”, that lets you filter your data by using one of your domain objects as a probe/example (you just need to pass such an example to one of Spring’s JpaRepositories).

In Vaadin it’s very easy to show domain objects in a grid, and it’s also quite easy to bind a couple of input fields to a domain object.
So I figured, why not use this with the new data provider system of Vaadin and combine it with Spring’s QBE: If you want to create a filter form/filter row, just bind a couple of fields to a domain object/entity, and pass it to the data provider, and let spring do the rest of the work.

The result ist the following DataProvider (quick and dirty, just to give an idea how this can work).

[code]
public class ExampleFilterDataProvider<T, ID extends Serializable> implements ConfigurableFilterDataProvider<T, T, T> {
private final JpaRepository<T, ID> repository;
private final ExampleMatcher matcher;
private final List defaultSort;
private final ConfigurableFilterDataProvider<T, T, T> delegate;

public ExampleFilterDataProvider(JpaRepository<T, ID> repository,
                                 ExampleMatcher matcher,
                                 List<QuerySortOrder> defaultSort) {
    Preconditions.checkNotNull(defaultSort);
    Preconditions.checkArgument(defaultSort.size() > 0,
            "At least one sort property must be specified!");

    this.repository = repository;
    this.matcher = matcher;
    this.defaultSort = defaultSort;

    delegate = buildDataProvider();
}

private ConfigurableFilterDataProvider<T, T, T> buildDataProvider() {
    CallbackDataProvider<T, T> dataProvider = DataProvider.fromFilteringCallbacks(
            q -> q.getFilter()
                    .map(document -> repository.findAll(buildExample(document), ChunkRequest.of(q, defaultSort)).getContent())
                    .orElseGet(() -> repository.findAll(ChunkRequest.of(q, defaultSort)).getContent())
                    .stream(),
            q -> Ints.checkedCast(q
                    .getFilter()
                    .map(document -> repository.count(buildExample(document)))
                    .orElseGet(repository::count)));
    return dataProvider.withConfigurableFilter((q, c) -> c);
}

private Example<T> buildExample(T probe) {
    return Example.of(probe, matcher);
}

@Override
public void setFilter(T filter) {
    delegate.setFilter(filter);
}

@Override
public boolean isInMemory() {
    return delegate.isInMemory();
}

@Override
public int size(Query<T, T> query) {
    return delegate.size(query);
}

@Override
public Stream<T> fetch(Query<T, T> query) {
    return delegate.fetch(query);
}

@Override
public void refreshItem(T item) {
    delegate.refreshItem(item);
}

@Override
public void refreshAll() {
    delegate.refreshAll();
}

@Override
public Object getId(T item) {
    return delegate.getId(item);
}

@Override
public Registration addDataProviderListener(DataProviderListener<T> listener) {
    return delegate.addDataProviderListener(listener);
}

@Override
public <C> DataProvider<T, C> withConvertedFilter(SerializableFunction<C, T> filterConverter) {
    return delegate.withConvertedFilter(filterConverter);
}

@Override
public <Q, C> ConfigurableFilterDataProvider<T, Q, C> withConfigurableFilter(SerializableBiFunction<Q, C, T> filterCombiner) {
    return delegate.withConfigurableFilter(filterCombiner);
}

@Override
public ConfigurableFilterDataProvider<T, Void, T> withConfigurableFilter() {
    return delegate.withConfigurableFilter();
}

private static class ChunkRequest implements Pageable {
    public static <T> ChunkRequest of(Query<T, T> q, List<QuerySortOrder> defaultSort) {
        return new ChunkRequest(q.getOffset(), q.getLimit(), mapSort(q.getSortOrders(), defaultSort));
    }

    private static Sort mapSort(List<QuerySortOrder> sortOrders, List<QuerySortOrder> defaultSort) {
        if (sortOrders == null || sortOrders.isEmpty()) {
            return new Sort(mapSortCriteria(defaultSort));
        } else {
            return new Sort(mapSortCriteria(sortOrders));
        }
    }

    private static Sort.Order[] mapSortCriteria(List<QuerySortOrder> sortOrders) {
        return sortOrders.stream()
                .map(s -> new Sort.Order(s.getDirection() == SortDirection.ASCENDING ? Sort.Direction.ASC : Sort.Direction.DESC, s.getSorted()))
                .toArray(Sort.Order[]::new);
    }

    private final Sort sort;
    private int limit = 0;
    private int offset = 0;

    private ChunkRequest(int offset, int limit, Sort sort) {
        Preconditions.checkArgument(offset >= 0, "Offset must not be less than zero!");
        Preconditions.checkArgument(limit > 0, "Limit must be greater than zero!");
        this.sort = sort;
        this.offset = offset;
        this.limit = limit;
    }

    @Override
    public int getPageNumber() {
        return 0;
    }

    @Override
    public int getPageSize() {
        return limit;
    }

    @Override
    public int getOffset() {
        return offset;
    }

    @Override
    public Sort getSort() {
        return sort;
    }

    @Override
    public Pageable next() {
        return null;
    }

    @Override
    public Pageable previousOrFirst() {
        return this;
    }

    @Override
    public Pageable first() {
        return this;
    }

    @Override
    public boolean hasPrevious() {
        return false;
    }
}

}
[/code]With that data provider, you can use a domain object for filtering (see setFilter below).
How QBE matches is specified by passing an example matcher (there you can configure that, for example, a substring match should be performed, or you can ignore certain properties of your domain objects). More information about QBE can be found here:
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example

The following example would fill a grid with all users that have a username that contains the String ‘Quill’:

[code]
List defaultSort = ImmutableList.of(new QuerySortOrder(“username”, SortDirection.ASCENDING));
ExampleFilterDataProvider<User, Integer> filterDataProvider = new ExampleFilterDataProvider<>(userRepository,
ExampleMatcher.matching()
.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING)
.withIgnoreNullValues(),
defaultSort);

User probe = new User();
probe.setUsername(“Quill”)
filterDataProvider.setFilter(probe);

Grid grid = new Grid<>();
grid.setDataProvider(filterDataProvider);
grid.addColumn(User::getUsername)
.setSortProperty(“username”);
grid.addColumn(User::getEmail)
.setSortProperty(“email”);
[/code]Although this is only a prototype, it does filtering, sorting, and lazy loading, and should work with any kind of Spring Data Repository / JPA Entity.

Awesome, thanks for the example Tom!

Just the thing I was looking for. Beautiful!!