From 29c86d74b318dde89206f4cbe5bf2a01168fbf46 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 18 Jul 2016 13:24:19 +0200 Subject: [PATCH] DATACMNS-884 - Move PageableExecutionSupport from JPA to Spring Data Commons. PageableExecutionSupport assumes that data queries are cheaper than count queries so we can apply some optimizations. We determine the total from the pageable and the results in which we don't hit the page size bounds (i.e. results are less than a full page without offset or results are greater 0 and less than a full page with offset). In all other cases we issue an additional count query. --- .../support/PageableExecutionSupport.java | 79 +++++++++++ .../PageableExecutionSupportUnitTests.java | 129 ++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 src/main/java/org/springframework/data/repository/support/PageableExecutionSupport.java create mode 100644 src/test/java/org/springframework/data/repository/support/PageableExecutionSupportUnitTests.java diff --git a/src/main/java/org/springframework/data/repository/support/PageableExecutionSupport.java b/src/main/java/org/springframework/data/repository/support/PageableExecutionSupport.java new file mode 100644 index 000000000..1f070cde2 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/support/PageableExecutionSupport.java @@ -0,0 +1,79 @@ +/* + * Copyright 2016 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 + * + * http://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 org.springframework.data.repository.support; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.util.Assert; + +/** + * Support for query execution using {@link Pageable}. Using {@link PageableExecutionSupport} assumes that data queries + * are cheaper than {@code COUNT} queries and so some cases can take advantage of optimizations. + * + * @author Mark Paluch + * @since 1.13 + */ +public abstract class PageableExecutionSupport { + + private PageableExecutionSupport() {} + + /** + * Constructs a {@link Page} based on the given {@code content}, {@link Pageable} and {@link TotalSupplier} applying + * optimizations. The construction of {@link Page} omits a count query if the total can be determined based on the + * result size and {@link Pageable}. + * + * @param content must not be {@literal null}. + * @param pageable can be {@literal null}. + * @param totalSupplier must not be {@literal null}. + * @return the {@link Page}. + */ + public static Page getPage(List content, Pageable pageable, TotalSupplier totalSupplier) { + + Assert.notNull(content, "Content must not be null!"); + Assert.notNull(totalSupplier, "TotalSupplier must not be null!"); + + if (pageable == null || pageable.getOffset() == 0) { + + if (pageable == null || pageable.getPageSize() > content.size()) { + return new PageImpl(content, pageable, content.size()); + } + + return new PageImpl(content, pageable, totalSupplier.get()); + } + + if (content.size() != 0 && pageable.getPageSize() > content.size()) { + return new PageImpl(content, pageable, pageable.getOffset() + content.size()); + } + + return new PageImpl(content, pageable, totalSupplier.get()); + } + + /** + * Supplies the total count for a particular query. Can be replaced with a Java 8 Supplier when upgrading to Java 8. + * + * @author Mark Paluch + */ + public interface TotalSupplier { + + /** + * @return the total count for a particular query. + */ + long get(); + } +} diff --git a/src/test/java/org/springframework/data/repository/support/PageableExecutionSupportUnitTests.java b/src/test/java/org/springframework/data/repository/support/PageableExecutionSupportUnitTests.java new file mode 100644 index 000000000..c7a481d9e --- /dev/null +++ b/src/test/java/org/springframework/data/repository/support/PageableExecutionSupportUnitTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2016 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 + * + * http://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 org.springframework.data.repository.support; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.repository.support.PageableExecutionSupport.TotalSupplier; + +/** + * Unit tests for {@link PageableExecutionSupport}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class PageableExecutionSupportUnitTests { + + @Mock TotalSupplier totalSupplierMock; + + /** + * @see DATAMCNS-884 + */ + @Test + public void firstPageRequestIsLessThanOneFullPageDoesNotRequireTotal() { + + Page page = PageableExecutionSupport.getPage(Arrays.asList(1, 2, 3), new PageRequest(0, 10), + totalSupplierMock); + + assertThat(page, hasItems(1, 2, 3)); + assertThat(page.getTotalElements(), is(3L)); + verifyZeroInteractions(totalSupplierMock); + } + + /** + * @see DATAMCNS-884 + */ + @Test + public void noPageableRequesDoesNotRequireTotal() { + + Page page = PageableExecutionSupport.getPage(Arrays.asList(1, 2, 3), null, totalSupplierMock); + + assertThat(page, hasItems(1, 2, 3)); + assertThat(page.getTotalElements(), is(3L)); + verifyZeroInteractions(totalSupplierMock); + } + + /** + * @see DATAMCNS-884 + */ + @Test + public void subsequentPageRequestIsLessThanOneFullPageDoesNotRequireTotal() { + + Page page = PageableExecutionSupport.getPage(Arrays.asList(1, 2, 3), new PageRequest(5, 10), + totalSupplierMock); + + assertThat(page, hasItems(1, 2, 3)); + assertThat(page.getTotalElements(), is(53L)); + verifyZeroInteractions(totalSupplierMock); + } + + /** + * @see DATAMCNS-884 + */ + @Test + public void firstPageRequestHitsUpperBoundRequiresTotal() { + + when(totalSupplierMock.get()).thenReturn(4L); + + Page page = PageableExecutionSupport.getPage(Arrays.asList(1, 2, 3), new PageRequest(0, 3), + totalSupplierMock); + + assertThat(page, hasItems(1, 2, 3)); + assertThat(page.getTotalElements(), is(4L)); + verify(totalSupplierMock).get(); + } + + /** + * @see DATAMCNS-884 + */ + @Test + public void subsequentPageRequestHitsUpperBoundRequiresTotal() { + + when(totalSupplierMock.get()).thenReturn(7L); + + Page page = PageableExecutionSupport.getPage(Arrays.asList(1, 2, 3), new PageRequest(1, 3), + totalSupplierMock); + + assertThat(page, hasItems(1, 2, 3)); + assertThat(page.getTotalElements(), is(7L)); + verify(totalSupplierMock).get(); + } + + /** + * @see DATAMCNS-884 + */ + @Test + public void subsequentPageRequestWithoutResultRequiresRequireTotal() { + + when(totalSupplierMock.get()).thenReturn(7L); + Page page = PageableExecutionSupport.getPage(Collections. emptyList(), new PageRequest(5, 10), + totalSupplierMock); + + assertThat(page.getTotalElements(), is(7L)); + verify(totalSupplierMock).get(); + } +}