practice best rest pagination api-design

rest - best - Mejores prácticas de paginación API



rest api conventions (10)

Me encantaría algo de ayuda con un extraño caso de borde con una API paginada que estoy construyendo.

Al igual que muchas API, esta página genera grandes resultados. Si consulta / foos, obtendrá 100 resultados (es decir, foo # 1-100), y un enlace a / foos? Page = 2 que debería devolver foo # 101-200.

Desafortunadamente, si foo # 10 se elimina del conjunto de datos antes de que el consumidor de API realice la próxima consulta, / foos? Page = 2 se compensará con 100 y devolverá foos # 102-201.

Este es un problema para los consumidores de API que intentan obtener todos los foos; no recibirán foo # 101.

¿Cuál es la mejor práctica para manejar esto? Nos gustaría que sea lo más liviano posible (es decir, evitando el manejo de sesiones para solicitudes de API). ¡Los ejemplos de otras APIs serían muy apreciados!


Creo que actualmente su API está respondiendo como debería. Los primeros 100 registros en la página en el orden general de los objetos que está manteniendo. Su explicación dice que está utilizando algún tipo de ID de pedido para definir el orden de sus objetos para la paginación.

Ahora, en caso de que desee que la página 2 siempre comience desde 101 y termine en 200, entonces debe hacer que la cantidad de entradas en la página sea variable, ya que están sujetas a eliminación.

Deberías hacer algo como el siguiente pseudocódigo:

page_max = 100 def get_page_results(page_no) : start = (page_no - 1) * page_max + 1 end = page_no * page_max return fetch_results_by_id_between(start, end)


He pensado mucho sobre esto y finalmente terminé con la solución que describiré a continuación. Es un gran avance en complejidad, pero si lo hace, terminará con lo que realmente busca, lo cual es un resultado determinista para futuras solicitudes.

Su ejemplo de un elemento que se elimina es solo la punta del iceberg. ¿Qué sucede si está filtrando por color=blue pero alguien cambia los colores de los elementos entre las solicitudes? Obtener todos los elementos de una manera paginada de manera confiable es imposible ... a menos que ... implementemos el historial de revisiones .

Lo he implementado y en realidad es menos difícil de lo que esperaba. Esto es lo que hice:

  • Creé una sola tabla de changelogs con una columna de ID de incremento automático
  • Mis entidades tienen un campo de id , pero esta no es la clave principal
  • Las entidades tienen un campo changeId que es tanto la clave principal como una clave externa para los registros de cambios.
  • Cada vez que un usuario crea, actualiza o elimina un registro, el sistema inserta un registro nuevo en los changelogs de changelogs , toma el id y lo asigna a una nueva versión de la entidad, que luego inserta en el DB
  • Mis consultas seleccionan el máximo ChangeId (agrupado por id) y se unen automáticamente para obtener las versiones más recientes de todos los registros.
  • Los filtros se aplican a los registros más recientes.
  • Un campo de estado realiza un seguimiento de si un elemento se elimina
  • Max changeId se devuelve al cliente y se agrega como un parámetro de consulta en solicitudes posteriores
  • Debido a que solo se crean cambios nuevos, cada cambio individual representa una instantánea única de los datos subyacentes en el momento en que se creó el cambio.
  • Esto significa que puede almacenar en caché los resultados de las solicitudes que tienen el parámetro changeId en ellos para siempre. Los resultados nunca caducarán porque nunca cambiarán.
  • Esto también abre una función interesante como deshacer / revertir, sincronizar el caché del cliente, etc. Cualquier característica que se beneficie del historial de cambios.

La paginación es generalmente una operación de "usuario" y para evitar la sobrecarga tanto en las computadoras como en el cerebro humano, generalmente se asigna un subconjunto. Sin embargo, en lugar de pensar que no obtenemos la lista completa, puede ser mejor preguntar, ¿importa?

Si se necesita una vista precisa de desplazamiento en vivo, las API REST que son de naturaleza de solicitud / respuesta no son adecuadas para este propósito. Para esto, debe considerar los eventos WebSockets o HTML5 Server-Env para que su interfaz de usuario sepa cuándo lidiar con los cambios.

Ahora, si hay una necesidad de obtener una instantánea de los datos, solo proporcionaría una llamada a la API que proporciona todos los datos en una solicitud sin paginación. Ten en cuenta que necesitarías algo que hiciera la transmisión de la salida sin cargarla temporalmente en la memoria si tienes un gran conjunto de datos.

Para mi caso, designo implícitamente algunas llamadas a la API para permitir obtener toda la información (principalmente datos de la tabla de referencia). También puede proteger estas API para que no dañe su sistema.


No estoy completamente seguro de cómo se manejan sus datos, por lo que esto puede o no funcionar, pero ¿ha considerado paginar con un campo de marca de tiempo?

Cuando consultas / foos obtienes 100 resultados. Su API debería devolver algo como esto (asumiendo JSON, pero si necesita XML, se pueden seguir los mismos principios):

{ "data" : [ { data item 1 with all relevant fields }, { data item 2 }, ... { data item 100 } ], "paging": { "previous": "http://api.example.com/foo?since=TIMESTAMP1" "next": "http://api.example.com/foo?since=TIMESTAMP2" } }

Solo una nota, solo el uso de una marca de tiempo se basa en un "límite" implícito en sus resultados. Es posible que desee agregar un límite explícito o también usar una propiedad until .

La marca de tiempo se puede determinar dinámicamente utilizando el último elemento de datos en la lista. Esto parece ser más o menos como Facebook se compagina en su Graph API (desplácese hacia abajo para ver los enlaces de paginación en el formato que dí anteriormente).

Un problema puede ser si agrega un elemento de datos, pero según su descripción parece que se agregarían al final (en caso contrario, avíseme y veré si puedo mejorar esto).


Puede haber dos enfoques dependiendo de la lógica del lado del servidor.

Enfoque 1: cuando el servidor no es lo suficientemente inteligente como para manejar estados de objetos.

Puede enviar todos los ID únicos del registro en caché al servidor, por ejemplo ["id1", "id2", "id3", "id4", "id5", "id6", "id7", "id8", "id9", "id10"] y un parámetro booleano para saber si está solicitando registros nuevos (extraer para actualizar) o registros antiguos (cargar más).

Su servidor debe ser responsable de devolver nuevos registros (cargar más registros o nuevos registros mediante extracción para actualizar), así como la identificación de los registros eliminados de ["id1", "id2", "id3", "id4", "id5", " id6 "," id7 "," id8 "," id9 "," id10 "].

Ejemplo: - Si está solicitando una carga más, su solicitud debería tener un aspecto similar al siguiente:

{ "isRefresh" : false, "cached" : ["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10"] }

Ahora suponga que está solicitando registros antiguos (cargar más) y suponga que el registro "id2" sea actualizado por alguien y que los registros "id5" e "id8" se eliminen del servidor, entonces la respuesta de su servidor debería tener este aspecto:

{ "records" : [ {"id" :"id2","more_key":"updated_value"}, {"id" :"id11","more_key":"more_value"}, {"id" :"id12","more_key":"more_value"}, {"id" :"id13","more_key":"more_value"}, {"id" :"id14","more_key":"more_value"}, {"id" :"id15","more_key":"more_value"}, {"id" :"id16","more_key":"more_value"}, {"id" :"id17","more_key":"more_value"}, {"id" :"id18","more_key":"more_value"}, {"id" :"id19","more_key":"more_value"}, {"id" :"id20","more_key":"more_value"}], "deleted" : ["id5","id8"] }

Pero en este caso, si tiene muchos registros locales en caché, suponga 500, entonces su cadena de solicitud será demasiado larga como esta:

{ "isRefresh" : false, "cached" : ["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10",………,"id500"]//Too long request }

Enfoque 2: cuando el servidor es lo suficientemente inteligente como para manejar estados de objetos según la fecha.

Puede enviar el ID del primer registro y el último registro y el tiempo de la época de la solicitud previa. De esta manera, su solicitud siempre es pequeña, incluso si tiene una gran cantidad de registros en caché

Ejemplo: - Si está solicitando una carga más, su solicitud debería tener un aspecto similar al siguiente:

{ "isRefresh" : false, "firstId" : "id1", "lastId" : "id10", "last_request_time" : 1421748005 }

El servidor es responsable de devolver los ID de los registros eliminados que se eliminan después de last_request_time, así como también devolver el registro actualizado después de last_request_time entre "id1" e "id10".

{ "records" : [ {"id" :"id2","more_key":"updated_value"}, {"id" :"id11","more_key":"more_value"}, {"id" :"id12","more_key":"more_value"}, {"id" :"id13","more_key":"more_value"}, {"id" :"id14","more_key":"more_value"}, {"id" :"id15","more_key":"more_value"}, {"id" :"id16","more_key":"more_value"}, {"id" :"id17","more_key":"more_value"}, {"id" :"id18","more_key":"more_value"}, {"id" :"id19","more_key":"more_value"}, {"id" :"id20","more_key":"more_value"}], "deleted" : ["id5","id8"] }

Tire para actualizar: -

Carga más


Puede ser difícil encontrar las mejores prácticas ya que la mayoría de los sistemas con API no se adaptan a este escenario, ya que es una ventaja extrema, o no suelen eliminar registros (Facebook, Twitter). Facebook realmente dice que cada "página" puede no tener la cantidad de resultados solicitados debido al filtrado realizado después de la paginación. https://developers.facebook.com/blog/post/478/

Si realmente necesita acomodar este caso de borde, necesita "recordar" dónde lo dejó. La sugerencia de jandjorgensen es casi perfecta, pero usaría un campo que se garantiza que es único como la clave principal. Es posible que necesite utilizar más de un campo.

Siguiendo el flujo de Facebook, puede (y debe) almacenar en caché las páginas ya solicitadas y simplemente devolver aquellas con filas eliminadas filtradas si solicitan una página que ya habían solicitado.


Si tienes paginación, también ordenas los datos por alguna clave. Por qué no permitir que los clientes API incluyan la clave del último elemento de la colección devuelta anteriormente en la URL y agregue una cláusula WHERE a su consulta SQL (o algo equivalente, si no está usando SQL) para que solo devuelva esos elementos para ¿Cuál es la clave mayor que este valor?


Solo para agregar a esta respuesta por Kamilk: https://www..com/a/13905589

Depende mucho del tamaño del conjunto de datos en el que estés trabajando. Los conjuntos de datos pequeños funcionan de manera efectiva en la paginación de desplazamiento, pero los conjuntos de datos grandes en tiempo real requieren la paginación del cursor.

Encontré un artículo maravilloso sobre cómo Slack evolucionó la paginación de su api ya que los conjuntos de datos aumentaron explicando los aspectos positivos y negativos en cada etapa: https://slack.engineering/evolving-api-pagination-at-slack-1c1f644f8e12


Tienes varios problemas.

Primero, tienes el ejemplo que citaste.

También tiene un problema similar si se insertan filas, pero en este caso el usuario obtiene datos duplicados (posiblemente más fácil de administrar que los datos faltantes, pero sigue siendo un problema).

Si no está tomando instantáneas del conjunto de datos original, entonces esto es solo un hecho de la vida real.

Puede hacer que el usuario haga una instantánea explícita:

POST /createquery filter.firstName=Bob&filter.lastName=Eubanks

Cuales resultados:

HTTP/1.1 301 Here''s your query Location: http://www.example.org/query/12345

Entonces puedes pagarlo todo el día, ya que ahora es estático. Esto puede ser razonablemente ligero, ya que solo puede capturar las claves reales del documento en lugar de las filas completas.

Si el caso de uso es simplemente que sus usuarios quieren (y necesitan) todos los datos, simplemente puede dárselos:

GET /query/12345?all=true

y acaba de enviar todo el kit.


Opción A: Paginación de conjunto de teclas con una marca de tiempo

Para evitar los inconvenientes de la paginación de compensación que ha mencionado, puede utilizar la paginación basada en conjunto de claves. Generalmente, las entidades tienen una marca de tiempo que indica su tiempo de creación o modificación. Esta marca de tiempo se puede usar para la paginación: simplemente pase la marca de tiempo del último elemento como el parámetro de consulta para la siguiente solicitud. El servidor, a su vez, utiliza la marca de tiempo como un criterio de filtro (p. Ej., WHERE modificationDate >= receivedTimestampParameter )

{ "elements": [ {"data": "data", "modificationDate": 1512757070} {"data": "data", "modificationDate": 1512757071} {"data": "data", "modificationDate": 1512757072} ], "pagination": { "lastModificationDate": 1512757072, "nextPage": "https://domain.de/api/elements?modifiedSince=1512757072" } }

De esta manera, no te perderás ningún elemento. Este enfoque debería ser lo suficientemente bueno para muchos casos de uso. Sin embargo, tenga en cuenta lo siguiente:

  • Puede encontrarse con bucles infinitos cuando todos los elementos de una sola página tienen la misma marca de tiempo.
  • Puede entregar muchos elementos varias veces al cliente cuando los elementos con la misma marca de tiempo se superponen en dos páginas.

Puede hacer que esos inconvenientes sean menos probables aumentando el tamaño de la página y utilizando marcas de tiempo con una precisión de milisegundos.

Opción B: Paginación de conjunto de claves extendida con un token de continuación

Para manejar los inconvenientes mencionados de la paginación de conjunto de teclas normal, puede agregar un desplazamiento a la marca de tiempo y usar el llamado "Token de Continuación" o "Cursor". El desplazamiento es la posición del elemento con respecto al primer elemento con la misma marca de tiempo. Normalmente, el token tiene un formato como Timestamp_Offset . Se pasa al cliente en la respuesta y se puede enviar de nuevo al servidor para recuperar la página siguiente.

{ "elements": [ {"data": "data", "modificationDate": 1512757070} {"data": "data", "modificationDate": 1512757072} {"data": "data", "modificationDate": 1512757072} ], "pagination": { "continuationToken": "1512757072_2", "nextPage": "https://domain.de/api/elements?continuationToken=1512757072_2" } }

El token "1512757072_2" apunta al último elemento de la página y dice "el cliente ya obtuvo el segundo elemento con la marca de tiempo 1512757072". De esta manera, el servidor sabe dónde continuar.

Tenga en cuenta que debe manejar los casos en que los elementos se cambiaron entre dos solicitudes. Esto generalmente se hace agregando una suma de comprobación al token. Esta suma de comprobación se calcula sobre los ID de todos los elementos con esta marca de tiempo. Así que terminamos con un formato de token como este: Timestamp_Offset_Checksum .

Para obtener más información sobre este enfoque, consulte la publicación del blog " Paginación de API web con tokens de continuación ". Un inconveniente de este enfoque es la difícil implementación, ya que hay muchos casos de esquina que deben tenerse en cuenta. Es por eso que las bibliotecas como continuation-token pueden ser útiles (si está utilizando Java / un lenguaje JVM). Descargo de responsabilidad: soy el autor de la publicación y coautor de la biblioteca.