1

I am doing the learning on Broadcom's Spring Academy to be up to date with the latest best practices. I bumped Spring Boot to 4.0.0 instead of the 3.0.0 on Spring Academy. But I am having some issues with PagingAndSortingRepository interface. I can succesfully use pagination /demo?page=0&size=1 and sorting /demo?sort=amount,desc but I cannot use the combination /demo?page=0&size=1&sort=amount,desc. And I cannot figure out how to fix it with JDBC.

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '4.0.0'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
description = 'Demo project for Spring Boot'

java {
    sourceCompatibility = '17'
}

repositories {
    mavenCentral()
}

dependencies {
    // Rest API
    implementation 'org.springframework.boot:spring-boot-starter-webmvc'
    testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'

    // Database
    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
    runtimeOnly 'com.h2database:h2'

    // Security
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
    testImplementation 'org.springframework.security:spring-security-test'

    // Test
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}

test {
    testLogging {
        events "passed", "skipped", "failed" //, "standardOut", "standardError"

        showExceptions true
        exceptionFormat "full"
        showCauses true
        showStackTraces true

        // Change from false to true
        showStandardStreams = true
    }
}

DemoRepository.java

interface DemoRepository extends CrudRepository<Demo, Long>, PagingAndSortingRepository<Demo, Long> {
    boolean existsByIdAndOwner(Long id, String owner);

    Demo findByIdAndOwner(Long id, String owner);

    Page<Demo> findByOwner(String owner, Pageable pageable);
}

DemoController.java (I commented the problematic line proposed on Spring Academy and replaced it by the simpler one above.)

    @GetMapping
    private ResponseEntity<List<Demo>> readDemos(Pageable pageable, Principal principal) {
        Page<Demo> page = demoRepository.findByOwner(principal.getName(),
                PageRequest.of(
                        pageable.getPageNumber(),
                        pageable.getPageSize(),
                        pageable.getSort()
                        // pageable.getSortOr(Sort.by(Sort.Direction.ASC, "amount"))
                ));

        return ResponseEntity.ok(page.getContent());
    }

DemoApplicationTests.java (If I use the previous code, it works fine. But if I use the commented out line, it breaks.)

 @Test
    void readDemos_sorting() {
        EntityExchangeResult<String> result = client.get()
                .uri("/demo?page=0&size=1&sort=amount,desc")
                .header(HttpHeaders.AUTHORIZATION, authHeader)
                .exchange()
                .expectStatus()
                .isOk()
                .expectHeader()
                .contentTypeCompatibleWith(MediaType.APPLICATION_JSON)
                .expectBody(String.class)
                .returnResult();

        String responseBody = result.getResponseBody();

        DocumentContext documentContext = JsonPath.parse(responseBody);
        JSONArray read = documentContext.read("$[*]");
        assertThat(read.size()).isEqualTo(3);

        double amount = documentContext.read("$[0].amount");
        assertThat(amount).isEqualTo(150.00);
    }

Error when using the commented out line:

org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "DEMO.AMOUNT" must be in the GROUP BY list; SQL statement:
SELECT COUNT(*) FROM "DEMO" WHERE "DEMO"."OWNER" = ? ORDER BY "DEMO"."AMOUNT" ASC [90016-240]

Also, other tests testing the sorting logic fail, whereas they work when I use pageable.getSort().

One one hand, I can use pageable.getSort() to support a default call without any sorting params; it also supports sort params; but I cannot combine those sort params with the pagination params or it fails.

On the other hand, if I use pageable.getSortOr(Sort.by(Sort.Direction.ASC, "amount")), all sorting fails, and if I use pagination params without sorting params, I get that SQL error.

View the full code on GitHub

5
  • (1) Original: /demo?page=0&size=1sort=amount,desc, Correction: The correct URL is /demo?page=0&size=1&sort=amount,desc. Reason: We need to add the & before the sort parameter. (2) Question: Why does the assertion expect a size of 3? Given the parameters page=0&size=1, it should only return 1 record. Commented yesterday
  • Yeah, corrected. But that was just a typo copypaste. The issue still occurs, that typo was not the problem ;-) Commented yesterday
  • I added the full source code at github.com/jlanssie/spring-boot Commented yesterday
  • Have you specified that you can only use Spring Data JDBC? Are you open to using Spring Data JPA? Commented 16 hours ago
  • I didn’t specify but indeed, I am trying with JDBC. Nothing particular against JPA. I’ll try that out! Commented 16 hours ago

3 Answers 3

1

The issue is because of the count query which is generated for the pageable object by Spring.

See the javadoc for countQuery

String countQuery

Defines a special count query that shall be used for pagination queries to look up the total number of elements for a page. If none is configured we will derive the count query from the original query or countProjection() query if any.

In your case, it is using the original query (including the sort) to perform the count which results in an invalid query.

