Qual é o melhor método RESTful para retornar o número total de itens em um objeto?


139

Estou desenvolvendo um serviço de API REST para um grande site de rede social em que estou envolvido. Até agora, está funcionando muito bem. Eu posso emitir GET, POST, PUTe DELETEsolicitações para URLs objeto e afetar meus dados. No entanto, esses dados são paginados (limitados a 30 resultados por vez).

No entanto, qual seria a melhor maneira RESTful de obter o número total de digamos, membros, por meio da minha API?

Atualmente, emito solicitações para uma estrutura de URL como a seguinte:

  • / api / members - retorna uma lista de membros (30 por vez, conforme mencionado acima)
  • / api / members / 1 - afeta um único membro, dependendo do método de solicitação usado

Minha pergunta é: como eu usaria uma estrutura de URL semelhante para obter o número total de membros no meu aplicativo? Obviamente, solicitar apenas o idcampo (semelhante à API Graph do Facebook) e contar os resultados seria ineficaz, pois apenas uma fatia de 30 resultados seria retornada.


Respostas:


84

Embora a resposta a / API / users seja paginada e retorne apenas 30 registros, nada impede que você inclua na resposta também o número total de registros e outras informações relevantes, como o tamanho da página, o número da página / deslocamento, etc. .

A API StackOverflow é um bom exemplo desse mesmo design. Aqui está a documentação para o método Users - https://api.stackexchange.com/docs/users


3
+1: Definitivamente, a coisa mais RESTful a fazer se os limites de busca forem impostos.
Donal Fellows

2
@bzim Você saberia que há uma próxima página para buscar, porque existe um link com rel = "next".
Darrel Miller

4
@Donal, o "próximo" rel é registrado com IANA iana.org/assignments/link-relations/link-relations.txt
Darrel Miller

1
@ Darrel - sim, isso poderia ser feito com qualquer tipo de "próximo" sinalizador na carga. Apenas sinto que ter a contagem total dos itens de coleção na resposta é valioso por si só e funciona como uma bandeira "seguinte" da mesma forma.
Franci Penov

5
Retornar um objeto que não é uma lista de itens não é uma implementação adequada de uma API REST, mas o REST não fornece nenhuma maneira de obter uma lista parcial de resultados. Então, para respeitar isso, acho que devemos usar cabeçalhos para transmitir outras informações, como total, token da próxima página e token da página anterior. Eu nunca tentei e preciso de conselhos de outros desenvolvedores.
Loenix 24/10/16

74

Eu prefiro usar cabeçalhos HTTP para esse tipo de informação contextual.

Para o número total de elementos eu uso o X-total-countcabeçalho.
Para obter links para a página seguinte, anterior etc., eu uso o Linkcabeçalho http :
http://www.w3.org/wiki/LinkHeader

O Github faz o mesmo: https://developer.github.com/v3/#pagination

Na minha opinião, é mais limpo, pois também pode ser usado quando você devolve conteúdo que não suporta hiperlinks (por exemplo, binários, imagens).


5
O RFC6648 substitui a convenção de prefixar os nomes de parâmetros não padronizados com a sequência X-.
JDawg

70

Ultimamente, tenho pesquisado extensivamente essa e outras perguntas relacionadas à paginação REST e achei construtivo adicionar algumas das minhas descobertas aqui. Estou expandindo um pouco a questão para incluir pensamentos sobre paginação, bem como a contagem, pois eles estão intimamente relacionados.

Cabeçalhos

Os metadados de paginação são incluídos na resposta na forma de cabeçalhos de resposta. O grande benefício dessa abordagem é que a carga útil da resposta é exatamente o que o solicitante de dados real estava solicitando. Facilitando o processamento da resposta para clientes que não estão interessados ​​nas informações de paginação.

Existem vários cabeçalhos (padrão e personalizados) usados ​​em caráter selvagem para retornar informações relacionadas à paginação, incluindo a contagem total.

Contagem X total

X-Total-Count: 234

Isso é usado em algumas APIs que encontrei na natureza. Existem também pacotes NPM para adicionar suporte a este cabeçalho, por exemplo, Loopback. Alguns artigos recomendam a configuração desse cabeçalho também.

