web-dev-qa-db-fra.com

Meilleures pratiques de pagination API

J'aimerais avoir de l'aide pour gérer un cas étrange Edge avec une API paginée que je construis.

Comme beaucoup d’API, celle-ci pagine de grands résultats. Si vous interrogez/foos, vous obtiendrez 100 résultats (par exemple, foo # 1-100) et un lien vers/foos? Page = 2, qui devrait renvoyer foo # 101-200.

Malheureusement, si foo # 10 est supprimé de l'ensemble de données avant que le consommateur d'API n'effectue la requête suivante,/foos? Page = 2 sera compensé par 100 et renverra les foos # 102-201.

Ceci est un problème pour les consommateurs d'API qui essaient de tirer tous les foos - ils ne recevront pas foo # 101.

Quelle est la meilleure pratique pour gérer cela? Nous aimerions le rendre aussi léger que possible (c'est-à-dire éviter de gérer les sessions pour les requêtes d'API). Des exemples d'autres API seraient grandement appréciés!

269
2arrs2ells

Je ne suis pas tout à fait sûr de la manière dont vos données sont gérées. Cela peut donc ou non fonctionner, mais avez-vous envisagé de paginer avec un champ d'horodatage?

Lorsque vous interrogez/foos, vous obtenez 100 résultats. Votre API devrait alors renvoyer quelque chose comme ceci (en supposant que JSON, mais si elle a besoin de XML, les mêmes principes peuvent être suivis):

{
    "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"
    }

}

Juste une note, n'utilisez qu'un seul horodatage repose sur une "limite" implicite dans vos résultats. Vous voudrez peut-être ajouter une limite explicite ou également utiliser une propriété until.

L'horodatage peut être déterminé dynamiquement à l'aide du dernier élément de données de la liste. Cela semble être plus ou moins la façon dont Facebook pagine dans son Graph API (faites défiler vers le bas pour voir les liens de pagination dans le format que j'ai donné ci-dessus).

Un problème peut être si vous ajoutez un élément de données, mais d'après votre description, il semblerait que ces éléments seraient ajoutés à la fin (sinon, faites le moi savoir et je verrai si je peux améliorer cela).

166
ramblinjan

Vous avez plusieurs problèmes.

Premièrement, vous avez l'exemple que vous avez cité.

Vous rencontrez également un problème similaire si des lignes sont insérées, mais dans ce cas, l'utilisateur obtient les données en double (sans doute plus facile à gérer que les données manquantes, mais cela pose toujours un problème).

Si vous n'effectuez pas une capture instantanée du jeu de données d'origine, il ne s'agit que d'une réalité.

Vous pouvez demander à l'utilisateur de créer un instantané explicite:

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

Quels résultats:

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

Ensuite, vous pouvez parcourir cette page toute la journée, car elle est maintenant statique. Cela peut être raisonnablement léger, car vous pouvez simplement capturer les clés de document réelles plutôt que les lignes entières.

Si le cas d'utilisation est simplement que vos utilisateurs veulent (et ont besoin) de toutes les données, vous pouvez simplement leur donner:

GET /query/12345?all=true

et juste envoyer le kit entier.

27
Will Hartung

Si vous avez une pagination, vous triez également les données à l'aide d'une clé. Pourquoi ne pas laisser les clients API inclure la clé du dernier élément de la collection précédemment renvoyée dans l’URL et ajouter une clause WHERE à votre requête SQL (ou quelque chose d’équivalent si vous n’utilisez pas SQL) pour qu’elle renvoie seuls les éléments pour lesquels la clé est supérieure à cette valeur?

24
kamilk

Il peut y avoir deux approches en fonction de la logique de votre serveur.

Approche 1: Lorsque le serveur n’est pas assez intelligent pour gérer les états d’objet.

Vous pouvez envoyer tous les identifiants uniques des enregistrements mis en cache au serveur, par exemple ["id1", "id2", "id3", "id4", "id5", "id6", "id7", "id8", "id9", "id10"] et un paramètre booléen pour savoir si vous demandez de nouveaux enregistrements (extraire pour actualiser) ou d'anciens enregistrements (en charger plus).

Votre serveur doit vous renvoyer de nouveaux enregistrements (charger plus d’enregistrements ou de nouveaux enregistrements via pull pour actualiser), ainsi que les identifiants des enregistrements supprimés dans ["id1", "id2", "id3", "id4", "id5", " id6 "," id7 "," id8 "," id9 "," id10 "].

Exemple: - Si vous demandez plus de charge, votre demande devrait ressembler à ceci: -

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

Supposons maintenant que vous demandez d'anciens enregistrements (chargez plus) et supposons que l'enregistrement "id2" soit mis à jour par quelqu'un et que les enregistrements "id5" et "id8" soient supprimés du serveur, votre réponse du serveur devrait alors ressembler à ceci: -

{
        "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"]
}

