TL;DR

O método findAll da interface PagingAndSortingRepository, faz uma query/busca adicional à sua base de dados.

Serendipidade no App do Pássaro Azul

Esses dias durante um doom scrolling no twitter, me deparei com um post interessante sobre o PagingAndSortingRepository do Spring Data, basicamente era um alerta sobre a execução de uma query adicional(um simples count) no método findAll, e, como isso dependendo do teu cenário, poderia ser um gargalo.

Post do Simon Martinelli - simas_ch

Esteja atent@ que o método findAll do PagingAndSortingRepository do Spring Data realizará uma query adicional, se você possue uma consulta lenta, ela levará o dobro do tempo!

Vendo para crer

Isso me chamou atenção e fui buscar informações. Olhei na documentação e não encontrei os detalhes específicos sobre isso. Resolvi subir um projeto que já tenho preparado para vários tipos de experimentações e rodar esse cenário

Big surprise: realmente, executa a query adicional de count

Conforme o autor do twit responde uma possível contramedida é utilizar getBy(Pageable pageable) porém acaba abrindo mão dos metadados do objeto Page.

Mise en Place

mise en place Foto de Rudy Issa na Unsplash

Para verificar esse caso, utilizei no projeto:

  • Java 11+
  • Spring Boot 3+
  • Spring Data
  • Spring Web
  • Actuator
  • Lombok (opcional)
  • IntelliJ IDEA Community Edition 2022.3.1 (opcional)
  • Mysql Employee DB Sample

O repositório já implementado pode ser clonado e/ou verificado aqui!

Interface TitlesRepository

package com.nssp.employees.data.repositories;

import com.nssp.employees.data.models.Titles;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.PagingAndSortingRepository;

import java.util.List;

public interface TitlesRepository extends PagingAndSortingRepository<Titles, Long> {
    List<Titles> findBy(Pageable pageable);
}

Classe principal e de controller

package com.nssp.employees;

import com.nssp.employees.data.models.Titles;
import com.nssp.employees.data.repositories.EmployeesRepository;
import com.nssp.employees.data.repositories.TitlesRepository;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.concurrent.TimeUnit;

@SpringBootApplication
@RestController
@RequestMapping("/")
public class EmployeesApplication {

	private EmployeesRepository repository;
	private TitlesRepository titlesRepository;

	public EmployeesApplication(final EmployeesRepository repository,
								final TitlesRepository titlesRepository) {
		this.repository = repository;
		this.titlesRepository = titlesRepository;
	}

	public static void main(String[] args) {
		SpringApplication.run(EmployeesApplication.class, args);
	}

	@GetMapping("page")
	public Page<Titles> get() {
		long startTime = System.nanoTime();
		var response = this.titlesRepository.findAll(PageRequest.of(1, 10000));
		long endTime = System.nanoTime();
		long duration = TimeUnit.SECONDS.convert( (endTime - startTime), TimeUnit.NANOSECONDS);
		System.out.println("Page time: "+duration);
		return response;
	}

	@GetMapping("list")
	public List<Titles> getList() {
		long startTime = System.nanoTime();
		var response = this.titlesRepository.findBy(PageRequest.of(1, 10000));
		long endTime = System.nanoTime();
		long duration = TimeUnit.SECONDS.convert( (endTime - startTime), TimeUnit.NANOSECONDS);
		System.out.println("List time: "+duration);
		return response;
	}
}

Console da execução(list):

Hibernate: select t1_0.emp_no,t1_0.title,t1_0.to_date,t1_0.from_date from titles t1_0 limit ?,?
List time: 209

Console da execução(findAll):

Hibernate: select t1_0.emp_no,t1_0.title,t1_0.to_date,t1_0.from_date from titles t1_0 limit ?,?
Hibernate: select count(*) from titles t1_0
Page time: 264

Com o actuator liberado temos as seguintes métricas:

http://localhost:8080/actuator/metrics/http.server.requests?tag=uri:/page

{
  "name": "http.server.requests",
  "baseUnit": "seconds",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 4
    },
    {
      "statistic": "TOTAL_TIME",
      "value": 2.840016801
    },
    {
      "statistic": "MAX",
      "value": 1.752134
    }
  ],
  "availableTags": [
    {
      "tag": "exception",
      "values": [
        "none"
      ]
    },
    {
      "tag": "method",
      "values": [
        "GET"
      ]
    },
    {
      "tag": "error",
      "values": [
        "none"
      ]
    },
    {
      "tag": "outcome",
      "values": [
        "SUCCESS"
      ]
    },
    {
      "tag": "status",
      "values": [
        "200"
      ]
    }
  ]
}

http://localhost:8080/actuator/metrics/http.server.requests?tag=uri:/list

{
  "name": "http.server.requests",
  "baseUnit": "seconds",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 3
    },
    {
      "statistic": "TOTAL_TIME",
      "value": 1.4389705
    },
    {
      "statistic": "MAX",
      "value": 0
    }
  ],
  "availableTags": [
    {
      "tag": "exception",
      "values": [
        "none"
      ]
    },
    {
      "tag": "method",
      "values": [
        "GET"
      ]
    },
    {
      "tag": "error",
      "values": [
        "none"
      ]
    },
    {
      "tag": "outcome",
      "values": [
        "SUCCESS"
      ]
    },
    {
      "tag": "status",
      "values": [
        "200"
      ]
    }
  ]
}

Temos numa pesquisa simples de 10k de registros uma diferença sensível na casa dos milisegundos

Discussão

Qual o melhor? qual o pior? Depende do teu contexto, do tradeoff que você pode fazer Pois o que você perde ao não utilizar a paginação são dados como número total de elementos, ordenação, etc.

Metadados

"pageable": {
    "sort": {
      "empty": true,
      "sorted": false,
      "unsorted": true
    },
    "offset": 100,
    "pageNumber": 1,
    "pageSize": 100,
    "paged": true,
    "unpaged": false
  },
  "totalPages": 4434,
  "totalElements": 443308,
  "last": false,
  "size": 100,
  "number": 1,
  "sort": {
    "empty": true,
    "sorted": false,
    "unsorted": true
  },
  "numberOfElements": 100,
  "first": false,
  "empty": false

Espero que tenha sido interessante essa análise
E você, vai pesar se vale ou não a pena na tua próxima codificação?

Obrigado.