Add scrolling sample for JPA.

Closes #662
This commit is contained in:
Christoph Strobl
2023-05-09 11:10:28 +02:00
committed by Mark Paluch
parent 767b90911b
commit 1b8872abce
6 changed files with 367 additions and 0 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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 {
}

View File

@@ -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());
}
}

View File

@@ -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>