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.
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
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.