Geralmente é usado em combinação com o Linkcabeçalho, que é uma solução muito boa para paginação, mas não possui as informações totais de contagem.

Ligação

Link: </TheBook/chapter2>;
      rel="previous"; title*=UTF-8'de'letztes%20Kapitel,
      </TheBook/chapter4>;
      rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel

Sinto-me, de ler muito sobre este assunto, que o consenso geral é usar o Linkcabeçalho para fornecer paginação links para clientes usando rel=next, rel=previousetc. O problema com isto é que ela não tem a informação de quantos registros totais existem, o que é por que muitas APIs combinam isso com o X-Total-Countcabeçalho.

Como alternativa, algumas APIs e, por exemplo, o padrão JsonApi , usam o Linkformato, mas adicionam as informações em um envelope de resposta em vez de em um cabeçalho. Isso simplifica o acesso aos metadados (e cria um local para adicionar as informações totais da contagem) às custas do aumento da complexidade do acesso aos próprios dados reais (adicionando um envelope).

Intervalo de conteúdo

Content-Range: items 0-49/234

Promovido por um artigo de blog chamado Range header, eu escolho você (para paginação)! . O autor defende fortemente o uso dos cabeçalhos Rangee Content-Rangepara paginação. Quando lemos atentamente a RFC nesses cabeçalhos, descobrimos que estender seu significado além dos intervalos de bytes foi realmente antecipado pela RFC e é explicitamente permitido. Quando usado no contexto de em itemsvez de bytes, o cabeçalho Range realmente nos permite solicitar um certo intervalo de itens e indicar a qual intervalo do resultado total os itens de resposta se referem. Esse cabeçalho também oferece uma ótima maneira de mostrar a contagem total. E é um verdadeiro padrão que mapeia principalmente a paginação individual. Também é usado na natureza .

Envelope

Muitas APIs, incluindo a do nosso site de perguntas e respostas favorito, usam um envelope , um invólucro em torno dos dados que são usados ​​para adicionar meta informações sobre os dados. Além disso, os padrões OData e JsonApi usam um envelope de resposta.

A grande desvantagem disso (imho) é que o processamento dos dados de resposta se torna mais complexo, pois os dados reais precisam ser encontrados em algum lugar do envelope. Além disso, existem muitos formatos diferentes para esse envelope e você precisa usar o correto. É revelador que os envelopes de resposta do OData e JsonApi são totalmente diferentes, com o OData misturando metadados em vários pontos da resposta.

Ponto final separado

Eu acho que isso foi abordado o suficiente nas outras respostas. Não investiguei muito isso, porque concordo com os comentários de que isso é confuso, pois agora você tem vários tipos de pontos de extremidade. Eu acho que é melhor se cada endpoint representar um (conjunto de) recursos.

Pensamentos adicionais

Não precisamos apenas comunicar as meta informações de paginação relacionadas à resposta, mas também permitir que o cliente solicite páginas / intervalos específicos. É interessante observar também esse aspecto para obter uma solução coerente. Aqui também podemos usar cabeçalhos (o Rangecabeçalho parece muito adequado) ou outros mecanismos, como parâmetros de consulta. Algumas pessoas defendem o tratamento de páginas de resultados como recursos separados, o que pode fazer sentido em alguns casos de uso (por exemplo /books/231/pages/52, acabei selecionando uma variedade de parâmetros de solicitação usados ​​com frequência, como pagesize, page[size]e limitetc, além de oferecer suporte ao Rangecabeçalho (e como parâmetro de solicitação também).


Eu estava particularmente interessado no Rangecabeçalho, mas não consegui encontrar evidências suficientes de que o uso de algo além de bytesum tipo de intervalo seja válido.
VisioN

2
Eu acho que a evidência mais clara pode ser encontrada na seção 14.5 da RFC : acceptable-ranges = 1#range-unit | "none"Eu acho que essa formulação deixa explicitamente espaço para outras unidades de alcance que bytes, embora as especificações em si só definam bytes.
Stijn de Witt

24

Alternativa quando você não precisa de itens reais

A resposta de Franci Penov é certamente o melhor caminho a percorrer, para que você sempre retorne itens, juntamente com todos os metadados adicionais sobre as solicitações de suas entidades. É assim que deve ser feito.

mas, às vezes, retornar todos os dados não faz sentido, porque talvez você não precise deles. Talvez tudo o que você precise é de metadados sobre o recurso solicitado. Como contagem total ou número de páginas ou outra coisa. Nesse caso, você sempre pode fazer com que a consulta de URL informe ao seu serviço para não retornar itens, mas apenas metadados como:

/api/members?metaonly=true
/api/members?includeitems=0

ou algo parecido ...


10
A incorporação dessas informações nos cabeçalhos tem a vantagem de poder fazer uma solicitação HEAD para apenas obter a contagem.
Felixfbecker

1
@felixfbecker exatamente, obrigado por reinventar a roda e desordenar as APIs com todos os tipos de diferentes mecanismos :)
EralpB

