Spring Data Pagination & AJAX - java

I have the following Controller and I've just included pagination into my returned results
#RequestMapping(value = "/search/{person}", produces="application/json", method = RequestMethod.GET)
public Page<Person> findAllPersons(#PathVariable String person) {
Page<Person> list = personRepo.findAll(new PageRequest(1, PAGE_SIZE));
return list;
}
I'm now trying to figure out how to actually tab through these results - The search on the Person table has is it's own AJAX request, where as selecting "next" or "previous" on my UI tool can launch it's own GET
<a id="previous" href="onclick="setPageNumber(1)">
<a id="next" href="onclick="setPageNumber(2)">
function setPageNumber(num) { //relaunch request with page number value retrieved from previous or next}
Should I include a pageNumber as a #PathVariable like so:
#RequestMapping(value = "/search/{person}/{pageNumber}", produces="application/json", method = RequestMethod.GET)
public Page<Person> findAllPersons(#PathVariable String person, #PathVariable int pageNumber) {
Page<Person> list = personRepo.findAll(new PageRequest(pageNumber, PAGE_SIZE));
return list;
}
or should setting the pageNumber be a completely separate controller method that somehow invokes findAllPersons with the pageNumber argument? I may be confusing myself here - any input is welcome thanks!

For REST service I would put it to the parameters rather then to URI page_start=X&page_size=Y.

I know this post isn't active for a long time but for anyone still looking for a way to use Spring pagination with Ajax, here are some possible solutions:
1. If your repository is an instance of JpaRepository (or more precisely PagingAndSortingRepository) then you can simply pass a Pageable to it:
#Controller
public class FooController {
//...
#GetMapping("/foo/list")
public List<Foo> handleList(Pageable pageable) {
return fooRepository.findAll(pageable);
}
//...
}
2. Instead of Pageable, you can also retrieve your pagination parameters as #RequestParam and create a PageRequest yourself. This approach might be useful if the project does not use Spring Data and JPA:
#Controller
public class FooController {
//...
#GetMapping("/foo/list")
public List<Foo> handleList(
#RequestParam(value = "size", required = false) Optional<Integer> pageSize,
#RequestParam(value = "page", required = false) Optional<Integer> pageNumber,
#RequestParam(value = "sort", required = false) Sort sort,
) {
PageRequest pageable = new PageRequest(pageNumber.orElse(0), pageSize.orElse(10), sort);
return fooRepository.customfindAll(pageable);
}
//...
}
(e.g. this repository above might be a class extending a JDBCRepository such as this one )
And for the AJAX part of these two possible solutions, something like that can be used:
// ... handle pageNo, listSize etc.
var url = '/yourprj/foo/list?page=' + pageNo + '&size=' + listSize
$.ajax({
type: "GET",
contentType: "application/json",
url: url,
success: function(result) {
// handle Foo list...
}
});
3. Alternatively, if you are using Thymeleaf+Spring Data there is a dialect to automatically add pagination.

Related

Is there a better way to provide filtering feature through REST API?

#GetMapping
public ResponseEntity<Page<CsatSurveyModel>> getAllSurveys(
#RequestParam(required = false) String teamName,
#RequestParam(required = false) String customerName,
#RequestParam(required = false) Integer year,
#RequestParam(defaultValue = "id") String orderBy,
#RequestParam(defaultValue = "DESC") Direction direction,
#RequestParam(defaultValue = AppConstant.DEFAULT_PAGE) int page,
#RequestParam(defaultValue = AppConstant.DEFAULT_PAGE_SIZE) int size) {
Sort sort = Sort.by(direction, orderBy);
Pageable pageRequest = PageRequest.of(page, size, sort);
Specification<CsatSurvey> csatSurveySpecification = Specification.where(null);
if (Objects.nonNull(teamName)) {
csatSurveySpecification = csatSurveySpecification.and(CsatSurvey.teamNameSpec(teamName));
}
if (Objects.nonNull(customerName)) {
csatSurveySpecification =
csatSurveySpecification.and(CsatSurvey.customerNameSpec(customerName));
}
if (Objects.nonNull(year)) {
csatSurveySpecification = csatSurveySpecification.and(CsatSurvey.yearSpec(year));
}
UserModel loggedInUser = sessionUtils.getLoggedInUser();
List<Team> teams =
UserRole.ADMIN.equals(loggedInUser.getRole())
? Collections.emptyList()
: loggedInUser.getTeams();
Page<CsatSurveyModel> csatSurveyModels =
csatService.getAllSurveysForTeams(teams, csatSurveySpecification, pageRequest);
return ResponseEntity.ok(csatSurveyModels);
}
The first three parameters are used for filtering purposes with specifications. The rest is for page requests. I was wondering if there's a better way to do this. There's a lot of code in the controller, and even if I want to move the processing to the service layer, the method would have to accept a long list of parameters, which I don't want to do. Although this method only accepts seven parameters, there are other routes that accept more than ten parameters.
I understand that one way is to accept all these params as Map<String, String>, but isn't it a bit tedious to process that?
My way is using a request class.
The advantage is you can change the params without changing the method signature both for the controller and the service (assuming you pass that request object to the service as well).
Example of a Controller method:
public UserDataResponse getUserData(#RequestBody UserDataRequest userDataRequest)
Where UserDataRequest is a simple class with getters and setters.
class UserDataRequest {
private String paramA;
private String paramB;
public String getParamA() {
return paramA;
}
}
etc.
Spring MVC can take a Pageable (or a Sort) as a controller parameter directly, and Spring Data can accept one as a query parameter.

Spring Boot: Slice/Pageable not returning proper chunk based on page

In my PSQL DB, I have two objects stored and I would like to retrieve each item on their separate page/slice. I am trying to achieve this by passing in the following Page objects:
PageRequest.of(0,1) for the first item and PageRequest.of(1, 1) for the second item.
However, when I create the Pageable object via PageRequest.of(1, 1), this always results in only the first item being returned every time, but I have confirmed that both items indeed does exist by calling repo.findAll().
What am I doing incorrectly?
My service layer call looks like the following:
#Transactional
public Slice<Foo> findAllInactive(Pageable pageable) {
return repo.findAllInactive(new Date(), pageable));
}
And my repo is:
#Repository
public interface FooRepository extends JpaRepository<Foo, String> {
value =
"SELECT * FROM fooschema.foo i WHERE i.valid_until < :currentDate OR i.valid_until IS NULL --#pageable\n",
nativeQuery = true,
countQuery = "SELECT count(*) FROM fooschema.foo i")
Slice<Foo> findAllInactive(#Param("currentDate") Date currentDate, Pageable pageable);
}
If it makes any difference, here is the test call
#Autowired private MockMvc mvc;
#Test
void testStuff() throws Exception {
// two elements added....
ResultActions resultActions =
mvc.perform(
get("/foo")
.param("page", "1")
.param("size", "1"))// should return the second element, but returns the first
.andExpect(status().isOk())
.andExpect(content().contentType("application/json"));
}
and the controller
#RestController
#RequestMapping("/foo")
public class FooController {
#GetMapping
#ApiImplicitParams({
#ApiImplicitParam(
name = "page",
dataType = "int",
paramType = "query",
value = "Page you want to retrieve",
defaultValue = "0"),
#ApiImplicitParam(
name = "size",
dataType = "int",
paramType = "query",
value = "Number of foo per page.",
defaultValue = "10"))
public Slice<Foo> getFoo(Pageable pageable) {
return service.findAllInactive(pageable);
}
}
Anshul's comment got me on the right track, and in the end, it appears that creating a derived query, as noted here: https://www.baeldung.com/spring-data-derived-queries, works.
In the end, the following got it working for me:
Slice<Foo> findByValidUntilIsNullOrValidUntilBefore(Date currentDate, Pageable pageable); // or can return a List<Foo>
Can you try with Page object instead Slice.
Step-1 - Create a page size
Pageable page1 = PageRequest.of(0, 1);
Pageable page2 = PageRequest.of(1, 1);
Step-2
Page <Foo> findAllInactive(Date currentDate, Pageable page2);

