DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Workarounds for Oracle Restrictions on the Size of Expression Lists
  • JSON Handling With GSON in Java With OOP Essence
  • Exploring Apache Ignite With Spring Boot
  • SQL Query Performance Tuning in MySQL

Trending

  • A Modern Stack for Building Scalable Systems
  • Unmasking Entity-Based Data Masking: Best Practices 2025
  • AI’s Role in Everyday Development
  • Docker Base Images Demystified: A Practical Guide
  1. DZone
  2. Coding
  3. Languages
  4. Implementing Infinite Scroll in jOOQ

Implementing Infinite Scroll in jOOQ

Infinite scroll is a classical usage of keyset pagination and is gaining popularity these days.

By 
Anghel Leonard user avatar
Anghel Leonard
DZone Core CORE ·
Updated Jan. 17, 23 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
8.7K Views

Join the DZone community and get the full member experience.

Join For Free

In this article, we cover keyset pagination and infinite scroll via jOOQ. The schema used in the examples is available here.

You may also like "We need tool support for keyset pagination." 

jOOQ Keyset Pagination

Keyset (or seek) pagination doesn't have a default implementation in Spring Boot, but this shouldn't stop you from using it. Simply start by choosing a table's column that should act as the latest visited record/row (for instance, the id column), and use this column in the WHERE and ORDER BY clauses. The idioms relying on the ID column are as follows (sorting by multiple columns follows this same idea):

SQL
 
SELECT ... FROM ...
WHERE id < {last_seen_id}
ORDER BY id DESC
LIMIT {how_many_rows_to_fetch}

SELECT ... FROM ...
WHERE id > {last_seen_id}
ORDER BY id ASC
LIMIT {how_many_rows_to_fetch}


Or, like this:

SQL
 
SELECT ... FROM ...
WHERE ... AND id < {last_seen_id}
ORDER BY id DESC
LIMIT {how_many_rows_to_fetch}

SELECT ... FROM ...
WHERE ... AND id > {last_seen_id}
ORDER BY id ASC
LIMIT {how_many_rows_to_fetch}


Expressing these queries in jOOQ should be a piece of cake. For instance, let's apply the first idiom to the PRODUCT table via PRODUCT_ID:

Java
 
List<Product> result = ctx.selectFrom(PRODUCT)
   .where(PRODUCT.PRODUCT_ID.lt(productId))
   .orderBy(PRODUCT.PRODUCT_ID.desc())
   .limit(size)
   .fetchInto(Product.class);


In MySQL, the rendered SQL is (where productId = 20 and size = 5) as follows:

SQL
 
SELECT `classicmodels`.`product`.`product_id`,
       `classicmodels`.`product`.`product_name`,
       ...
FROM `classicmodels`.`product`
WHERE `classicmodels`.`product`.`product_id` < 20
ORDER BY `classicmodels`.`product`.`product_id` DESC
LIMIT 5


This was easy! You can practice this case in KeysetPagination for MySQL. In the same place, you can find the approach for PostgreSQL, SQL Server, and Oracle.

However, keyset pagination becomes a little bit trickier if the WHERE clause becomes more complicated. Fortunately, jOOQ saves us from this scenario via a synthetic clause named SEEK. Let's dive into it!

The jOOQ SEEK Clause

The jOOQ synthetic SEEK clause simplifies the implementation of keyset pagination. Among its major advantages, the SEEK clause is type-safe and is capable of generating/emulating the correct/expected WHERE clause (including the emulation of row value expressions).

For instance, the previous keyset pagination example can be expressed using the SEEK clause, as shown here (productId is provided by the client): 

Java
 
List<Product> result = ctx.selectFrom(PRODUCT)
   .orderBy(PRODUCT.PRODUCT_ID)
   .seek(productId)
   .limit(size)
   .fetchInto(Product.class);


Note that there is no explicit WHERE clause. jOOQ will generate it on our behalf based on the seek() arguments. While this example may not look so impressive, let's consider another one. This time, let's paginate EMPLOYEE using the employee's office code and salary:

Java
 
List<Employee> result = ctx.selectFrom(EMPLOYEE)
   .orderBy(EMPLOYEE.OFFICE_CODE, EMPLOYEE.SALARY.desc())
   .seek(officeCode, salary)
   .limit(size)
   .fetchInto(Employee.class);


Both officeCode and salary are provided by the client, and they land into the following generated SQL sample (where officeCode = 1, salary = 75000, and size = 10):

SQL
 
SELECT `classicmodels`.`employee`.`employee_number`,
   ...
FROM `classicmodels`.`employee`
WHERE (`classicmodels`.`employee`.`office_code` > '1'
    OR (`classicmodels`.`employee`.`office_code` = '1'
       AND `classicmodels`.`employee`.`salary` < 75000))
ORDER BY `classicmodels`.`employee`.`office_code`,
         `classicmodels`.`employee`.`salary` DESC
LIMIT 10


Check out the generated WHERE clause! I am pretty sure that you don't want to get your hands dirty and explicitly write this clause. How about the following example? 

Java
 
List<Orderdetail> result = ctx.selectFrom(ORDERDETAIL)
   .orderBy(ORDERDETAIL.ORDER_ID, ORDERDETAIL.PRODUCT_ID.desc(),
            ORDERDETAIL.QUANTITY_ORDERED.desc())
   .seek(orderId, productId, quantityOrdered)
   .limit(size)
   .fetchInto(Orderdetail.class);


