Olá, meu nome é Dmitry Karlovsky e eu ... não gosto de ler livros, porque enquanto você vira a página, você começa uma história fascinante. E vale a pena hesitar um pouco quando você esquecer o que terminou a última frase da página anterior e precisar voltar para lê-la novamente. E se não é tão assustador com livros físicos, com a emissão de um servidor de descanso tudo fica muito mais triste - afinal, agora existem alguns dados na página e, depois de um segundo, é completamente diferente. Vamos pensar em como aconteceu, quem é o culpado e o mais importante - o que fazer.

O problema
Portanto, precisamos emitir todas as mensagens para a consulta "paginação", começando pelas mais recentes (as últimas foram alteradas a partir do topo ) ou em uma ordem complicada. Está tudo bem, desde que tenhamos menos de cem dessas mensagens - basta fazer uma seleção no banco de dados e retornar os dados:
Pedido do cliente:
GET /message/text=/
Solicitação do banco de dados:
SELECT FROM Message WHERE text LICENE "" ORDER BY changed DESC
Esquema de resposta JSON para o cliente:
Array<{ id : number , text : string }>
Mas o número de mensagens está crescendo e temos os seguintes problemas:
- As consultas ao banco de dados estão ficando mais lentas à medida que mais dados precisam ser coletados.
- O envio de dados pela rede está demorando cada vez mais.
- A renderização desses dados no cliente está ficando cada vez mais longa.
A partir de um certo limite, os atrasos se tornam tão significativos que tornam impossível o uso do nosso site. Se, é claro, ele ainda não havia se deitado, cansado de um grande número de pedidos pesados paralelos.
A solução mais simples, que talvez venha à mente primeiro, e você pode encontrá-la agora em qualquer torradeira - para fornecer dados não todos em massa, mas divididos em páginas. Tudo o que precisamos fazer é lançar um parâmetro adicional do cliente na solicitação do banco de dados:
GET /message/text=/page=5/
SELECT FROM Message WHERE text LICENE "" ORDER BY changed DESC SKIP 5 * 10 LIMIT 10
SELECT count(*) FROM Message WHERE text LICENE ""
{ pageItems : Array<{ id : number , text : string }> totalCount : number }
Bem, sim, ainda tivemos que recontar todas as mensagens para que o cliente pudesse desenhar uma lista de páginas ou calcular a altura do pergaminho virtual, mas pelo menos não precisamos obter todas essas 100500 mensagens do banco de dados.
E tudo ficaria bem se tivéssemos algum tipo de fórum não popular por muito tempo e não mais tópicos relevantes. Mas eles escrevem e escrevem para nós, escrevem e escrevem, e enquanto o usuário lê a quinta página, a lista de mensagens muda além do reconhecimento: novas são adicionadas e antigas são excluídas. Assim, temos dois tipos de problemas do ponto de vista do usuário:
- Na página seguinte, podem aparecer novamente mensagens que já estavam na anterior.
- O usuário não verá nenhuma mensagem, pois conseguiu passar da página 6 para a 5 exatamente entre a transição do usuário da 5 para a 6.
Além disso, ainda temos problemas de desempenho. Cada transição para a próxima página leva ao fato de que precisamos fazer até duas consultas de pesquisa no banco de dados com um número crescente de elementos ignorados das páginas anteriores.
Sim, e uma implementação competente no lado do cliente não é tão simples - você sempre deve estar preparado para o fato de que qualquer resposta do servidor pode retornar um novo número total de mensagens, o que significa que precisaremos redesenhar o paginador e redirecionar para outra página se a atual de repente está vazio. E é claro que você não pode cair no caso de duplicatas.
Além disso, às vezes o cliente precisa atualizar os resultados da pesquisa, mas a carga ainda receberá dados que já podem ter de solicitações anteriores.
Como você pode ver, a paginação tem muitos problemas. Realmente não há solução melhor?
Solução
Primeiro, vamos prestar atenção que, ao trabalhar com o banco de dados, existem 2 operações que são essencialmente diferentes:
- Pesquisar. Operação relativamente pesada de localização de ponteiros para dados para alguma consulta.
- Amostragem. Uma operação relativamente simples de realmente obter dados.
Seria ideal:
- Uma vez procure e em algum lugar para lembrar seus resultados na forma de um instantâneo em um determinado momento.
- Selecione rapidamente os dados em pequenas porções, conforme necessário.
Onde armazenar instantâneos? existem 2 opções:
- No servidor Mas então o entupimos com um monte de lixo com resultados de pesquisa que precisam ser limpos com o tempo.
- Para o cliente. Mas você deve transferir imediatamente todo o instantâneo para o cliente.
Vamos estimar o tamanho do instantâneo, que é apenas uma lista de identificadores. É duvidoso que o usuário tenha paciência para rolar pelo menos 100 páginas sem usar filtragem e classificação. Digamos que temos 20 elementos por página. Cada identificador não ocupará mais que 10 bytes na representação json. Multiplique e obtenha não mais que 20kb. E provavelmente muito menos. Seria razoável definir um limite rígido para o tamanho da saída em, digamos, 1000 elementos.
GET /message/text=/
SELECT id FROM Message WHERE text LICENE "" ORDER BY changed DESC LIMIT 1000
Array<number>
Agora, o cliente pode desenhar pelo menos um paginador, pelo menos um pergaminho virtual, solicitando dados apenas para identificadores de seu interesse.
GET /message=49,48,47,46,45,42,41,40,39,37/
SELECT FROM Message WHERE id IN [49,48,47,46,45,42,41,40,39,37]
Array< { id : number , text : string } | { id : number , error : string } >
O que finalmente obtemos:
- API normalizada: pesquise separadamente, selecione os dados separadamente.
- Minimize o número de consultas de pesquisa.
- Você não pode solicitar dados que já foram baixados ou atualizá-los em segundo plano.
- Código relativamente simples e universal no lado do cliente.
Das deficiências, podemos observar apenas:
- Para mostrar algo que o usuário precisa fazer pelo menos 2 solicitações consecutivas.
- É necessário lidar com o caso quando o identificador estiver e os dados nele não estiverem mais disponíveis.