Skip to content

Commit 1b8872a

Browse files
christophstroblmp911de
authored andcommitted
Add scrolling sample for JPA.
Closes #662
1 parent 767b909 commit 1b8872a

File tree

6 files changed

+367
-0
lines changed

6 files changed

+367
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.springdata.jpa.pagination;
17+
18+
import jakarta.persistence.Entity;
19+
import jakarta.persistence.Id;
20+
import jakarta.persistence.Table;
21+
import lombok.Data;
22+
23+
/**
24+
* @author Christoph Strobl
25+
*/
26+
@Data
27+
@Entity
28+
@Table(name = "authors")
29+
public class Author {
30+
31+
@Id
32+
private String id;
33+
private String firstName;
34+
private String lastName;
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.springdata.jpa.pagination;
17+
18+
import java.util.Date;
19+
20+
import jakarta.persistence.Entity;
21+
import jakarta.persistence.Id;
22+
import jakarta.persistence.ManyToOne;
23+
import jakarta.persistence.Table;
24+
import lombok.Data;
25+
26+
/**
27+
* @author Christoph Strobl
28+
*/
29+
@Data
30+
@Entity
31+
@Table(name = "books")
32+
public class Book {
33+
34+
@Id
35+
private String id;
36+
private String title;
37+
private String isbn10;
38+
private Date publicationDate;
39+
40+
@ManyToOne
41+
Author author;
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.springdata.jpa.pagination;
17+
18+
import org.springframework.data.domain.Page;
19+
import org.springframework.data.domain.Pageable;
20+
import org.springframework.data.domain.ScrollPosition;
21+
import org.springframework.data.domain.Slice;
22+
import org.springframework.data.domain.Window;
23+
import org.springframework.data.repository.ListCrudRepository;
24+
25+
/**
26+
* @author Christoph Strobl
27+
*/
28+
public interface BookRepository extends ListCrudRepository<Book, String> {
29+
30+
/**
31+
* Uses an {@literal offset} based pagination that first sorts the entries by their {@link Book#getPublicationDate() publication_date}
32+
* and then limits the result by dropping the number of rows specified in the {@link Pageable#getOffset() offset} clause.
33+
* To retrieve {@link Page#getTotalElements()} an additional count query is executed.
34+
*
35+
* @param title
36+
* @param pageable
37+
* @return
38+
*/
39+
Page<Book> findByTitleContainsOrderByPublicationDate(String title, Pageable pageable);
40+
41+
/**
42+
* Uses an {@literal offset} based slicing that first sorts the entries by their {@link Book#getPublicationDate() publication_date}
43+
* and then limits the result by dropping the number of rows specified in the {@link Pageable#getOffset() offset} clause.
44+
*
45+
* @param title
46+
* @param pageable
47+
* @return
48+
*/
49+
Slice<Book> findBooksByTitleContainsOrderByPublicationDate(String title, Pageable pageable);
50+
51+
/**
52+
* Depending on the provided {@link ScrollPosition} either {@link org.springframework.data.domain.OffsetScrollPosition offset}
53+
* or {@link org.springframework.data.domain.KeysetScrollPosition keyset} scrolling is possible.
54+
* Scrolling through results requires a stable {@link org.springframework.data.domain.Sort} which is different from
55+
* what {@link Pageable#getSort()} offers.
56+
* The {@literal limit} is defined via the {@literal Top} keyword.
57+
*
58+
* @param title
59+
* @param scrollPosition
60+
* @return
61+
*/
62+
Window<Book> findTop2ByTitleContainsOrderByPublicationDate(String title, ScrollPosition scrollPosition);
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.springdata.jpa.pagination;
17+
18+
import org.springframework.boot.autoconfigure.SpringBootApplication;
19+
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
20+
21+
/**
22+
* @author Christoph Strobl
23+
*/
24+
@SpringBootApplication
25+
@EnableJpaRepositories(repositoryBaseClass = BookRepository.class)
26+
class PagingRepoConfig {
27+
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.springdata.jpa.pagination;
17+
18+
import static org.assertj.core.api.AssertionsForClassTypes.*;
19+
20+
import java.util.List;
21+
import java.util.Random;
22+
import java.util.UUID;
23+
import java.util.concurrent.TimeUnit;
24+
import java.util.stream.Collectors;
25+
import java.util.stream.IntStream;
26+
27+
import com.github.javafaker.Faker;
28+
import jakarta.persistence.EntityManager;
29+
import org.junit.jupiter.api.BeforeEach;
30+
import org.junit.jupiter.api.Test;
31+
import org.springframework.beans.factory.annotation.Autowired;
32+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
33+
import org.springframework.boot.test.context.SpringBootTest;
34+
import org.springframework.context.annotation.Configuration;
35+
import org.springframework.data.domain.KeysetScrollPosition;
36+
import org.springframework.data.domain.OffsetScrollPosition;
37+
import org.springframework.data.domain.Page;
38+
import org.springframework.data.domain.PageRequest;
39+
import org.springframework.data.domain.Pageable;
40+
import org.springframework.data.domain.ScrollPosition;
41+
import org.springframework.data.domain.Slice;
42+
import org.springframework.data.domain.Window;
43+
import org.springframework.transaction.annotation.Transactional;
44+
45+
/**
46+
* Show different types of paging styles using {@link Page}, {@link org.springframework.data.domain.Slice} and {@link Window}
47+
*
48+
* @author Christoph Strobl
49+
*/
50+
@SpringBootTest
51+
@Transactional
52+
class PaginationTests {
53+
54+
@Configuration
55+
@EnableAutoConfiguration
56+
static class Config {
57+
58+
}
59+
60+
@Autowired
61+
BookRepository books;
62+
63+
@BeforeEach
64+
void setUp() {
65+
66+
Faker faker = new Faker();
67+
68+
// create some sample data
69+
List<Author> authorList = createAuthors(faker);
70+
createBooks(faker, authorList);
71+
}
72+
73+
/**
74+
* Page through the results using an offset/limit approach where the server skips over the number of results
75+
* specified via {@link Pageable#getOffset()}.
76+
* The {@link Page} return type will run an additional {@literal count} query to read the total number of matching rows
77+
* on each request.
78+
*/
79+
@Test
80+
void pageThroughResultsWithSkipAndLimit() {
81+
82+
Page<Book> page;
83+
Pageable pageRequest = PageRequest.of(0, 2);
84+
85+
do {
86+
87+
page = books.findByTitleContainsOrderByPublicationDate("the", pageRequest);
88+
assertThat(page.getContent().size()).isGreaterThanOrEqualTo(1).isLessThanOrEqualTo(2);
89+
90+
pageRequest = page.nextPageable();
91+
} while (page.hasNext());
92+
}
93+
94+
/**
95+
* Run through the results using an offset/limit approach where the server skips over the number of results specified
96+
* via {@link Pageable#getOffset()}.
97+
* No additional {@literal count} query to read the total number of matching rows is issued. Still {@link Slice} requests,
98+
* but does not emit, one row more than specified via {@link Page#getSize()} to feed {@link Slice#hasNext()}
99+
*/
100+
@Test
101+
void sliceThroughResultsWithSkipAndLimit() {
102+
103+
Slice<Book> slice;
104+
Pageable pageRequest = PageRequest.of(0, 2);
105+
106+
do {
107+
108+
slice = books.findBooksByTitleContainsOrderByPublicationDate("the", pageRequest);
109+
assertThat(slice.getContent().size()).isGreaterThanOrEqualTo(1).isLessThanOrEqualTo(2);
110+
111+
pageRequest = slice.nextPageable();
112+
} while (slice.hasNext());
113+
}
114+
115+
/**
116+
* Scroll through the results using an offset/limit approach where the server skips over the number of results
117+
* specified via {@link OffsetScrollPosition#getOffset()}.
118+
* <p>
119+
* This approach is similar to the {@link #sliceThroughResultsWithSkipAndLimit() slicing one}.
120+
*/
121+
@Test
122+
void scrollThroughResultsWithSkipAndLimit() {
123+
124+
Window<Book> window;
125+
ScrollPosition scrollPosition = OffsetScrollPosition.initial();
126+
127+
do {
128+
129+
window = books.findTop2ByTitleContainsOrderByPublicationDate("the", scrollPosition);
130+
assertThat(window.getContent().size()).isGreaterThanOrEqualTo(1).isLessThanOrEqualTo(2);
131+
132+
scrollPosition = window.positionAt(window.getContent().size() - 1);
133+
} while (window.hasNext());
134+
}
135+
136+
/**
137+
* Scroll through the results using an index based approach where the {@link KeysetScrollPosition#getKeys() keyset}
138+
* keeps track of already seen values to resume scrolling by altering the where clause to only return rows after the
139+
* values contained in the keyset.
140+
* Set {@literal logging.level.org.hibernate.SQL=debug} to show the modified query in the log.
141+
*/
142+
@Test
143+
void scrollThroughResultsWithKeyset() {
144+
145+
Window<Book> window;
146+
ScrollPosition scrollPosition = KeysetScrollPosition.initial();
147+
do {
148+
149+
window = books.findTop2ByTitleContainsOrderByPublicationDate("the", scrollPosition);
150+
assertThat(window.getContent().size()).isGreaterThanOrEqualTo(1).isLessThanOrEqualTo(2);
151+
152+
scrollPosition = window.positionAt(window.getContent().size() - 1);
153+
} while (window.hasNext());
154+
}
155+
156+
// --> Test Data
157+
158+
@Autowired
159+
EntityManager em;
160+
161+
private List<Author> createAuthors(Faker faker) {
162+
163+
List<Author> authors = IntStream.range(0, 10).mapToObj(id -> {
164+
165+
Author author = new Author();
166+
author.setId("author-%s".formatted(id));
167+
author.setFirstName(faker.name().firstName());
168+
author.setLastName(faker.name().lastName());
169+
170+
em.persist(author);
171+
return author;
172+
}).collect(Collectors.toList());
173+
return authors;
174+
}
175+
176+
private List<Book> createBooks(Faker faker, List<Author> authors) {
177+
178+
Random rand = new Random();
179+
return IntStream.range(0, 100)
180+
.mapToObj(id -> {
181+
182+
Book book = new Book();
183+
book.setId("book-%03d".formatted(id));
184+
book.setTitle(faker.book().title());
185+
book.setIsbn10(UUID.randomUUID().toString().substring(0, 10));
186+
book.setPublicationDate(faker.date().past(5000, TimeUnit.DAYS));
187+
book.setAuthor(authors.get(rand.nextInt(authors.size())));
188+
189+
em.persist(book);
190+
return book;
191+
}).collect(Collectors.toList());
192+
}
193+
}

pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@
145145
<artifactId>junit-vintage-engine</artifactId>
146146
<scope>test</scope>
147147
</dependency>
148+
<dependency>
149+
<groupId>com.github.javafaker</groupId>
150+
<artifactId>javafaker</artifactId>
151+
<version>1.0.1</version>
152+
<scope>test</scope>
153+
</dependency>
148154

149155
</dependencies>
150156

0 commit comments

Comments
 (0)