Generate and set self links in nested items

I should migrate some code from jax-rs to spring mvc. We had a controller, which response with an object and set at the same time links in a list :
HateoasResponse.ok(content)
.selfLink(FieldPath.path("categories"), "some_controller_id", "id")
.build()
Did any one know, if there is something similar in spring mvc ?
I have checked spring-hateoas. If I use it , I should modify my models to something supported by this package (CollectionModel, EnitityModel..)
You have to make the response object extend ResourceSupport and then generate the links as follows.
org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo(methodOn(YourSpringMvcController.class)
.methodWhichHasMappingTo(param1,param2,paramN))
.withRel("relationOfThisLinkToTheRequestedResource").expand();
This link can then be added to the response object using the resource add method.
for example, let's say you have a controller like the following:
#RestController
public class OrderController {
#GetMapping(value = "/orders/{orderId}", method = RequestMethod.GET)
#ResponseBody
public ResponseEntity<Order> getOrder(#Valid #PathVariable Integer orderId) {
return getOrder(orderId);
}
#DeleteMapping(value = "/orders/{orderId}", method = RequestMethod.GET)
#ResponseBody
public ResponseEntity<Order> deleteOrder(#Valid #PathVariable Integer orderId) {
return orderRepo.deleteOrder(orderId);
}
}
then for a request to GET orders, you would build the response like the following:
Order which is a response entity will extend ResourceSupport
public Order getOrder(int orderId){
Order order = repo.findByOrderId(orderId);
Link deleteLink = ControllerLinkBuilder.linkTo(methodOn(OrderController.class)
.deleteOrder(orderId))
.withRel("delete").expand();
order.add(deleteLink);
Link selfLink = ControllerLinkBuilder.linkTo(methodOn(OrderController.class)
.getOrder(orderId))
.withSelfRel();
order.add(selfLink);
return order;
}
Hope this helps.

Sort passed in Pageable to Spring controller appends direction to property name, causing a SQL error

