In Spring Data, we have PagingAndSortingRepository which inherits from CrudRepository. In reactive Spring Data, we only have
ReactiveSortingRepository which inherits from ReactiveCrudRepository.
How could we make pagination in a reactive way ?
Will we able to make this in future with ReactivePagingAndSortingRepository for instance?
Reactive Spring Data MongoDB repositories do not provide paging in the sense of paging how it's designed for imperative repositories. Imperative paging requires additional details while fetching a page. In particular:
The number of returned records for a paging query
Optionally, total count of records the query yields if the number of returned records is zero or matches the page size to calculate the overall number of pages
Both aspects do not fit to the notion of efficient, non-blocking resource usage. Waiting until all records are received (to determine the first chunk of paging details) would remove a huge part of the benefits you get by reactive data access. Additionally, executing a count query is rather expensive, and increases the lag until you're able to process data.
You can still fetch chunks of data yourself by passing a Pageable (PageRequest) to repository query methods:
interface ReactivePersonRepository extends Repository<Person, Long> {
Flux<Person> findByFirstnameOrderByLastname(String firstname, Pageable pageable);
}
Spring Data will apply pagination to the query by translating Pageable to LIMIT and OFFSET.
References:
Reference documentation: Reactive repository usage
import com.thepracticaldeveloper.reactiveweb.domain.Quote;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
public interface QuoteMongoReactiveRepository extends ReactiveCrudRepository<Quote, String> {
#Query("{ id: { $exists: true }}")
Flux<Quote> retrieveAllQuotesPaged(final Pageable page);
}
more details , you could check here
I created a service with this method for anyone that may still be looking for a solution:
#Resource
private UserRepository userRepository; //Extends ReactiveSortingRepository<User, String>
public Mono<Page<User>> findAllUsersPaged(Pageable pageable) {
return this.userRepository.count()
.flatMap(userCount -> {
return this.userRepository.findAll(pageable.getSort())
.buffer(pageable.getPageSize(),(pageable.getPageNumber() + 1))
.elementAt(pageable.getPageNumber(), new ArrayList<>())
.map(users -> new PageImpl<User>(users, pageable, userCount));
});
}
I have created another approach using #kn3l solution (without using #Query ):
fun findByIdNotNull(page: Pageable): Flux< Quote>
It creates the same query without using #Query method
public Mono<Page<ChatUser>> findByChannelIdPageable(String channelId, Integer page, Integer size) {
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "chatChannels.joinedTime"));
Criteria criteria = new Criteria("chatChannels.chatChannelId").is(channelId);
Query query = new Query().with(pageable);
query.addCriteria(criteria);
Flux<ChatUser> chatUserFlux = reactiveMongoTemplate.find(query, ChatUser.class, "chatUser");
Mono<Long> countMono = reactiveMongoTemplate.count(Query.of(query).limit(-1).skip(-1), ChatUser.class);
return Mono.zip(chatUserFlux.collectList(),countMono).map(tuple2 -> {
return PageableExecutionUtils.getPage(
tuple2.getT1(),
pageable,
() -> tuple2.getT2());
});
}
I've faced same issue and end up having similar approach as the above but changed slightly the code as I use Query DSL, following example if someone needed.
#Repository
public interface PersonRepository extends ReactiveMongoRepository<Person, String>, ReactiveQuerydslPredicateExecutor<Person> {
default Flux<Person> applyPagination(Flux<Person> persons, Pageable pageable) {
return persons.buffer(pageable.getPageSize(), (pageable.getPageNumber() + 1))
.elementAt(pageable.getPageNumber(), new ArrayList<>())
.flatMapMany(Flux::fromIterable);
}
}
public Flux<Person> findAll(Pageable pageable, Predicate predicate) {
return personRepository.applyPagination(personRepository.findAll(predicate), pageable);
}
Searching for some idea for reactive pageable repositories I had seen solutions that will result in horrible boiler plate code so I ended up with this (did not tried yet in real life but should work fine, or maybe it can be inspiration for your solution)
So … let's create a brand new toolbox class with such method
public static
<R extends PageableForReactiveMongo<S, K>, S, T, K> Mono<Page<T>>
pageableForReactiveMongo(Pageable pageable,
R repository, Class<T> clazzTo) {
return repository.count()
.flatMap(c ->
repository.findOderByLimitedTo(pageable.getSort(),
pageable.getPageNumber() + 1)
.buffer(pageable.getPageSize(), (pageable.getPageNumber() + 1))
.elementAt(pageable.getPageNumber(), new ArrayList<>())
.map(r -> mapToPage(pageable, c, r, clazzTo))
);
}
and it will need also something like that:
private static <S, T> Page<T> mapToPage(Pageable pageable, Long userCount, Collection<S> collection, Class<T> clazzTo) {
return new PageImpl<>(
collection.stream()
.map(r -> mapper.map(r, clazzTo))
.collect(Collectors.toList())
, pageable, userCount);
}
and then we need also an abstract layer wrapping reactive repositories
public interface PageableForReactiveMongo<D, K> extends ReactiveMongoRepository<D, K> {
Flux<D> findOderByLimitedTo(Sort sort, int i);
}
to let it instantiate by spring
#Repository
interface ControllerRepository extends PageableForReactiveMongo<ControllerDocument, String> {
}
And finally use it many many many times like that
public Mono<Page<Controller>> findAllControllers(Pageable pageable) {
return getFromPageableForReactiveMongo(pageable, controllerRepository, Controller.class);
}
this is how your code can look like :) please tell me if it is fine, or helped out with something
Related
I have a spring batch job which consists of
ItemReader<Product>: reads a product from DB
ItemProcesser<Product, List<RelatedProduct>>: reads related products from the original product
ItemWriter<List<RelatedProduct>>: writes some aspect of related products into DB
Recently, I found a case where a product had so many related products so it caused a long transaction in DB which took about an hour. This happened in our OLTP DB, so we want to split the list of related products into smaller chunks to avoid long transactions.
At first I tried to check the feasibility of code below. But it seemed this kind of code is not possible in Spring Batch.
#Bean
#JobScope
public Step step1() {
return stepBuilderFactory.get("step1")
.chunk<Product, List<RelatedProduct>>(1)
.reader(productItemReader()) // reads a product from DB
.processor(relatedProductProcessor()) // reads related products, the number of related products can be huge.
// Beginning of my hope
.reader(eachRelatedProductInListReader()) // reads each related product in the list.
.chunk(100) // re-aggregate them smaller chunks
// End of my hope
.writer(relatedProductItemWriter()) // writes info about related products
.build()
}
So now I'm thinking of storing the long list in the job context and adding one more step to process RelatedProduct in smaller chunks. But I'm wondering if there are any better ways. Any suggestions?
I implemented a new ItemReader like below to read RelatedProduct one by one using the original reader and processor:
public class FlatteningItemReader<I, O> implements ItemReader<O> {
private final ItemReader<I> originalReader;
private final ItemProcessor<I, ? extends Iterable<O>> iterableProducer;
private final Deque<O> buffer = new ArrayDeque<>();
public FlatteningItemReader(ItemReader<I> originalReader, ItemProcessor<I, ? extends Iterable<O>> iterableProducer) {
this.originalReader = Objects.requireNonNull(originalReader);
this.iterableProducer = Objects.requireNonNull(iterableProducer);
}
#Override
public O read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
if (!buffer.isEmpty()) {
return buffer.pollFirst();
}
I item;
while ((item = originalReader.read()) != null) {
Iterable<O> iter = iterableProducer.process(item);
if (iter == null) continue;
for (O o : iter) {
buffer.offerLast(o);
}
if (!buffer.isEmpty()) {
return buffer.pollFirst();
}
}
return null;
}
}
The step configuration using FlatteningItemReader looks like:
#Bean
#JobScope
public Step step1() {
return stepBuilderFactory.get("step1")
.chunk<Product, List<RelatedProduct>>(1)
// reads each RelatedItem using this new reader
.reader(new FlatteningItemReader(productItemReader(), relatedProductProcessor()))
.processor(relatedProductProcessor())
.writer(relatedProductItemWriter())
.build()
}
I'm not sure if this is good approach in Spring Batch.
I know pagination is somewhat against reactive principles, but due to requirements I have to make it work somehow. I'm using Spring Data 2.1.6 and I can't upgrade so ReactiveQuerydslSpecification for the dynamic query is out of the question. I figured I could use ReactiveMongoTemplate so I came up with this:
public interface IPersonRepository extends ReactiveMongoRepository<Person, String>, IPersonFilterRepository {
Flux<Person> findAllByCarId(String carId);
}
public interface IPersonFilterRepository {
Flux<Person> findAllByCarIdAndCreatedDateBetween(String carId, PersonStatus status,
OffsetDateTime from, OffsetDateTime to,
Pageable pageable);
}
#Repository
public class PersonFilterRepository implements IPersonFilterRepository {
#Autowired
private ReactiveMongoTemplate reactiveMongoTemplate;
#Override
public Flux<Person> findAllByCarIdAndCreatedDateBetween(String carId, PersonStatus status,
OffsetDateTime from, OffsetDateTime to,
Pageable pageable) {
Query query = new Query(Criteria.where("carId").is(carId));
if (status != null) {
query.addCriteria(Criteria.where("status").is(status));
}
OffsetDateTime maxLimit = OffsetDateTime.now(ZoneOffset.UTC).minusMonths(3).withDayOfMonth(1); // beginning of month
if (from == null || from.isBefore(maxLimit)) {
from = maxLimit;
}
query.addCriteria(Criteria.where("createdDateTime").gte(from));
if (to == null) {
to = OffsetDateTime.now(ZoneOffset.UTC);
}
query.addCriteria(Criteria.where("createdDateTime").lte(to));
// problem is trying to come up with a decent page-ish behavior compatible with Flux
/*return reactiveMongoTemplate.count(query, Person.class)
.flatMap(count -> reactiveMongoTemplate.find(query, Person.class)
.flatMap(p -> new PageImpl<Person>(p, pageable, count))
.collectList()
.map());*/
/* return reactiveMongoTemplate.find(query, Person.class)
.buffer(pageable.getPageSize(), pageable.getPageNumber() + 1)
//.elementAt(pageable.getPageNumber(), new ArrayList<>())
.thenMany(Flux::from);*/
}
I've tried to return a Page<Person> (assuming for once this single method could be non-reactive, for once) and it fails with the following error while running testing (Spring context does not load successfully due to: InvalidDataAccessApiUsageException: 'IDocumentFilterRepository.findAllByCustomerIdAndCreatedDateBetween' must not use sliced or paged execution. Please use Flux.buffer(size, skip). I've also tried returning Mono<Page<Person>> and then fails with "Method has to use a either multi-item reactive wrapper return type or a wrapped Page/Slice type. Offending method: 'IDocumentFilterRepository.findAllByCustomerIdAndCreatedDateBetween', so I guess my only option is returning a Flux, according to Example 133, snippet 3
Turns out you can just add the following to the query object:
query.with(pageable);
reactiveMongoTemplate.find(query, Person.class);
Return Flux<T> and it will work out of the box.
I'm using Spring Boot Data, QueryDSL and Swagger.
I've define endpoint like this:
#GetMapping
public ResponseEntity<?> listOfThings(
#PageableDefault(size = 20, sort = "uID", direction = Sort.Direction.DESC) final Pageable pageable,
#QuerydslPredicate(root = Thing.class) final Predicate predicate)
However Swagger define only variables: page, size, sort - it doesn't seem to parse Entity to show all fields as filterable.
I have repository like this:
#Repository
public interface ThingRepository
extends JpaSpecificationExecutor<Thing>, CrudRepository<Thing, String>, PagingAndSortingRepository<Thing, String>,
QuerydslPredicateExecutor<Thing>, QuerydslBinderCustomizer<QThing>
{
#Override
default void customize(final QuerydslBindings bindings, final QThing thing)
{
bindings.bind(thing.status).first((status, value) -> status.eq(value));
bindings.bind(thing.recipient).first(StringExpression::containsIgnoreCase);
bindings.bind(String.class).first((StringPath path, String value) -> path.containsIgnoreCase(value));
}
}
I expect Swagger to display all String fields as filters, especially status & recipient which are strictly defined.
Use maven dependency:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-data-rest</artifactId>
<version>1.6.8</version>
</dependency>
And specify your endpoint like this:
#GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<Case>> getCasesByQuery(#ParameterObject
#QuerydslPredicate(root = Case.class, bindings = CaseRepository.class) Predicate predicate,
#ParameterObject Pageable pageable) {
Now you should see all query params in SwaggerUI.
Define some dummy parameters and add all query parameters as RequestParams but do not use them ... just use the predicate. We use this as a workaround to support swagger file for codegeneration. Not perfect but works!
public Iterable<SCGameInfo> findSCGameInfo(
#QuerydslPredicate(root = GameInfo.class) Predicate predicate,
#RequestParam(name= "gameName",required = false) String gameName,
#RequestParam(name= "vendor",required = false) String vendor
){
return scService.findAllGameInfosForPredicate(predicate);
}
To reduce the DB hits to read the data from DB using the query, I am planning to keep resultant in the cache. To do this I am using guava caching.
studentController.java
public Map<String, Object> getSomeMethodName(Number departmentId, String departmentType){
ArrayList<Student> studentList = studentManager.getStudentListByDepartmentType(departmentId, departmentType);
----------
----------
}
StudentHibernateDao.java(criteria query )
#Override
public ArrayList<Student> getStudentListByDepartmentType(Number departmentId, String departmentType) {
Criteria criteria =sessionFactory.getCurrentSession().createCriteria(Student.class);
criteria.add(Restrictions.eq("departmentId", departmentId));
criteria.add(Restrictions.eq("departmentType", departmentType));
ArrayList<Student> studentList = (ArrayList)criteria.list();
return studentList;
}
To cache the criteria query resultant i started off with building CacheBuilder, like below.
private static LoadingCache<Number departmentId, String departmentType, ArrayList<Student>> studentListCache = CacheBuilder
.newBuilder().expireAfterAccess(1, TimeUnit.MINUTES)
.maximumSize(1000)
.build(new CacheLoader<Number departmentId, String departmentType, ArrayList<Student>>() {
public ArrayList<Student> load(String key) throws Exception {
return getStudentListByDepartmentType(departmentId, departmentType);
}
});
Here I dont know where to put CacheBuilder function and how to pass multiple key parameters(i.e departmentId and departmentType) to CacheLoader and call it.
Is this the correct way of caching using guava. Am I missing anything?
Guava's cache only accepts two type parameters, a key and a value type. If you want your key to be a compound key then you need to build a new compound type to encapsulate it. Effectively it would need to look like this (I apologize for my syntax, I don't use Java that often):
// Compound Key type
class CompoundDepartmentId {
public CompoundDepartmentId(Long departmentId, String departmentType) {
this.departmentId = departmentId;
this.departmentType = departmentType;
}
}
private static LoadingCache<CompoundDepartmentId, ArrayList<Student>> studentListCache =
CacheBuilder
.newBuilder().expireAfterAccess(1, TimeUnit.MINUTES)
.maximumSize(1000)
.build(new CacheLoader<CompoundDepartmentId, ArrayList<Student>>() {
public ArrayList<Student> load(CompoundDepartmentId key) throws Exception {
return getStudentListByDepartmentType(key.departmentId, key.departmentType);
}
});
Can somebody please explain me how to wrap existing REST API in graphql,
and how it will improve the performance ?
Any small working example would help a lot.
Thanks in advance !!!
Define the Query/Mutation in GraphQL schema.
type Query {
getBook(id: String): Book
}
Inside the Query resolver class use any method to consume rest service.
public class Query implements GraphQLQueryResolver {
public Book getBook(String bookId) {
RestTemplate restTemplate = new RestTemplate();
Map<String, String> map = new HashMap<>();
map.put("bookId", bookId);
ResponseEntity <Book> response = restTemplate.getForEntity("http://localhost:8080/bookDetails/getBook/{bookId}",Book.class, map);
Book book = response.getBody();
return book;
}
}
In the same way you can define Mutation also by implementing GraphQLMutationResolver.