How to Implement Dynamic Pagination Using a Native Query
Here, you can find the repository and service layers and your data transfer object (DTO), which will be used for mapping our result and sending it to the controller layer.
public interface CustomSomethingRepository {
List<Something> findPagedResultBySomethingElseId(long somethingElseId, int offset, int limit);
}
public class SomethingRepositoryImpl implements CustomSomethingRepository {
@Autowired
private EntityManager em;
@SuppressWarnings("unchecked")
@Override
public List<Something> findPagedResultBySomethingElseId(long somethingElseId, int offset, int limit) {
String query = "select s.* from Something s "
+ "join somethingelse selse on selse.id = s.fk_somethingelse "
+ "where selse.id = :somethingElseId "
+ "order by selse.date";
Query nativeQuery = em.createNativeQuery(query);
nativeQuery.setParameter("somethingElseId", somethingElseId);
//Paginering
nativeQuery.setFirstResult(offset);
nativeQuery.setMaxResults(limit);
final List<Object[]> resultList = nativeQuery.getResultList();
List<Something> somethingList = Lists.newArrayList();
resultList.forEach(object -> somethingList.add(//map obj to something));
return somethingList;
}
}
Hibernate translates your query as follows:
SELECT inner_query.*, ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as __hibernate_row_nr__ FROM ( select TOP(?) t as page0_ from Something s join s.somethingelse as selse order by selse.date ) inner_query ) SELECT page0_ FROM query WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?
@Service
public class SomethingService {
private SomethingRepository somethingRepository;
@Autowired
public SomethingService(SomethingRepository somethingRepository){
this.somethingRepository = somethingRepository;
}
@Transactional(readOnly=true)
public PageDto getSomething(long somethingElseId, int page, int size){
List<Something> somethings = somethingRepository.findBySomethingElseId(somethingElseId, offset, limit);
return new PagedResult<>(somethings
.stream()
.map(SomethingDto::createDto)
.sorted(comparing(SomethingDto::getDatum))
.collect(toList()), somethings.getTotalElements(), somethings.getTotalPages();
}
}
@Controller
//....
public class PagedResult<T> {
public static final long DEFAULT_OFFSET = 0;
public static final int DEFAULT_MAX_NO_OF_ROWS = 100;
private int offset;
private int limit;
private long totalElements;
private List<T> elements;
public PagedResult(List<T> elements, long totalElements, int offset, int limit) {
this.elements = elements;
this.totalElements = totalElements;
this.offset = offset;
this.limit = limit;
}
public boolean hasMore() {
return totalElements > offset + limit;
}
public boolean hasPrevious() {
return offset > 0 && totalElements > 0;
}
public long getTotalElements() {
return totalElements;
}
public int getOffset() {
return offset;
}
public int getLimit() {
return limit;
}
public List<T> getElements() {
return elements;
}
}
Pros and Cons
Pros: Fewer SQL queries will be generated, compared to using Spring Data. These complex queries cannot be written in Spring Data, and we have to specify our query as a native one, which can still be paged by using this methodology.
Cons: The "object" array must map to a Java object. It is painful and hard to maintain.
How to Implement OffsetLimit Pagination With Spring Data
As far as I know, there is no "out-of-the-box" support for what you need in default Spring Data repositories. But you can create a custom implementation of Pageable objects that will take limit/offset parameters.
Make a pageable object and pass it to PaginationAndSortingRepository:
public class OffsetLimitRequest implements Pageable {
private int limit;
private int offset;
public OffsetLimitRequest(int offset, int limit){
this.limit = limit;
this.offset = offset;
}
@Override
public int getPageNumber() {
return 0;
}
@Override
public int getPageSize() {
return limit;
}
@Override
public int getOffset() {
return offset;
}
....
}
It means there is no need to change the repository layer. The only change you would need is to make is to the service layer, as follows:
@Service
public class SomethingService {
private SomethingRepository somethingRepository;
@Autowired
public SomethingService(SomethingRepository somethingRepository){
this.somethingRepository = somethingRepository;
}
@Transactional(readOnly=true)
public PageDto getSomething(long somethingElseId, int page, int size){
Page<Something> somethings = somethingRepository.findBySomethingElseId(somethingElseId, new OffsetLimitRequest(offset, limit));
return new PageDto(somethings.getContent()
.stream()
.map(SomethingDto::createDto)
.sorted(comparing(SomethingDto::getDatum))
.collect(toList()), somethings.getTotalElements(), somethings.getTotalPages();
}
}
Note that you don't need to map the result manually, and it will take off a good amount of time from development.