orderBy() {
+ return orderBy(missingProperties -> {
+ });
+ }
+
+ /**
+ * Creates an {@link OrderByNameDeclaration} consumer based on the
+ * {@link Pageable}'s sort information using the provided {@link PropertyMetamodelResolver}.
+ *
+ * If the {@link Pageable} is unsorted, or if no {@link PropertyMetamodel} can be resolved
+ * for a given sort property, a default ordering is applied.
+ *
+ * The provided {@code handleMissingProperties} consumer is called with a list of
+ * property names that could not be resolved. This can be used to throw an exception,
+ * log a warning, or handle the situation in a custom way.
+ *
+ * @param handleMissingProperties a callback that handles property names which could not be resolved
+ * @return a consumer that configures ordering based on the resolved {@link PropertyMetamodel} instances
+ */
+ public Consumer orderBy(
+ Consumer> handleMissingProperties) {
+ if (pageable.getSort().isUnsorted()) {
+ return sortConfig.defaultOrder();
+ }
+ final var orderSpecifiers = new ArrayList>();
+ final var missingProperties = new ArrayList();
+ for (final var order : pageable.getSort()) {
+ sortConfig
+ .propertyMetamodelResolver()
+ .resolve(order.getProperty())
+ .ifPresentOrElse(
+ propertyMetamodel -> orderSpecifiers.add(
+ switch (order.getDirection()) {
+ case ASC -> c -> c.asc(propertyMetamodel);
+ case DESC -> c -> c.desc(propertyMetamodel);
+ }),
+ () -> missingProperties.add(order.getProperty()));
+ }
+ if (!missingProperties.isEmpty()) {
+ handleMissingProperties.accept(missingProperties);
+ }
+ if (orderSpecifiers.isEmpty()) {
+ return sortConfig.defaultOrder();
+ }
+ return c -> orderSpecifiers.forEach(orderSpecifier -> orderSpecifier.accept(c));
+ }
+}
diff --git a/doma-spring-boot-core/src/test/java/org/seasar/doma/boot/UnifiedQueryPageableTest.java b/doma-spring-boot-core/src/test/java/org/seasar/doma/boot/UnifiedQueryPageableTest.java
new file mode 100644
index 00000000..dacaef06
--- /dev/null
+++ b/doma-spring-boot-core/src/test/java/org/seasar/doma/boot/UnifiedQueryPageableTest.java
@@ -0,0 +1,190 @@
+package org.seasar.doma.boot;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.CoreMatchers.sameInstance;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.seasar.doma.Entity;
+import org.seasar.doma.Id;
+import org.seasar.doma.Metamodel;
+import org.seasar.doma.jdbc.criteria.declaration.OrderByNameDeclaration;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+
+public class UnifiedQueryPageableTest {
+ @ParameterizedTest
+ @CsvSource(value = {
+ "0 | 10 | 0 | 10",
+ "2 | 10 | 20 | 10",
+ "2 | 5 | 10 | 5",
+ }, delimiter = '|')
+ public void testOffsetAndLimit(
+ int pageNumber, int pageSize, int expectedOffset, int expectedLimit) {
+ Pageable pageable = PageRequest.of(pageNumber, pageSize);
+ UnifiedQueryPageable p = UnifiedQueryPageable.of(pageable, c -> Optional.empty());
+
+ Integer offset = p.offset();
+ Integer limit = p.limit();
+
+ assertThat(offset, is(expectedOffset));
+ assertThat(limit, is(expectedLimit));
+ }
+
+ @Test
+ public void testOffsetAndLimitWhenUnpaged() {
+ Pageable pageable = Pageable.unpaged();
+ UnifiedQueryPageable p = UnifiedQueryPageable.of(pageable, c -> Optional.empty());
+
+ Integer offset = p.offset();
+ Integer limit = p.limit();
+
+ assertThat(offset, nullValue());
+ assertThat(limit, nullValue());
+ }
+
+ @Test
+ public void testOrderBy() {
+ Pageable pageable = PageRequest.of(0, 10, Sort.by("name").ascending());
+ Person_ entity = new Person_();
+ UnifiedQueryPageable p = UnifiedQueryPageable.of(
+ pageable,
+ propertyName -> switch (propertyName) {
+ case "name" -> Optional.of(entity.name);
+ default -> Optional.empty();
+ });
+
+ Consumer consumer = p.orderBy();
+
+ OrderByNameDeclaration orderByNameDeclaration = mock(OrderByNameDeclaration.class);
+ consumer.accept(orderByNameDeclaration);
+ verify(orderByNameDeclaration, times(1)).asc(entity.name);
+ }
+
+ @Test
+ public void testOrderBy2() {
+ Pageable pageable = PageRequest.of(0, 10,
+ Sort.by("name").descending().and(Sort.by("age").ascending()));
+ Person_ entity = new Person_();
+ UnifiedQueryPageable p = UnifiedQueryPageable.of(
+ pageable,
+ propertyName -> switch (propertyName) {
+ case "name" -> Optional.of(entity.name);
+ case "age" -> Optional.of(entity.age);
+ default -> Optional.empty();
+ });
+
+ Consumer consumer = p.orderBy();
+
+ OrderByNameDeclaration orderByNameDeclaration = mock(OrderByNameDeclaration.class);
+ consumer.accept(orderByNameDeclaration);
+ var sortOrderVerifier = inOrder(orderByNameDeclaration);
+ sortOrderVerifier.verify(orderByNameDeclaration, times(1)).desc(entity.name);
+ sortOrderVerifier.verify(orderByNameDeclaration, times(1)).asc(entity.age);
+ }
+
+ @Test
+ public void testOrderByWhenNonSort() {
+ Pageable pageable = PageRequest.of(0, 10);
+ UnifiedQueryPageable p = UnifiedQueryPageable.of(
+ pageable,
+ propertyName -> Optional.empty());
+
+ Consumer consumer = p.orderBy();
+
+ OrderByNameDeclaration orderByNameDeclaration = mock(OrderByNameDeclaration.class);
+ consumer.accept(orderByNameDeclaration);
+ verifyNoMoreInteractions(orderByNameDeclaration);
+ }
+
+ @Test
+ public void testOrderByWhenNonSortAndSetDefault() {
+ Pageable pageable = PageRequest.of(0, 10);
+ Person_ entity = new Person_();
+ Consumer defaultOrder = c -> c.asc(entity.id);
+ UnifiedQueryPageable p = UnifiedQueryPageable.of(
+ pageable,
+ propertyName -> Optional.empty(),
+ defaultOrder);
+
+ Consumer consumer = p.orderBy();
+
+ assertThat(consumer, sameInstance(defaultOrder));
+ }
+
+ @Test
+ public void testOrderBySingleEntity() {
+ Pageable pageable = PageRequest.of(0, 10,
+ Sort.by("name").descending().and(Sort.by("age").ascending()));
+ Person_ entity = new Person_();
+ UnifiedQueryPageable p = UnifiedQueryPageable.from(pageable, entity);
+
+ Consumer consumer = p.orderBy();
+
+ OrderByNameDeclaration orderByNameDeclaration = mock(OrderByNameDeclaration.class);
+ consumer.accept(orderByNameDeclaration);
+ var sortOrderVerifier = inOrder(orderByNameDeclaration);
+ sortOrderVerifier.verify(orderByNameDeclaration, times(1)).desc(entity.name);
+ sortOrderVerifier.verify(orderByNameDeclaration, times(1)).asc(entity.age);
+ }
+
+ @Test
+ public void testOrderByWhenMissingProperties() {
+ Pageable pageable = PageRequest.of(0, 10,
+ Sort.by("dog").and(Sort.by("name")).and(Sort.by("cat")));
+ Person_ entity = new Person_();
+ UnifiedQueryPageable p = UnifiedQueryPageable.from(pageable, entity);
+
+ Consumer consumer = p.orderBy();
+
+ OrderByNameDeclaration orderByNameDeclaration = mock(OrderByNameDeclaration.class);
+ consumer.accept(orderByNameDeclaration);
+ verify(orderByNameDeclaration, times(1)).asc(entity.name);
+ }
+
+ @Test
+ public void testOrderByWhenMissingAllProperties() {
+ Pageable pageable = PageRequest.of(0, 10,
+ Sort.by("dog").and(Sort.by("cat")));
+ Person_ entity = new Person_();
+ Consumer defaultOrder = c -> c.desc(entity.age);
+ UnifiedQueryPageable p = UnifiedQueryPageable.from(pageable, entity, defaultOrder);
+
+ Consumer consumer = p.orderBy();
+
+ assertThat(consumer, sameInstance(defaultOrder));
+ }
+
+ @Test
+ public void testOrderByWhenMissingPropertiesHandle() {
+ Pageable pageable = PageRequest.of(0, 10,
+ Sort.by("dog").and(Sort.by("name")).and(Sort.by("cat")));
+ Person_ entity = new Person_();
+ UnifiedQueryPageable p = UnifiedQueryPageable.from(pageable, entity);
+
+ assertThatThrownBy(() -> p.orderBy(missingProperties -> {
+ throw new IllegalArgumentException(
+ missingProperties.stream().collect(Collectors.joining(",")));
+ }))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("dog,cat");
+ }
+}
+
+@Entity(metamodel = @Metamodel)
+record Person(@Id String id, String name, Integer age) {
+}
\ No newline at end of file