Pagination in Spring Data JPA (limit and offset)
Asked Answered
D

9

79

I want the user to be able to specify the limit (the size of the amount returned) and offset (the first record returned / index returned) in my query method.

Here are my classes without any paging capabilities. My entity:

@Entity
public Employee {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;

    @Column(name="NAME")
    private String name;

    //getters and setters
}

My repository:

public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
    
    @Query("SELECT e FROM Employee e WHERE e.name LIKE :name ORDER BY e.id")
    public List<Employee> findByName(@Param("name") String name);
}

My service interface:

public interface EmployeeService {

    public List<Employee> findByName(String name);
}

My service implementation:

public class EmployeeServiceImpl {

    @Resource
    EmployeeRepository repository;

    @Override
    public List<Employee> findByName(String name) {
        return repository.findByName(name);
    }
}

Now here is my attempt at providing paging capabilities that support offset and limit. My entity class remains the same.

My "new" repository takes in a pageable parameter:

public interface EmployeeRepository extends JpaRepository<Employee, Integer> {

    @Query("SELECT e FROM Employee e WHERE e.name LIKE :name ORDER BY e.id")
    public List<Employee> findByName(@Param("name") String name, Pageable pageable);
}

My "new" service interface takes in two additional parameters:

public interface EmployeeService {
    
    public List<Employee> findByName(String name, int offset, int limit);
}

My "new" service implementation:

public class EmployeeServiceImpl {
    
    @Resource
    EmployeeRepository repository;
    
    @Override
    public List<Employee> findByName(String name, int offset, int limit) {
        return repository.findByName(name, new PageRequest(offset, limit);
    }
}

This however isn't what i want. PageRequest specifies the page and size (page # and the size of the page). Now specifying the size is exactly what I want, however, I don't want to specify the starting page #, I want the user to be able to specify the starting record / index. I want something similar to

public List<Employee> findByName(String name, int offset, int limit) {
    TypedQuery<Employee> query = entityManager.createQuery("SELECT e FROM Employee e WHERE e.name LIKE :name ORDER BY e.id", Employee.class);
    query.setFirstResult(offset);
    query.setMaxResults(limit);
    return query.getResultList();
}

Specifically the setFirstResult() and setMaxResult() methods. But I can't use this method because I want to use the Employee repository interface. (Or is it actually better to define queries through the entityManager?)

Is there a way to specify the offset without using the entityManager?

Deyo answered 29/7, 2014 at 4:56 Comment(1)
#30218052Cynde
S
121

Below code should do it. I am using in my own project and tested for most cases.

usage:

   Pageable pageable = new OffsetBasedPageRequest(offset, limit);
   return this.dataServices.findAllInclusive(pageable);

and the source code:

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.data.domain.AbstractPageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

import java.io.Serializable;

/**
* Created by Ergin
**/
public class OffsetBasedPageRequest implements Pageable, Serializable {

    private static final long serialVersionUID = -25822477129613575L;

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

    /**
     * Creates a new {@link OffsetBasedPageRequest} with sort parameters applied.
     *
     * @param offset zero-based offset.
     * @param limit  the size of the elements to be returned.
     * @param sort   can be {@literal null}.
     */
    public OffsetBasedPageRequest(int offset, int limit, Sort sort) {
        if (offset < 0) {
            throw new IllegalArgumentException("Offset index must not be less than zero!");
        }

        if (limit < 1) {
            throw new IllegalArgumentException("Limit must not be less than one!");
        }
        this.limit = limit;
        this.offset = offset;
        this.sort = sort;
    }

    /**
     * Creates a new {@link OffsetBasedPageRequest} with sort parameters applied.
     *
     * @param offset     zero-based offset.
     * @param limit      the size of the elements to be returned.
     * @param direction  the direction of the {@link Sort} to be specified, can be {@literal null}.
     * @param properties the properties to sort by, must not be {@literal null} or empty.
     */
    public OffsetBasedPageRequest(int offset, int limit, Sort.Direction direction, String... properties) {
        this(offset, limit, new Sort(direction, properties));
    }

    /**
     * Creates a new {@link OffsetBasedPageRequest} with sort parameters applied.
     *
     * @param offset zero-based offset.
     * @param limit  the size of the elements to be returned.
     */
    public OffsetBasedPageRequest(int offset, int limit) {
        this(offset, limit, Sort.unsorted());
    }

    @Override
    public int getPageNumber() {
        return offset / limit;
    }

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

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

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

    @Override
    public Pageable next() {
        return new OffsetBasedPageRequest(getOffset() + getPageSize(), getPageSize(), getSort());
    }

    public OffsetBasedPageRequest previous() {
        return hasPrevious() ? new OffsetBasedPageRequest(getOffset() - getPageSize(), getPageSize(), getSort()) : this;
    }