Mais dans ce cas, si vous avez beaucoup d’enregistrements en cache locaux supposez 500, votre chaîne de requête sera trop longue comme ceci: -

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

Approche 2: Lorsque le serveur est suffisamment intelligent pour gérer les états des objets en fonction de la date.

Vous pouvez envoyer l'ID du premier enregistrement, le dernier enregistrement et l'heure de la demande précédente. Ainsi, votre demande est toujours petite, même si vous avez beaucoup d’enregistrements en cache.

Exemple: - Si vous demandez plus de charge, votre demande devrait ressembler à ceci: -

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

Votre serveur est responsable de renvoyer l’id des enregistrements supprimés qui est supprimé après last_request_time, ainsi que de renvoyer l’enregistrement mis à jour après last_request_time entre "id1" et "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"]
}

Tirer pour rafraîchir:-

enter image description here

Charger plus

enter image description here

17

Il peut être difficile de trouver les meilleures pratiques car la plupart des systèmes avec des API ne tiennent pas compte de ce scénario, car il s'agit d'un Edge extrême ou ne suppriment généralement pas les enregistrements (Facebook, Twitter). Facebook indique en fait que chaque "page" peut ne pas avoir le nombre de résultats demandé en raison du filtrage effectué après la pagination. https://developers.facebook.com/blog/post/478/

Si vous avez vraiment besoin de prendre en charge cette affaire Edge, vous devez "vous souvenir" de l'endroit où vous l'avez laissée. La suggestion de jandjorgensen est quasi parfaite, mais j’utiliserais un champ unique, comme la clé primaire. Vous devrez peut-être utiliser plusieurs champs.

Après le flux de Facebook, vous pouvez (et devriez) mettre en cache les pages déjà demandées et ne renvoyer que celles dont les lignes supprimées sont filtrées si elles demandent une page déjà demandée.

14
Brent Baisley

La pagination est généralement une opération "utilisateur" et, pour éviter la surcharge, à la fois sur les ordinateurs et le cerveau humain, vous donnez généralement un sous-ensemble. Cependant, plutôt que de penser que nous n’obtenons pas toute la liste, il vaut peut-être mieux demander est-ce important?

Si une vue de défilement dynamique en direct est nécessaire, les API REST qui sont de nature requête/réponse ne conviennent pas à cette fin. Pour cela, vous devez envisager les événements WebSockets ou HTML5 Server-Sent pour informer votre serveur frontal du traitement des modifications.

Maintenant, s'il y a un besoin pour obtenir un instantané des données, je fournirais simplement un appel d'API qui fournit toutes les données dans une demande sans pagination. Notez que vous auriez besoin de quelque chose qui ferait le streaming de la sortie sans le charger temporairement en mémoire si vous avez un grand ensemble de données.

Dans mon cas, je désigne implicitement certains appels d'API pour permettre d'obtenir toutes les informations (principalement des données de table de référence). Vous pouvez également sécuriser ces API pour ne pas nuire à votre système.

9
Archimedes Trajano

Option A: Pagination de jeu de clés avec un horodatage

Pour éviter les inconvénients de la pagination offset que vous avez mentionnés, vous pouvez utiliser la pagination basée sur le jeu de clés. Habituellement, les entités ont un horodatage qui indique leur heure de création ou de modification. Cet horodatage peut être utilisé pour la pagination: il suffit de passer l'horodatage du dernier élément comme paramètre de requête pour la requête suivante. Le serveur, à son tour, utilise l’horodatage comme critère de filtrage (par exemple, 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 cette façon, vous ne manquerez aucun élément. Cette approche devrait suffire pour de nombreux cas d'utilisation. Cependant, gardez à l'esprit les points suivants:

  • Vous pouvez rencontrer des boucles sans fin lorsque tous les éléments d'une même page ont le même horodatage.
  • Vous pouvez transmettre de nombreux éléments au client plusieurs fois lorsque des éléments ayant le même horodatage chevauchent deux pages.

Vous pouvez réduire ces risques en augmentant la taille de la page et en utilisant des horodatages d'une précision à la milliseconde.

Option B: Pagination de jeu de clés étendue avec un jeton de continuation

Pour gérer les inconvénients mentionnés de la pagination normale du jeu de clés, vous pouvez ajouter un décalage à l'horodatage et utiliser un "jeton de continuation" ou un "curseur". Le décalage est la position de l'élément par rapport au premier élément avec le même horodatage. Généralement, le jeton a un format tel que Timestamp_Offset. Il est transmis au client dans la réponse et peut être renvoyé au serveur afin de récupérer la page suivante.