1
@EralpB Obrigado por reinventar a roda e desordenar as APIs !? HEAD é especificado em HTTP. metaonlyou includeitemsnão é.
Felixfbecker 11/01/19

2
O @felixfbecker apenas "exatamente" foi feito para você, o resto é para o OP. Desculpe pela confusão.
EralpB

O REST tem tudo a ver com alavancar o HTTP e utilizá-lo para o que foi planejado o máximo possível. O intervalo de conteúdo (RFC7233) deve ser usado neste caso. As soluções dentro do corpo não são boas, principalmente porque não funcionam com o HEAD. criar novos cabeçalhos, conforme sugerido aqui, é desnecessário e errado.
Vance Shipley

23

Você pode retornar a contagem como um cabeçalho HTTP personalizado em resposta a uma solicitação HEAD. Dessa forma, se um cliente deseja apenas a contagem, você não precisa retornar a lista real e não há necessidade de um URL adicional.

(Ou, se você estiver em um ambiente controlado de um ponto a outro, poderá usar um verbo HTTP personalizado, como COUNT.)


4
"Cabeçalho HTTP personalizado"? Isso seria algo surpreendente, o que, por sua vez, é contrário ao que eu acho que uma API RESTful deveria ser. Em última análise, não deve surpreender.
Donal Fellows