My advice would be to add a custom count query which excludes the sort and everything should work as expected.

    @Query(
            value = "FROM Demo d WHERE d.owner = :owner",
            countQuery = "SELECT count(1) FROM Demo d WHERE d.owner = :owner"
    )
    Page<Demo> findByOwner(@Param("owner") String owner, Pageable pageable);
Sign up to request clarification or add additional context in comments.

2 Comments

countQuery has an erro: Cannot find @interface method 'countQuery(). Also, what if I want to sort my returned items, and slice it in pages because the list of results is too long? That is not really a solution for me. The idea is to support a combination of pagination and sorting.
I added the full source code at github.com/jlanssie/spring-boot
1

In Spring Boot 4.0 with Spring Data JDBC 2024.0, Pageable.getSortOr(...) started behaving differently than in Spring Boot 3.2. The sort provided by getSortOr() is now applied to both the main query and the COUNT query used for pagination.

Because of this, a query like:

SELECT COUNT(*) 
FROM demo 
WHERE owner = ? 
ORDER BY amount ASC;

is generated when no sort is provided by the client. H2 does not allow ORDER BY in a count query unless the column is also in a GROUP BY clause, which leads to the error:

Column "DEMO.AMOUNT" must be in the GROUP BY list

Using pageable.getSort() works fine because it only returns user-provided sorting and leaves the COUNT query unsorted, so H2 doesn’t complain.

The problem with combining pagination and sorting in your controller comes from recreating the Pageable:

PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), pageable.getSort())

Spring MVC already constructs a correct Pageable object. Rebuilding it can interfere with default values and fallback sorting, which causes issues in Spring Boot 4.

The simplest solution is to pass the pageable directly to the repository:

@GetMapping
public ResponseEntity<List<Demo>> readDemos(Pageable pageable, Principal principal) {
    Page<Demo> page = demoRepository.findByOwner(principal.getName(), pageable);
    return ResponseEntity.ok(page.getContent());
}

Also, in your test, there’s a small typo in the URL:

/demo?page=0&size=1sort=amount,desc

It should be:

/demo?page=0&size=1&sort=amount,desc

Without the &, Spring treats it as a single parameter and the sort is ignored.

With this approach, pagination and sorting work together correctly, and the H2 error is avoided.

2 Comments

I tried your suggestion ... java @GetMapping public ResponseEntity<List<Demo>> readDemos(Pageable pageable, Principal principal) { Page<Demo> page = demoRepository.findByOwner(principal.getName(), pageable); return ResponseEntity.ok(page.getContent()); } ... but my request for /demo?page=0&size=1&sort=amount,desc still fails with the H2 SQL error. Also, it does not address any solution for the commented line, where there is a fallback (default) for the sorting.
I added the full source code at github.com/jlanssie/spring-boot
0

It's not clear what error you're encountering. A clear error message from your logs would help. In the meantime, I tried this quickly and it works:

is used page PageableDefault (it makes code little clear)

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;

@Data
@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    private String name;
    private String ownerId;
    private Double price;
}
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.CrudRepository;

public interface ProductRepository extends CrudRepository<Product, Long> {
    Page<Product> findByOwnerId(String owner, Pageable pageable);

    String id(Long id);
}
@RestController
@AllArgsConstructor
public class ProductController {

    private final ProductRepository productRepository;

    @GetMapping("/product")
    private ResponseEntity<Page<Product>> readDemos(@RequestParam("ownerId") String ownerId,
                                                    @PageableDefault(
                                                            page = 0,
                                                            size = 5, sort = {"price"},
                                                            direction = Sort.Direction.DESC) Pageable pageable) {
        Page<Product> page = productRepository.findByOwnerId(ownerId, pageable);

        return ResponseEntity.ok(page);
    }
}
@SpringBootTest
@AutoConfigureMockMvc
@Import(TestcontainersConfiguration.class)
class ProductControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ProductRepository productRepository;

    @BeforeEach
    void setUp() {
        productRepository.deleteAll();
        // Add 10 products for the same ownerId with ascending price
        IntStream.rangeClosed(1, 10).forEach(i -> {
            Product p = new Product();
            p.setName("Product " + i);
            p.setOwnerId("owner1");
            p.setPrice((double) (i * 10));
            productRepository.save(p);
        });
    }

    @Test
    void testPaginationAndSortingByPriceDesc() throws Exception {
        mockMvc.perform(get("/product")
                        .param("ownerId", "owner1")
                        .param("page", "0")
                        .param("size", "3")
                        .param("sort", "price,desc")
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.content.length()").value(3))
                .andExpect(jsonPath("$.content[0].price").value(100.0))
                .andExpect(jsonPath("$.content[1].price").value(90.0));
    }
}

4 Comments

I tested but it was worse than before because now it is not only failling the combination but also call with only pagination or only sorting. Did you test with Spring Boot 4?
I added the full source code at github.com/jlanssie/spring-boot
i will check your code. here is link of code that i share with tests... github.com/jinternals/demo-spring-boot-4
Oh you’re using JPA spring-boot-starter-data-jpa. That seems to work indeed. Im trying this on JDBC.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.