Prácticas recomendadas de paginación de API


Me encantaría un poco de ayuda para manejar un extraño caso edge con una API paginada que estoy construyendo.

Al igual que muchas API, esta pagina resultados grandes. Si usted 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 API realice la siguiente consulta, /foos?page = 2 se compensará por 100 y devolverá foos # 102-201.

Este es un problema para los consumidores API que están tratando de tirar de todos los foos-no recibirán foo #101.

¿Cuál es la mejor práctica para manejar esto? Nos gustaría hacerlo lo más ligero posible (es decir, evitar el manejo de sesiones para solicitudes de API). Ejemplos de otras API serían muy apreciados!

Author: Laurel, 2012-12-14

10 answers

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 usted consulta /foos obtiene 100 resultados. Tu 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 usando 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 un until propiedad.

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

Un problema puede ser si agrega un elemento de datos, pero según su descripción, parece que se agregarían al final (si no, hágamelo saber y veré si puedo mejorar esto).

 153
Author: ramblinjan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-10-09 22:45:04

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 (podría decirse que es más fácil de administrar que los datos faltantes, pero sigue siendo un problema).

Si no está snapshoteando el conjunto de datos original, entonces esto es solo un hecho de la vida.

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

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

Que resulta:

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

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

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

GET /query/12345?all=true

Y simplemente envíe el kit completo.

 25
Author: Will Hartung,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-12-18 21:27:29

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

 21
Author: kamilk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-12-16 21:21:52

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 de 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 nuevos registros(tire para actualizar) o registros antiguos(cargar más).

Su servidor debe ser responsable de devolver nuevos registros (cargar más registros o nuevos registros a través de pull to refresh), así como id de registros eliminados de ["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10"].

Ejemplo: - Si está solicitando cargar más, entonces su solicitud debe verse algo como esto: -

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

Ahora supongamos que está solicitando registros antiguos (cargar más) y supongamos que alguien actualiza el registro" id2 "y los registros" id5 "e" id8 " se eliminan del servidor, entonces su respuesta del servidor debería verse algo como esto: -

{
        "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é supongamos 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 los estados de los objetos de acuerdo con la fecha.

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

Ejemplo: - Si usted es solicitar cargar más de lo que su solicitud debe ser algo como esto: -

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

Su servidor es responsable de devolver los id de los registros eliminados que se eliminan después del last_request_time, así como devolver el registro actualizado después del last_request_time entre "id1" y "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: -

introduzca la descripción de la imagen aquí

Cargar más

introduzca la descripción de la imagen aquí

 16
Author: Mohd Iftekhar Qurashi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-12 19:28:59

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, porque es una ventaja extrema, o no suelen eliminar registros (Facebook, Twitter). Facebook en realidad dice que cada "página" puede no tener el número 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, debe "recordar" dónde lo dejó. jandjorgensen la sugerencia es casi perfecta, pero usaría un campo garantizado para ser único como la clave primaria. Es posible que necesite usar 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.

 13
Author: Brent Baisley,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-12-16 21:29:17

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 da un subconjunto. Sin embargo, en lugar de pensar que no obtenemos toda la lista, puede ser mejor preguntar ¿importa?

Si se necesita una vista de desplazamiento en vivo precisa, las API REST que son de naturaleza solicitud/respuesta no son adecuadas para este propósito. Para esto, debe considerar los eventos enviados por el servidor WebSockets o HTML5 para que su front end sepa cuándo lidiando con los cambios.

Ahora, si hay una necesidad para 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. Eso sí, necesitaría algo que hiciera streaming de la salida sin cargarla temporalmente en la memoria si tiene un conjunto de datos grande.

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 asegurar estos API para que no dañe su sistema.

 8
Author: Archimedes Trajano,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-29 19:25:10

Creo que actualmente tu api está respondiendo de la manera que debería. Los primeros 100 registros de 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 orden 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 el número de entradas en la página sea variable, ya que están sujetas a eliminación.

Usted debe hacer algo así como el pseudocódigo siguiente:

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)
 3
Author: mickeymoon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-02-05 11:16:37

He pensado mucho sobre esto y finalmente terminé con la solución que describiré a continuación. Es un paso bastante grande en complejidad, pero si haces este paso, terminarás con lo que realmente buscas, que son resultados deterministas para solicitudes futuras.

El ejemplo de un elemento que se elimina es solo la punta del iceberg. ¿Qué pasa si está filtrando por color=blue pero alguien cambia los colores de los elementos entre las solicitudes? Recuperar todos los elementos de una manera paginada de forma fiable es imposible... excepto... implementamos historial de revisiones.

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

  • He creado una sola tabla changelogs con una columna de ID de incremento automático
  • Mis entidades tienen un campo id, pero esta no es la clave primaria
  • Las entidades tienen un campo changeId que es tanto la clave primaria como una clave foránea para los registros de cambios.
  • Cada vez que un usuario crea, actualiza o borra un registro, el sistema inserta un nuevo registro en changelogs, toma el id y lo asigna a una nueva versión de la entidad, que luego inserta en la 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
  • El max changeId se devuelve al cliente y se añade como un parámetro de consulta en solicitudes posteriores
  • Debido a que solo se crean cambios nuevos, cada changeId 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 ellas para siempre. Los resultados nunca caducarán porque nunca cambiarán.
  • Esto también abre funciones interesantes como reversión / reversión, sincronización de caché de cliente, etc. Cualquier característica que benefíciese del historial de cambios.
 2
Author: Stijn de Witt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-04 23:58:25

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

Para evitar los inconvenientes de la paginación de desplazamiento que ha mencionado, puede usar la paginación basada en conjuntos de claves. Por lo general, las entidades tienen una marca de tiempo que indica su hora 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 parámetro de consulta para la siguiente solicitud. El servidor, a su vez, utiliza la marca de tiempo como 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 interminables cuando todos los elementos de una sola página tienen la misma marca de tiempo.
  • Puede entregar varios elementos varias veces al cliente cuando los elementos con la misma marca de tiempo están superponiendo 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 precisión de milisegundos.

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

Para manejar los inconvenientes mencionados de la paginación normal del conjunto de claves, puede agregar un desplazamiento a la marca de tiempo y usar un llamado "Token de continuación" o "Cursor". El desplazamiento es la posición del elemento en relación con el primer elemento con la misma marca de tiempo. Por lo general, el token tiene un formato como Timestamp_Offset. Se pasa al cliente en la respuesta y se puede enviar volver 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 tiene que manejar los casos en los que los elementos se cambiaron entre dos solicitudes. Esto generalmente se hace agregando una suma de verificació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 " Web API Pagination with Continuation Tokens". Un inconveniente de este enfoque es la aplicación difícil, ya que hay muchos casos de esquina que deben tenerse en cuenta. Es por eso que bibliotecas como continuation-token pueden ser útiles (si está utilizando Java/un lenguaje JVM). Descargo de responsabilidad: Soy el autor del post y coautor del biblioteca.

 2
Author: phauer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-20 12:02:01

Solo para añadir a esta respuesta de Kamilk: https://www.stackoverflow.com/a/13905589

Depende mucho del tamaño del conjunto de datos en el que esté trabajando. Los conjuntos de datos pequeños funcionan efectivamente en la paginación en offset, 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 a medida que aumentaban los conjuntos de datos explicando los aspectos positivos y negativos en cada etapa : https://slack.ingeniería / evolución-api-paginación-en-slack-1c1f644f8e12

 0
Author: Shubham Srivastava,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-02 17:35:51