Skip to content

Add scrolling sample for JPA. #662

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.jpa.pagination;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;

/**
* @author Christoph Strobl
*/
@Data
@Entity
@Table(name = "authors")
public class Author {

@Id
private String id;
private String firstName;
private String lastName;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.jpa.pagination;

import java.util.Date;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Data;

/**
* @author Christoph Strobl
*/
@Data
@Entity
@Table(name = "books")
public class Book {

@Id
private String id;
private String title;
private String isbn10;
private Date publicationDate;

@ManyToOne
Author author;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.jpa.pagination;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Window;
import org.springframework.data.repository.ListCrudRepository;

/**
* @author Christoph Strobl
*/
public interface BookRepository extends ListCrudRepository<Book, String> {

/**
* Uses an {@literal offset} based pagination that first sorts the entries by their {@link Book#getPublicationDate() publication_date}
* and then limits the result by dropping the number of rows specified in the {@link Pageable#getOffset() offset} clause.
* To retrieve {@link Page#getTotalElements()} an additional count query is executed.
*
* @param title
* @param pageable
* @return
*/
Page<Book> findByTitleContainsOrderByPublicationDate(String title, Pageable pageable);

/**
* Uses an {@literal offset} based slicing that first sorts the entries by their {@link Book#getPublicationDate() publication_date}
* and then limits the result by dropping the number of rows specified in the {@link Pageable#getOffset() offset} clause.
*
* @param title
* @param pageable
* @return
*/
Slice<Book> findBooksByTitleContainsOrderByPublicationDate(String title, Pageable pageable);

/**
* Depending on the provided {@link ScrollPosition} either {@link org.springframework.data.domain.OffsetScrollPosition offset}
* or {@link org.springframework.data.domain.KeysetScrollPosition keyset} scrolling is possible.
* Scrolling through results requires a stable {@link org.springframework.data.domain.Sort} which is different from
* what {@link Pageable#getSort()} offers.
* The {@literal limit} is defined via the {@literal Top} keyword.
*
* @param title
* @param scrollPosition
* @return
*/
Window<Book> findTop2ByTitleContainsOrderByPublicationDate(String title, ScrollPosition scrollPosition);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.jpa.pagination;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

/**
* @author Christoph Strobl
*/
@SpringBootApplication
@EnableJpaRepositories(repositoryBaseClass = BookRepository.class)
class PagingRepoConfig {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.jpa.pagination;

import static org.assertj.core.api.AssertionsForClassTypes.*;

import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import com.github.javafaker.Faker;
import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Window;
import org.springframework.transaction.annotation.Transactional;

/**
* Show different types of paging styles using {@link Page}, {@link org.springframework.data.domain.Slice} and {@link Window}
*
* @author Christoph Strobl
*/
@SpringBootTest
@Transactional
class PaginationTests {

@Configuration
@EnableAutoConfiguration
static class Config {

}

@Autowired
BookRepository books;

@BeforeEach
void setUp() {

Faker faker = new Faker();

// create some sample data
List<Author> authorList = createAuthors(faker);
createBooks(faker, authorList);
}

/**
* Page through the results using an offset/limit approach where the server skips over the number of results
* specified via {@link Pageable#getOffset()}.
* The {@link Page} return type will run an additional {@literal count} query to read the total number of matching rows
* on each request.
*/
@Test
void pageThroughResultsWithSkipAndLimit() {

Page<Book> page;
Pageable pageRequest = PageRequest.of(0, 2);

do {

page = books.findByTitleContainsOrderByPublicationDate("the", pageRequest);
assertThat(page.getContent().size()).isGreaterThanOrEqualTo(1).isLessThanOrEqualTo(2);

pageRequest = page.nextPageable();
} while (page.hasNext());
}

/**
* Run through the results using an offset/limit approach where the server skips over the number of results specified
* via {@link Pageable#getOffset()}.
* No additional {@literal count} query to read the total number of matching rows is issued. Still {@link Slice} requests,
* but does not emit, one row more than specified via {@link Page#getSize()} to feed {@link Slice#hasNext()}
*/
@Test
void sliceThroughResultsWithSkipAndLimit() {

Slice<Book> slice;
Pageable pageRequest = PageRequest.of(0, 2);

do {

slice = books.findBooksByTitleContainsOrderByPublicationDate("the", pageRequest);
assertThat(slice.getContent().size()).isGreaterThanOrEqualTo(1).isLessThanOrEqualTo(2);

pageRequest = slice.nextPageable();
} while (slice.hasNext());
}

/**
* Scroll through the results using an offset/limit approach where the server skips over the number of results
* specified via {@link OffsetScrollPosition#getOffset()}.
* <p>
* This approach is similar to the {@link #sliceThroughResultsWithSkipAndLimit() slicing one}.
*/
@Test
void scrollThroughResultsWithSkipAndLimit() {

Window<Book> window;
ScrollPosition scrollPosition = OffsetScrollPosition.initial();

do {

window = books.findTop2ByTitleContainsOrderByPublicationDate("the", scrollPosition);
assertThat(window.getContent().size()).isGreaterThanOrEqualTo(1).isLessThanOrEqualTo(2);

scrollPosition = window.positionAt(window.getContent().size() - 1);
} while (window.hasNext());
}

/**
* Scroll through the results using an index based approach where the {@link KeysetScrollPosition#getKeys() keyset}
* keeps track of already seen values to resume scrolling by altering the where clause to only return rows after the
* values contained in the keyset.
* Set {@literal logging.level.org.hibernate.SQL=debug} to show the modified query in the log.
*/
@Test
void scrollThroughResultsWithKeyset() {

Window<Book> window;
ScrollPosition scrollPosition = KeysetScrollPosition.initial();
do {

window = books.findTop2ByTitleContainsOrderByPublicationDate("the", scrollPosition);
assertThat(window.getContent().size()).isGreaterThanOrEqualTo(1).isLessThanOrEqualTo(2);

scrollPosition = window.positionAt(window.getContent().size() - 1);
} while (window.hasNext());
}

// --> Test Data

@Autowired
EntityManager em;

private List<Author> createAuthors(Faker faker) {

List<Author> authors = IntStream.range(0, 10).mapToObj(id -> {

Author author = new Author();
author.setId("author-%s".formatted(id));
author.setFirstName(faker.name().firstName());
author.setLastName(faker.name().lastName());

em.persist(author);
return author;
}).collect(Collectors.toList());
return authors;
}

private List<Book> createBooks(Faker faker, List<Author> authors) {

Random rand = new Random();
return IntStream.range(0, 100)
.mapToObj(id -> {

Book book = new Book();
book.setId("book-%03d".formatted(id));
book.setTitle(faker.book().title());
book.setIsbn10(UUID.randomUUID().toString().substring(0, 10));
book.setPublicationDate(faker.date().past(5000, TimeUnit.DAYS));
book.setAuthor(authors.get(rand.nextInt(authors.size())));

em.persist(book);
return book;
}).collect(Collectors.toList());
}
}
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>1.0.1</version>
<scope>test</scope>
</dependency>

</dependencies>

Expand Down