    @Override
    public Pageable previousOrFirst() {
        return hasPrevious() ? previous() : first();
    }

    @Override
    public Pageable first() {
        return new OffsetBasedPageRequest(0, getPageSize(), getSort());
    }

    @Override
    public boolean hasPrevious() {
        return offset > limit;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;

        if (!(o instanceof OffsetBasedPageRequest)) return false;

        OffsetBasedPageRequest that = (OffsetBasedPageRequest) o;

        return new EqualsBuilder()
                .append(limit, that.limit)
                .append(offset, that.offset)
                .append(sort, that.sort)
                .isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 37)
                .append(limit)
                .append(offset)
                .append(sort)
                .toHashCode();
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this)
                .append("limit", limit)
                .append("offset", offset)
                .append("sort", sort)
                .toString();
    }
}
Shayshaya answered 1/4, 2016 at 20:42 Comment(8)
This is nice, just what I need, but what is an EqualsBuilder?Rumormonger
@Rumormonger it's Apache common library for comparing equality of the fields. You can just compare each fields one by one. Please see this article for details mkyong.com/java/java-how-to-overrides-equals-and-hashcodeShayshaya
For hasPrevious(), shouldn't it be return offset >= limit; ?Upgrowth
It would be a good idea to include this or something similar directly in the framework...Loreenlorelei
Does this perform a sort on all the records in the DB first before returning the subset of rows? I mean if I query for, let's say, 10 rows, will they be taken from the beginning of the table and then sorted or they will just be a chunk of all the records from DB sorted?Lymphangitis
This is the layer pass that information to your persistent layer and that layer handles sorting and pagination on the fly. How sorting and pagination done depends on the persistent layers (optimization of sorting all records vs some is something handled at the persistent layer). see here - baeldung.com/hibernate-paginationShayshaya
In Spring 2.3.4 - getting error for Sort method 'Sort(Direction, List<string>) has private access org.springframework.data.domain.Sort. Also offset has changed to long datatype instead of intHedve
@shivajibhosale By checking documentation I found Sort.by(Direction, String...) to construct a Sort object from direction and properties that replaces the previous constructor.Stannfield
J
20

You can do that by creating your own Pageable.

Try out this basic sample. Works fine for me:

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

public class ChunkRequest implements Pageable {

    private final int offset;
    private final int limit;

    // this attribute can be let out if you don't need it
    private Sort sort;

    public ChunkRequest(int offset, int limit, Sort sort) {
        if (offset < 0)
            throw new IllegalArgumentException("Offset must not be less than zero!");

        if (limit < 0)
            throw new IllegalArgumentException("Limit must not be less than zero!");

        this.offset = offset;
        this.limit = limit;

        if (sort != null) {
            this.sort = sort;
        }
    }

    public ChunkRequest(int offset, int limit) {
        this(offset, limit, null);
    }

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

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

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

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

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

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

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

    @Override
    public Pageable withPage(int pageNumber) { return null; }

    @Override
    public boolean hasPrevious() { return false; }
}
Jethro answered 24/9, 2015 at 14:24 Comment(1)
Why is offset = skip and limit = offset? Why don't you set offset and limit directly?Burgher
S
16

Here you go:

public interface EmployeeRepository extends JpaRepository<Employee, Integer> {

    @Query(value="SELECT e FROM Employee e WHERE e.name LIKE ?1 ORDER BY e.id offset ?2 limit ?3", nativeQuery = true)
    public List<Employee> findByNameAndMore(String name, int offset, int limit);
}
Sumach answered 6/2, 2018 at 20:25 Comment(1)
Readers, be aware this is solution uses a native SQL query, not a JPQL one.Manganese
C
13

Maybe the answer is kind of late, but I thought about the same thing. Compute the current page based on offset and limit. Well, it is not exactly the same because it "assumes" that the offset is a multiple of the limit, but maybe your application is suitable for this.

@Override
public List<Employee> findByName(String name, int offset, int limit) {
    // limit != 0 ;)
    int page = offset / limit;
    return repository.findByName(name, new PageRequest(page, limit));
}

I would suggest a change of the architecture. Change your controller or whatever calls the service to initially give you page and limit if possible.

Ciborium answered 14/4, 2015 at 12:40 Comment(1)
This will only work under very controlled circumstances. If offset is lower than limit it will be bugged because page will round uncontrollably. Offset = 9, Limit = 100 for example, will still return the first 9 rows.Classieclassification
A
9

Adapting the good @codingmonkey awnser with long offset and Sort.by().

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import java.io.Serializable;

public class OffsetBasedPageRequest implements Pageable, Serializable {

    private static final long serialVersionUID = -25822477129613575L;

    private final int limit;
    private final long offset;
    private final Sort sort;