And the following code is a sample of the generated SQL (where orderId = 10100, productId = 23, quantityOrdered = 30, and size = 10):

SQL
 
SELECT `classicmodels`.`orderdetail`.`orderdetail_id`,
   ...
FROM `classicmodels`.`orderdetail`
WHERE (`classicmodels`.`orderdetail`.`order_id` > 10100
   OR (`classicmodels`.`orderdetail`.`order_id` = 10100
   AND `classicmodels`.`orderdetail`.`product_id` < 23)
   OR (`classicmodels`.`orderdetail`.`order_id` = 10100
   AND `classicmodels`.`orderdetail`.`product_id` = 23
   AND `classicmodels`.`orderdetail`.`quantity_ordered` < 30))
ORDER BY `classicmodels`.`orderdetail`.`order_id`,
         `classicmodels`.`orderdetail`.`product_id` DESC,
         `classicmodels`.`orderdetail`.`quantity_ordered` DESC
LIMIT 10


After this example, I think it is obvious that you should opt for the SEEK clause and let jOOQ do its job! Look, you can even do this:

Java
 
List<Product> result = ctx.selectFrom(PRODUCT)
   .orderBy(PRODUCT.BUY_PRICE, PRODUCT.PRODUCT_ID)
   .seek(PRODUCT.MSRP.minus(PRODUCT.MSRP.mul(0.35)), val(productId))
   .limit(size)
   .fetchInto(Product.class);


You can practice these examples in SeekClausePagination, next to the other examples, including using jOOQ-embedded keys as arguments of the SEEK clause.

Implementing Infinite Scroll

Infinite scroll is a classical usage of keyset pagination and is gaining popularity these days. For instance, let's assume that we plan to obtain something, as shown in this figure:

Order Detail

So, we want an infinite scroll over the ORDERDETAIL table. At each scroll, we fetch the next n records via the SEEK clause:

Java
 
public List<Orderdetail> fetchOrderdetailPageAsc(long orderdetailId, int size) {

  List<Orderdetail> result = ctx.selectFrom(ORDERDETAIL)
     .orderBy(ORDERDETAIL.ORDERDETAIL_ID)
     .seek(orderdetailId)
     .limit(size)
     .fetchInto(Orderdetail.class);
  
  return result;
}


The fetchOrderdetailPageAsc() method gets the last visited ORDERDETAIL_ID and the number of records to fetch (size), and it returns a list of jooq.generated.tables.pojos.Orderdetail, which will be serialized in JSON format via a Spring Boot REST controller endpoint defined as @GetMapping("/orderdetail/{orderdetailId}/{size}").

On the client side, we rely on the JavaScript Fetch API (of course, you can use XMLHttpRequest, jQuery, AngularJS, Vue, React, and so on) to execute an HTTP GET request, as shown here:

JavaScript
 
const postResponse
   = await fetch('/orderdetail/${start}/${size}');
const data = await postResponse.json();


For fetching exactly three records, we replace ${size} with 3. Moreover, the ${start} placeholder should be replaced by the last visited ORDERDETAIL_ID, so the start variable can be computed as the following:

Java
 
start = data[size-1].orderdetailId;


While scrolling, your browser will execute an HTTP request at every three records, as shown here:

 
http://localhost:8080/orderdetail/0/3
http://localhost:8080/orderdetail/3/3
http://localhost:8080/orderdetail/6/3
…


You can check out this example in SeekInfiniteScroll. 

Infinite Scrolling and Dynamic Filters

Now, let's add some filters for ORDERDETAIL that allows a client to choose the price and quantity ordered range, as shown in this figure:

Add Filters

We can easily implement this behavior by fusing the powers of SEEK and SelectQuery:

Java
 
public List<Orderdetail> fetchOrderdetailPageAsc(
   long orderdetailId, int size, BigDecimal priceEach, Integer quantityOrdered) {
  
  SelectQuery sq = ctx.selectFrom(ORDERDETAIL)
    .orderBy(ORDERDETAIL.ORDERDETAIL_ID)
    .seek(orderdetailId)
    .limit(size)
    .getQuery();
  
  if (priceEach != null) {
    sq.addConditions(ORDERDETAIL.PRICE_EACH.between(
      priceEach.subtract(BigDecimal.valueOf(50)), priceEach));
  }
  
  if (quantityOrdered != null) {
    sq.addConditions(ORDERDETAIL.QUANTITY_ORDERED.between(
      quantityOrdered - 25, quantityOrdered));
  }
  
  return sq.fetchInto(Orderdetail.class);
}


The following example URL involves loading the first page of three records that have prices between 50 and 100 and an order quantity between 50 and 75:

 
http://localhost:8080/orderdetail/0/3?priceEach=100&quantityOrdered=75


You can find the complete example in SeekInfiniteScrollFilter for MySQL, SQL Server, PostgreSQL, and Oracle.

JSON MySQL Id (programming language) Java (programming language) Spring Boot sql Data Types Filter (software)

Opinions expressed by DZone contributors are their own.

Related

  • Workarounds for Oracle Restrictions on the Size of Expression Lists
  • JSON Handling With GSON in Java With OOP Essence
  • Exploring Apache Ignite With Spring Boot
  • SQL Query Performance Tuning in MySQL

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!