21
@Donal eu sei. Mas todas as boas respostas já foram tomadas. :(
bzlm

1
Eu também sei, mas às vezes você precisa deixar que outras pessoas respondam. Ou faça sua contribuição melhor de outras maneiras, como uma explicação detalhada de por que ela deve ser feita da melhor maneira possível, em vez de outras.
Donal Fellows

4
Em um ambiente controlado, isso pode não ser surpreendente, pois provavelmente seria usado internamente e com base na política de API de seus desenvolvedores. Eu diria que essa é uma boa solução em alguns casos e vale a pena ter aqui como uma nota de uma possível solução incomum.
precisa

1
Eu gosto muito de usar cabeçalhos HTTP para esse tipo de coisa (é realmente onde ele pertence). O cabeçalho de link padrão pode ser apropriado nesse caso (a API do Github usa isso).
Mike Marcacci


7

A partir de "X -" - o prefixo foi descontinuado. (consulte: https://tools.ietf.org/html/rfc6648 )

Nós achamos que "Accept-Ranges" é a melhor opção para mapear o intervalo de paginação: https://tools.ietf.org/html/rfc7233#section-2.3 Como as "Range Units" podem ser "bytes" ou " símbolo". Ambos não representam um tipo de dados personalizado. (consulte: https://tools.ietf.org/html/rfc7233#section-4.2 ) Ainda assim, afirma-se que

Implementações HTTP / 1.1 PODEM ignorar os intervalos especificados usando outras unidades.

O que indica: o uso de unidades de alcance personalizadas não é contra o protocolo, mas PODE ser ignorado.

Dessa forma, teríamos que definir os intervalos de aceitação para "membros" ou qualquer tipo de unidade à distância, esperávamos. Além disso, também defina o intervalo de conteúdo para o intervalo atual. (consulte: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.12 )

De qualquer forma, gostaria de seguir a recomendação do RFC7233 ( https://tools.ietf.org/html/rfc7233#page-8 ) para enviar um 206 em vez de 200:

Se todas as condições prévias forem verdadeiras, o servidor suporta o
campo de cabeçalho Range para o recurso de destino e os intervalos especificados são
válidos e satisfatórios (conforme definido na Seção 2.1), o servidor DEVE
enviar uma resposta 206 (Conteúdo Parcial) com uma carga útil que contém uma
ou mais representações parciais que correspondem aos
intervalos satisfatórios solicitados, conforme definido na Seção 4.

Portanto, como resultado, teríamos os seguintes campos de cabeçalho HTTP:

Para conteúdo parcial:

206 Partial Content
Accept-Ranges: members
Content-Range: members 0-20/100

Para conteúdo completo:

200 OK
Accept-Ranges: members
Content-Range: members 0-20/20

3

Parece mais fácil adicionar apenas um

GET
/api/members/count

e retorne a contagem total de membros


11
Não é uma boa ideia. Você obriga os clientes a fazer 2 solicitações para criar a paginação em suas páginas. Primeiro pedido para obter a lista de recursos e segundo para contar o total.
19413 Jekis

Eu acho que é uma boa abordagem ... você também pode retornar apenas uma lista de resultados como json e no lado do cliente, verificar o tamanho da coleção, para que esse caso seja um exemplo estúpido ... além disso, você pode ter / api / members / count e depois / api / members? offset = 10 & limit = 20
Michał Ziobro

1
Também tenha em mente que um monte de tipos de paginação não requerem uma contagem (Tais como rolagem infinita) - Por que calcular isso quando o cliente não pode precisar dele
tofarr

2

Que tal um novo ponto final> / api / members / count que apenas chama Members.Count () e retorna o resultado


27
Atribuir à contagem um terminal explícito o torna um recurso endereçável independente. Funcionará, mas suscitará perguntas interessantes para alguém novo em sua API - a contagem de membros da coleção é um recurso separado da coleção? Posso atualizá-lo com uma solicitação PUT? Existe para uma coleção vazia ou apenas se houver itens nela? Se a memberscoleção puder ser criada por uma solicitação POST para /api, também /api/members/countserá criada como efeito colateral, ou eu tenho que fazer uma solicitação POST explícita para criá-la antes de solicitá-la? :-)
Franci Penov

2

Às vezes, estruturas (como $ resource / AngularJS) exigem uma matriz como resultado da consulta, e você realmente não pode ter uma resposta, como {count:10,items:[...]}neste caso eu armazeno "count" em responseHeaders.

PS Na verdade, você pode fazer isso com $ resource / AngularJS, mas ele precisa de alguns ajustes.


Quais são esses ajustes? Eles seriam úteis em perguntas como esta: stackoverflow.com/questions/19140017/…
JBCP

Angular não EXIGE uma matriz como resultado da consulta, você apenas precisa configurar seu recurso com a propriedade de objeto de opção:isArray: false|true
Rémi Becheras

0

Você pode considerar countscomo um recurso. O URL seria então:

/api/counts/member

-1

Ao solicitar dados paginados, você conhece (por valor explícito do parâmetro do tamanho da página ou valor padrão do tamanho da página) o tamanho da página, para saber se obteve todos os dados em resposta ou não. Quando há menos dados em resposta do que o tamanho da página, você obtém dados inteiros. Quando uma página inteira é retornada, você deve solicitar outra página.

Prefiro ter um endpoint separado para count (ou o mesmo endpoint com o parâmetro countOnly). Porque você pode preparar o usuário final para um processo demorado / demorado, mostrando a barra de progresso iniciada corretamente.

Se você deseja retornar o tamanho dos dados em cada resposta, deve haver pageSize, deslocamento mencionado também. Para ser honesto, a melhor maneira é repetir também os filtros de solicitação. Mas a resposta se tornou muito complexa. Portanto, prefiro o endpoint dedicado para retornar a contagem.

<data>
  <originalRequest>
    <filter/>
    <filter/>
  </originalReqeust>
  <totalRecordCount/>
  <pageSize/>
  <offset/>
  <list>
     <item/>
     <item/>
  </list>
</data>

Em relação à minha, prefira um parâmetro countOnly ao ponto final existente. Portanto, quando especificada, a resposta contém apenas metadados.

ponto final? filter = value

<data>
  <count/>
  <list>
    <item/>
    ...
  </list>
</data>

ponto final? filter = value & countOnly = true

<data>
  <count/>
  <!-- empty list -->
  <list/>
</data>
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.