{
    "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"
    }
}

Le jeton "1512757072_2" pointe vers le dernier élément de la page et indique "le client a déjà reçu le deuxième élément avec l'horodatage 1512757072". De cette façon, le serveur sait où continuer.

Veuillez noter que vous devez gérer les cas où les éléments ont été modifiés entre deux demandes. Cela se fait généralement en ajoutant une somme de contrôle au jeton. Cette somme de contrôle est calculée sur les ID de tous les éléments avec cet horodatage. Nous nous retrouvons donc avec un format de jeton comme celui-ci: Timestamp_Offset_Checksum.

Pour plus d'informations sur cette approche, consultez l'article du blog " pagination de l'API Web avec jetons de continuation ". Un inconvénient de cette approche est la mise en œuvre délicate, car de nombreux cas critiques doivent être pris en compte. C'est pourquoi des bibliothèques telles que continuation-token peuvent être pratiques (si vous utilisez un langage Java/JVM). Disclaimer: Je suis l'auteur du post et un co-auteur de la bibliothèque.

7
phauer

Je pense qu'actuellement votre api répond comme il se doit. Les 100 premiers enregistrements de la page dans l'ordre global des objets que vous gérez. Votre explication indique que vous utilisez une sorte d'identifiant de commande pour définir l'ordre de vos objets pour la pagination.

Au cas où vous souhaiteriez que la page 2 commence toujours par 101 et se termine par 200, vous devez définir le nombre d'entrées de la page comme variable, car elles sont susceptibles d'être supprimées.

Vous devriez faire quelque chose comme le pseudocode ci-dessous:

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)
4
mickeymoon

Juste pour ajouter à cette réponse de Kamilk: https://www.stackoverflow.com/a/13905589

Cela dépend beaucoup de la taille du jeu de données sur lequel vous travaillez. Les petits ensembles de données fonctionnent efficacement sur la pagination offset , mais les grands ensembles de données en temps réel nécessitent une pagination du curseur .

Nous avons trouvé un article merveilleux sur la manière dont Slack a modifié la pagination de son api à mesure que ses jeux de données augmentaient, expliquant les aspects positifs et négatifs à chaque étape: https: // slack.engineering/evolving-api-pagination-at-slack-1c1f644f8e12

3

J'ai longuement réfléchi à cela et je me suis finalement retrouvé avec la solution que je vais décrire ci-dessous. La complexité est considérable, mais si vous franchissez cette étape, vous obtiendrez ce que vous recherchez vraiment, à savoir des résultats déterministes pour les demandes futures.

Votre exemple d'élément en cours de suppression n'est que la partie visible de l'iceberg. Que se passe-t-il si vous filtrez par color=blue mais que quelqu'un change de couleur d'élément entre les demandes? Extraire tous les éléments de manière fiable est impossible ... à moins que ... nous implémentions historique de révision.

Je l'ai mis en œuvre et c'est en réalité moins difficile que prévu. Voici ce que j'ai fait:

  • J'ai créé une seule table changelogs avec une colonne ID d'auto-incrémentation
  • Mes entités ont un champ id, mais ce n'est pas la clé primaire
  • Les entités ont un champ changeId qui est à la fois la clé primaire et une clé étrangère pour les changelogs.
  • Chaque fois qu'un utilisateur crée, met à jour ou supprime un enregistrement, le système insère un nouvel enregistrement dans changelogs, saisit l'ID et l'assigne à une nouvelle version de l'entité, qu'il insère ensuite dans la DB
  • Mes requêtes sélectionnent le changeId maximum (groupé par id) et se joignent automatiquement à celui-ci pour obtenir les versions les plus récentes de tous les enregistrements.
  • Les filtres sont appliqués aux enregistrements les plus récents
  • Un champ d'état indique si un élément est supprimé.
  • Le changeId max est renvoyé au client et ajouté en tant que paramètre de requête dans les requêtes suivantes.
  • Etant donné que seules les nouvelles modifications sont créées, chaque changeId représente un instantané unique des données sous-jacentes au moment de la création de la modification.
  • Cela signifie que vous pouvez mettre en cache les résultats des requêtes contenant le paramètre changeId pour toujours. Les résultats n'expireront jamais car ils ne changeront jamais.
  • Cela ouvre également des fonctionnalités intéressantes telles que l'annulation/la restauration, la synchronisation du cache client, etc. Toutes les fonctionnalités qui bénéficient de l'historique des modifications.
3
Stijn de Witt