DEV Community

Cover image for Spring Boot Cassandra Pagination
Milan Barać
Milan Barać

Posted on

Spring Boot Cassandra Pagination

Getting paginated data from Cassandra database in Spring Boot application can be a huge challenge, if you want to do the things right.

DataStax Java driver supports data paging, up to some extent. Using the native paging support via cluster configuration might look like this:

// At initialization:
Cluster cluster = Cluster.builder()
    .addContactPoint("127.0.0.1")
    .withQueryOptions(new QueryOptions().setFetchSize(2000))
    .build();

// Or at runtime:
cluster.getConfiguration().getQueryOptions().setFetchSize(2000);
Enter fullscreen mode Exit fullscreen mode

The fetch size can also be set on a statement:

Statement statement = new SimpleStatement("your query");
statement.setFetchSize(2000);
Enter fullscreen mode Exit fullscreen mode

If not set, the fetch size defaults to 5000.

Result set iteration is done this way:

ResultSet rs = session.execute("your query");
for (Row row : rs) {
    if (rs.getAvailableWithoutFetching() == 100 && !rs.isFullyFetched())
        rs.fetchMoreResults(); // this is asynchronous
    // Process the row ...
    System.out.println(row);
}
Enter fullscreen mode Exit fullscreen mode

Now comes the part which usually gives you a headache, if you want to allow your API service call to collect and return Pageable data.

Native DataStax Java driver allows saving and reusing paging state:

ResultSet resultSet = session.execute("your query");
// iterate the result set...
PagingState pagingState = resultSet.getExecutionInfo().getPagingState();
Enter fullscreen mode Exit fullscreen mode
String string = pagingState.toString();
byte[] bytes = pagingState.toBytes();
Enter fullscreen mode Exit fullscreen mode

The URL to the next page is passed as http://someservice.com/results?page=<...>. The state value can be deserialised later and used in a query:

PagingState pagingState = PagingState.fromString(string);
Statement st = new SimpleStatement("your query");
st.setPagingState(pagingState);
ResultSet rs = session.execute(st);
Enter fullscreen mode Exit fullscreen mode

Now, if you want to allow the frontend application to render a nice pagination component like this, the native state exchanging mechanism simply won't be enough:

Angular Material Paginator

We need to keep track of paging state in terms of page, size and total properties, required by frontend application.

This problem can be bypassed by creating two simple utility Java classes. The first one serves as a class to contain paging states and total number of rows, while the second one does the actual pagination:

package com.acme.library.utils;

import com.datastax.driver.core.PagingState;

import java.util.HashMap;
import java.util.Map;

public class PagingStats {
    private Map<Integer, PagingState> states = new HashMap<>();
    private Integer total = 0;

    private PagingStats() {
    }

    private PagingStats(Map<Integer, PagingState> states, Integer total) {
        this.states = states;
        this.total = total;
    }

    public static PagingStats createStats(Map<Integer, PagingState> state, Integer total)
    {
        return new PagingStats(state, total);
    }

    public Map<Integer, PagingState> getStates() {
        return states;
    }

    public void setStates(Map<Integer, PagingState> states) {
        this.states = states;
    }

    public Integer getTotal() {
        return total;
    }

    public void setTotal(Integer total) {
        this.total = total;
    }
}
Enter fullscreen mode Exit fullscreen mode
package com.acme.library.utils;

import com.datastax.driver.core.PagingState;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.Statement;
import com.datastax.driver.mapping.Mapper;
import com.datastax.driver.mapping.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;

import java.util.*;

public class PagingUtil {

    private final static Logger log = LoggerFactory.getLogger(PagingUtil.class);

    /**
     *
     * Retrieve Page of database rows for the specified pageable param.
     *
     * @param mapper mapper
     * @param session session
     * @param select select
     * @param pageable pageable
     * @return Page
     * @param <T> T
     */
    public static <T> Page<T> getPage(Mapper<T> mapper, Session session, Statement select, Pageable pageable) {
        try {
            return getAll(mapper, session, select, pageable);
        } catch (Exception e) {
            log.error(String.format("Error getting pageable data: %s", e.getMessage()));
            return new PageImpl<>(new ArrayList<>(0), pageable, 0);
        }
    }