    /**
     * Creates a new {@link OffsetBasedPageRequest} with sort parameters applied.
     *
     * @param offset zero-based offset.
     * @param limit  the size of the elements to be returned.
     * @param sort   can be {@literal null}.
     */
    public OffsetBasedPageRequest(long offset, int limit, Sort sort) {
        if (offset < 0) {
            throw new IllegalArgumentException("Offset index must not be less than zero!");
        }

        if (limit < 1) {
            throw new IllegalArgumentException("Limit must not be less than one!");
        }
        this.limit = limit;
        this.offset = offset;
        this.sort = sort;
    }

    /**
     * Creates a new {@link OffsetBasedPageRequest} with sort parameters applied.
     *
     * @param offset     zero-based offset.
     * @param limit      the size of the elements to be returned.
     * @param direction  the direction of the {@link Sort} to be specified, can be {@literal null}.
     * @param properties the properties to sort by, must not be {@literal null} or empty.
     */
    public OffsetBasedPageRequest(long offset, int limit, Sort.Direction direction, String... properties) {
        this(offset, limit, Sort.by(direction, properties));
    }

    /**
     * Creates a new {@link OffsetBasedPageRequest} with sort parameters applied.
     *
     * @param offset zero-based offset.
     * @param limit  the size of the elements to be returned.
     */
    public OffsetBasedPageRequest(int offset, int limit) {
        this(offset, limit, Sort.unsorted());
    }

    @Override
    public int getPageNumber() {
        return Math.toIntExact(offset / limit);
    }

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

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

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

    @Override
    public Pageable next() {
        return new OffsetBasedPageRequest(getOffset() + getPageSize(), getPageSize(), getSort());
    }

    public OffsetBasedPageRequest previous() {
        return hasPrevious() ? new OffsetBasedPageRequest(getOffset() - getPageSize(), getPageSize(), getSort()) : this;
    }


    @Override
    public Pageable previousOrFirst() {
        return hasPrevious() ? previous() : first();
    }

    @Override
    public Pageable first() {
        return new OffsetBasedPageRequest(0, getPageSize(), getSort());
    }

    @Override
    public boolean hasPrevious() {
        return offset > limit;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;

        if (!(o instanceof OffsetBasedPageRequest)) return false;

        OffsetBasedPageRequest that = (OffsetBasedPageRequest) o;

        return new EqualsBuilder()
                .append(limit, that.limit)
                .append(offset, that.offset)
                .append(sort, that.sort)
                .isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 37)
                .append(limit)
                .append(offset)
                .append(sort)
                .toHashCode();
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this)
                .append("limit", limit)
                .append("offset", offset)
                .append("sort", sort)
                .toString();
    }
}
Anlace answered 5/8, 2021 at 8:19 Comment(2)
Thanks buddy! I made limit and offset final, since they won't be changed. Also IntelliJ claims that.Actinoid
Spring-boot-parent in version 2.6.0 also wants a method called withPage. In case anyone needs it, I'll leave it here: @Override public Pageable withPage(int pageNumber) { return new OffsetBasedPageRequest((long) pageNumber * getPageSize(), getPageSize(), getSort()); }Actinoid
S
4

You probably can't to this with spring data jpa. If the offset is very small, you might just remove the top X statements from the query after retrieval.

Otherwise, you could define the page size to be the offset and start at page+1.

Slicker answered 29/7, 2014 at 13:9 Comment(1)
I like your idea of defining the page size to be the offset, but the only down side would be i couldn't set the actual page size (limit) of the results. Thanks for the feedback!Deyo
G
1

Try that:

public interface ContactRepository extends JpaRepository<Contact, Long> 
{
    @Query(value = "Select c.* from contacts c where c.username is not null order by c.id asc limit ?1,  ?2 ", nativeQuery = true)         
    List<Contact> findContacts(int offset, int limit);        
}
Graphomotor answered 23/1, 2019 at 7:16 Comment(0)
V
1

In my case I had to write a native custom query, as suggested by @supercobra, but his solution was not working because of limit being after offset key. Also, i prefer using parameter names instead of indexes.

So the working solution for me is:

@Query(value="SELECT e FROM Employee e WHERE e.name LIKE :name ORDER BY e.id limit :limit offset :offset", nativeQuery = true)
public List<Employee> findByNameAndMore(String name, int limit, int offset);
Vulnerary answered 23/4, 2024 at 11:15 Comment(0)
R
0

Suppose you are filtering and soring and paging at same time Below @Query will help you

    @Query(value = "SELECT * FROM table  WHERE firstname= ?1  or lastname= ?2 or age= ?3 or city= ?4 or "
        + " ORDER BY date DESC OFFSET ?8 ROWS FETCH NEXT ?9 ROWS ONLY" , nativeQuery = true)
List<JobVacancy> filterJobVacancyByParams(final String firstname, final String lastname,
        final String age, final float city,int offset, int limit);
Representational answered 23/2, 2019 at 3:14 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.