I need to call a pageable endpoint in my Spring Boot app from another Spring Boot app. I'm trying to pass the pageable options through from the first app to the second, but I'm having an issue where the property name comes in as firstName: ASC. When the direction is appended to that, it becomes firstName: ASC: ASC which causes the JPA query to throw an exception.
What's the proper way to pass the pageable options from my first endpoint to my second?
Calling app
#GetMapping("/v1/users")
public Flux<User> getUsersByAccount(#RequestParam Long accountId,
#PageableDefault(size = 10, sort = "firstName") Pageable pageable) {
return userService.getUsersByAccount(accountId, pageable);
}
public Flux<User> getUsersByAccount(Long accountId, Pageable pageable) {
int page = pageable.getPageNumber();
int size = pageable.getPageSize();
Sort sort = pageable.getSort();
return webClient.backendService()
.get().uri(builder -> builder
.path("/rest/users")
.queryParam("accountId", accountId)
.queryParam("page", page)
.queryParam("size", size)
.queryParam("sort", sort)
.build())
.retrieve()
.bodyToFlux(ContactInfo.class);
}
I am splitting the Pageable out into its components because I wasn't sure how to pass the whole object at once since it's not a named parameter in the second app. Note that at this point, the sort looks fine and appears as it should with firstName and ASC as separate values for property name and direction, respectively.
Called app
#GetMapping("/rest/users")
public List<User> getUsersByAccount(#RequestParam Long accountId, Pageable pageable) {
return userService.getUsersByAccount(accountId, pageable);
}
As #M. Deinum mentioned, Sort's toString() doesn't produce a representation that can be directly serialized back into a Sort object, (and there is no such method on the object to do that).
You can convert it to the proper form like this:
List<String> sorts = new ArrayList<>();
sort.forEach(order -> sorts.add(String.join(",", order.getProperty(), order.getDirection().toString())));
builder.queryParam("sort", sorts.toArray());
This produces the correct representation of ["propertyName,direction"].
Controller we can capture HttpServletRequest as follows
public ResponseEntity<Page<PriceBook>> getAll(HttpServletRequest servletRequest) {
return new ResponseEntity<>(priceBookService.findAll(servletRequest), HttpStatus.OK);
}
use queryString as follows to pass all request parameters to the backend service
public PageableResponse<PriceBook> getPbAllWithFilter(HttpServletRequest servletRequest) {
return webClient.get().uri(pbUrl.concat("?").concat(servletRequest.getQueryString()))
.accept(MediaType.APPLICATION_JSON)
.header(CORRELATION_ID, MDC.get(CORRELATION_ID_MDC))
.exchange()
.flatMap(this::getSubscriptionAll)
.block();
}

Spring: Bind a collection on controller parameter

I've seen lots of questions similar to mine but, I couldn't find a solution to this problem so far.
I am implementing a grid filtering and pagination on Spring + Hibernate. The load() method must receive the specific parameters for pagination (page, start and limit) and a list of key-value parameter for filtering, which is being the problem.
The parameters are coming like that:
page:1
start:0
limit:23
filter:[{"operator":"like","value":"tes","property":"desc"},{"operator":"like","value":"teste","property":"model_desc"}]
or (encoded version):
page=1&start=0&limit=23&filter=%5B%7B%22operator%22%3A%22like%22%2C%22value%22%3A%22tes%22%2C%22property%22%3A%22desc%22%7D%2C%7B%22operator%22%3A%22like%22%2C%22value%22%3A%22teste%22%2C%22property%22%3A%22model_desc%22%7D%5D
The filter parameter is coming as a String and the problem is to make Spring parse that either as something like ArrayList<Map<String,String>> or ArrayList<SomeFilterClass>.
This is the signature of my controller method (the commented lines are all not working, they are here just to show what I've tried so far):
public Map<String, Object> loadData(#RequestParam(value = "page", required = true) int page,
#RequestParam(value = "start", required = true) int start,
#RequestParam(value = "limit", required = true) int limit,
// #ModelAttribute("filter") ArrayList<Map<String, String>> filter) {
// #RequestParam(value = "filter", required = false) Map<String, Object>[] filter) {
// #RequestParam(value = "filter", required = false) List<Map<String, String>> filter) {
#ModelAttribute("filter") RemoteFilter filter)
This class, RemoteFilter, is a wrapper class that I built, following a suggestion from other posts but, it didn't work also. Its structure is:
public class RemoteFilter {
private ArrayList<Filter> filter;
//Getters and Setters....
class Filter {
private String operator;
private String value;
private String property;
//Getters and Setters....
}
}
I will be very glad if anybody help me with that.
Thanks!
Try to POST the data instead of using GET, Spring only offers JSON to Java conversion when data is posted.
Post
{
page:1
start:0
limit:23
filter:[{"operator":"like","value":"tes","property":"desc"},{"operator":"like","value":"teste","property":"model_desc"}]
}
And have the controller use #RequestBody
#RequestMapping(method = RequestMethod.POST, value = "url",
produces = MimeTypeUtils.APPLICATION_JSON_VALUE,
consumes = MimeTypeUtils.APPLICATION_JSON_VALUE)
public Map<String, Object> loadData(#RequestBody RemoteFilter filter) {
}
The response uses Objectas the Map value type. This will work, but using an un-typped return value is a bad thing in general.

Categories