    /**
     * Retrieve paging stats with states and total
     *
     * @param session session
     * @param select select statement
     * @param size size
     * @return PagingStats
     */
    private static PagingStats getPagingStats(Session session, Statement select, Integer size) {
        // Prepare stats variables
        Map<Integer, PagingState> states = new HashMap<>();
        PagingState state = null;

        int n = 0;
        int total = 0;

        do {
            // Get the result
            select.setFetchSize(size).setPagingState(state);
            ResultSet result = session.execute(select);

            // Get stats values
            state = result.getExecutionInfo().getPagingState();
            total += result.getAvailableWithoutFetching();

            if(state != null) {
                states.put(++n, state);
            }
        } while (state != null);

        // Return stats data
        return PagingStats.createStats(states, total);
    }

    /**
     * Map data rows
     *
     * @param mapper mapper
     * @param session session
     * @param statement select statement
     * @return List
     * @param <T> T
     */
    private static <T> Result<T> mapRows(Mapper<T> mapper, Session session, Statement statement) {
        // Get paged results
        ResultSet results = session.execute(statement);

        // Return results
        return mapper.map(results);
    }

    /**
     * Get all paged data
     *
     * @param mapper mapper
     * @param session session
     * @param select select session
     * @param pageable pageable
     * @return Page
     * @param <T> T
     */
    private static <T> Page<T> getAll(Mapper<T> mapper, Session session, Statement select, Pageable pageable) {
        // Get paging stats
        PagingStats stats = getPagingStats(session, select, pageable.getPageSize());

        // Check for page validity
        int start = pageable.getPageNumber() * pageable.getPageSize();

        if (start > stats.getTotal()) {
            return new PageImpl<>(new ArrayList<>(), pageable, 0L);
        }

        // Prepare command parameters
        select.setPagingState(stats.getStates().get(pageable.getPageNumber()));

        // Get and map rows
        Result<T> rows = mapRows(mapper, session, select);

        // Return paged rows
        return new PageImpl<>(rows.all(), pageable, stats.getTotal());
    }
}
Enter fullscreen mode Exit fullscreen mode

The star of the show is the getPagingStats(…) method, which prepares all potential states for a given Pageable parameter all at once, keeping track of total number of rows too.

Then, the getPage(…) method does the actual pagination, executing the given Select statement for the open Session and Pageable parameter.

The Select statement gets the exact paginated data by looking up the hashed state, depending on given Pageable parameter.

The paginated data are mapped from raw data to entity format using given Mapper<…>.

The PagingUtil class is used like this in repository layer:

private Mapper<Action> mapper = new MappingManager(session).mapper(Action.class);
Enter fullscreen mode Exit fullscreen mode
private PreparedStatement findAllStmt = session.prepare("SELECT * FROM action");
Enter fullscreen mode Exit fullscreen mode
private Page<Action> getPageFromSelect(BoundStatement select, Pageable pageable) {
    return PagingUtil.getPage(mapper, session, select, pageable);
}
Enter fullscreen mode Exit fullscreen mode
public Page<Action> findAllPageable(Pageable pageable) {
    return getPageFromSelect(findAllStmt.bind(), pageable);
}
Enter fullscreen mode Exit fullscreen mode

Service layer might look like this:

public Page<Action> findAllPageable(Pageable pageable) {
    return actionRepository.findAllPageable(pageable);
}
Enter fullscreen mode Exit fullscreen mode

Then in REST controller:

/**
 * GET  /action/pageable : get all actions pageable.
 *
 * @param pageable the pagination information
 * @return the ResponseEntity with status 200 (OK) and with body all actions
 */
@GetMapping("/action/pageable")
@Protected
@Audited
@ApiOperation(value = "Get all actions paged", response = Action.class, responseContainer = "List", authorizations = { @Authorization(value = "token"), @Authorization(value = "secret"), @Authorization(value = "basic") })
public ResponseEntity<List<Action>> getAllActionsPageable(@Param Pageable pageable) {
    Page<Action> page = actionService.findAllPageable(pageable);
    HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/action/pageable");
    return new ResponseEntity<> (page.getContent(), headers, HttpStatus.OK);
}
Enter fullscreen mode Exit fullscreen mode

REST API call executed from frontend client can have this format:

http://someservice.com/results/api/action/pageable?page=4&size=10

with the following json response:

[
  {
    "id": 41,
    "name": "getAllRoles"
  }, 
  {
    "id": 42,
    "name": "getAllUsers"
  },
  ...
]
Enter fullscreen mode Exit fullscreen mode

The response headers are used for sending paging data to a client:

Link:

</action/pageable?page=5&size=10>; rel="next",</action/pageable?page=3&size=10>; rel="prev",</action/pageable?page=31&size=10>; rel="last",</action/pageable?page=0&size=10>; rel="first"

Total:

311

These paging data can finally be used to update pagination component:

Angular Material Paginator

This utility has been tested with ~100k records and works blazingly fast.

Top comments (0)