OData Pagination avec l'API
La notion de pagination est fondamentale.
Par exemple si votre application souhaite afficher une liste de clients, vous allez définir une pagination pour proposer des pages de listes de clients cohérentes avec l’interface utilisateur et non pas une page affichant la totalité des clients.
Vous pourrez ainsi décider d’afficher 10, 25, 50 ou 100 clients par page mais jamais 1000, 5000 ou 10000, ces nombres n’étant pas cohérents avec l’interface utilisateur.
De plus vos développements doivent être en phase avec le nombre maximal d’enregistrements que l’API Sage Business Cloud Comptabilité renvoie à chaque appel.
Il est important de savoir que l’API Sage Business Cloud Comptabilité renvoie à chaque demande un maximum de 2000 enregistrements.
Ainsi si vous souhaitez par exemple récupérer la totalité des écritures comptables d’un exercice en vue de calculer des consolidations, vous devez prévoir que le nombre d’écritures à récupérer risque probablement de dépasser 2000 et il vous faudra implémenter une itération afin de récupérer par lot de 2000 enregistrements la totalité des écritures.
Pourquoi l’API Sage Business Cloud Comptabilité est limitée à 2000 enregistrements par appel ?
Les données requêtées par l’API Sage Business Cloud Comptabilité vont transiter entre l’application Sage Business Cloud Comptabilité et votre application.
Si une table de votre base de données contient des dizaines de milliers d’enregistrements, vous ne pouvez pas les envoyer en une seule demande à la fois pour des raisons de volumétrie et de sécurité.
Pour éviter ce problème, le serveur peut limiter le nombre d’enregistrements qu’il envoie à chaque demande.
Pour l’API Sage Business Cloud Comptabilité cette limite a été fixée à 2000.
Attention ! si vous récupérez des lots de 2000 enregistrements, vous devrez prévoir au maximum de limiter les propriétés à retourner plutôt que de demander par défaut à l’API de retourner les valeurs de toutes les propriétés.
Exemple : En moyenne, renvoyer toutes les valeurs de toutes les propriétés de 2000 écritures avec un $expand:journal
peut prendre jusqu’à 6 fois plus de temps que de renvoyer uniquement l’intitulé de l’écriture et le code du journal, ceci en raison du volume de données à transmettre.
Dans cet exemple utiliser de préférence $select=intitule
et $expand=journal($select='code')
plutôt que $select non affecté
et $expand=journal
.
Ayez donc toujours le réflexe de vous demander quelles sont les valeurs à retourner afin de définir la limitation $select
optimale.
Notion de pagination
Exemple
Imaginons que votre application souhaite afficher une liste de clients par pages de 100 selon des critères de filtres :
- Votre dossier contient 6500 clients,
- le filtre actuellement défini par l’utilisateur a été transmis à travers votre requête au serveur, le serveur a trouvé localement 175 clients répondant aux critères de filtre,
- côté application vous demanderez un maximum de 100 enregistrements à chaque appel en forçant
$top:100
, la requête va retourner 100 enregistrements et non 175, - pour que l’utilisateur puisse consulter les 175 résultats, il est nécessaire que vous définissiez une pagination,
- vous pourrez pour cela prévoir un bouton Suivant pour que votre utilisateur puisse consulter les 100 premiers résultats puis les 75 suivants.
Une application d’exemple exploite la pagination lors de la recherche des clients. Compte tenu du faible nombre de clients de la société de démonstration, la pagination a été limitée à 10.
voir : Exemples / Projet d’application en C# .NET + JavaScript.
Utilisation de $count et $skip
Pour gérer la pagination vous devez connaître le nombre total d’enregistrements répondant aux critères de filtre de votre requête et le nombre d’enregistrements renvoyés à chaque appel afin de définir le nombre d’enregistrements à sauter en utilisant $skip
.
Paramètre | Type | Description |
---|---|---|
$skip | integer | Jeton de pagination pour obtenir l’ensemble des résultats suivants |
$count | boolean | Nombre total d’éléments répondants à la requête |
- Le paramètre
$count
doit être affecté àtrue
pour pouvoir récupérer le nombre d’enregistrements répondant aux critères de filtre.
Il faudra alors récupérer la valeur via'@odata.count'
. - Le paramètre
$skip
permet de mentionner le nombre d’enregistrements à sauter qui ne seront par conséquent pas inclus dans le résultat.
Exemple JavaScript
Récupérer le nombre de comptes de charge du plan comptable (comptes de classe 6) :
$count
est forcé àtrue
pour que le total d’enregistrements répondant au filtre$filter
soit disponible dans'@odata.count'
- Dans cet appel
$top
est forcé à 0 puisqu’on ne souhaite pas récupérer les enregistrements mais juste le total, inutile donc que le serveur renvoie des données autre que ce total. '@odata.count'
renvoie le total réel calculé côté serveur, ce total peut donc être supérieur à la limite des 2000 enregistrements que l’API renverra à chaque appel.
var options=[];
options["$filter"] = "startswith(numero,'6') and type eq 'Detail'";
options["$top"] = "0";
options["$count"] = true;
Maintenant que nous connaissons le total d’enregistrements ('@odata.count'
) et le nombre d’enregistrements (par défaut 2000), nous pouvons gérer la pagination.
Il suffira d’incrémenter à chaque appel $skip
avec le nombre total d’enregistrements déjà lus.
Exemple
- Le total est de 257 enregistrements,
- le maximum par appel est forcé à 100 enregistrements via
$top:100
, - le premier appel se fait avec
$skip:0
, les enregistrements de 1 à 100 sont retournés, - le second appel se fait avec
$skip:100
, les enregistrements de 101 à 200 sont retournés, - le troisième se fait avec
$skip:200
, les enregistrements de 201 à 257 sont retournés, - pas d’appels supplémentaires nécessaires,
$skip+100
étant maintenant supérieur au total 257.
En comparant le total récupéré via'@odata.count'
et la valeur courante affectée à$skip
il est facilement possible de désactiver ou activer des boutons Précédent et Suivant et d’associer à chacun de ces boutons un traitement pour augmenter le compteur passé à$skip
ou le diminuer puis appeler de nouveau la requête.
Notes importantes
- Si
$orderby
est défini pour trier le résultat, il s’applique toujours côté serveur et donc s’applique à la totalité du résultat et non seulement au premier lot d’enregistrements retournés par l’API. - Par contre si vous appliquez dans votre application un tri sur le résultat ce tri sera local à ce résultat.
Si par exemple vous récupérez les 100 factures les plus récentes en estimant que ce nombre est suffisant pour l’utilisateur, si ensuite vous appliquez un tri sur la collection, vous ne triez que les 100 enregistrements récupérés à l’inverse du$orderby
qui s’applique à la totalité du résultat côté serveur. - Remarque identique si vous appliquez un filtre sur le résultat, le filtre ne s’appliquera qu’au 100 enregistrements à l’inverse de
$filter
qui s’applique à la totalité des données côté serveur.
Recommandations
Dossier de tests avec suffisamment de données
Nous vous recommandons d’utiliser un dossier avec suffisamment de données pour ne pas être piégés si pour certaines requêtes le seuil des 2000 enregistrements n’est pas atteint.
Par exemple, si vous testez avec une société de démonstration Sage Business Cloud Comptabilité, comme ce dossier risque d’avoir un nombre de clients, fournisseurs, écritures inférieur à 2000, vous pourriez croire, si vous avez omis de gérer la pagination, que votre développement est correct.
Mais une fois mise en production, votre application utilisée avec un dossier contenant des données réelles avec plus de 2000 clients, ou fournisseurs ou écritures, empêcherait l’utilisateur d’accéder aux données au delà des 2000 premiers enregistrements.
Ou simuler la limite à moins de 2000 enregistrements
Une astuce consiste aussi à définir un paramètre global nommé par exemple maxPaginate
avec une valeur inférieure au nombre d’enregistrements moyens de votre dossier de tests.
Par exemple vous pouvez affecter maxPaginate
avec la valeur 10.
Puis lors de vos requêtes, affectez toujours le paramètre $top
avec maxPaginate
, ainsi vos requêtes ne renverront que le nombre maximal mentionné dans maxPaginate
.
Vous serez alors plus rapidement alertés lors de la conception de votre application que vous devez gérer la pagination pour permettre d’accéder à l’ensemble des données et non à une vue tronquée des données.
Une fois la pagination testée et votre développement opérationnel, vous pourrez affecter maxPaginate
avec la valeur 2000 et votre développement continuera à fonctionner.
Paramètre | Type | Description |
---|---|---|
$top | integer | Nombre d’éléments à retourner dans la réponse |
Exemple
L’exemple ci-dessous permet de simuler que l’API va renvoyer un maximum de 10 enregistrements au lieu de la limite standard de 2000 enregistrements.
Bien évidemment, pour tester la pagination dans ce contexte, il faudra que $skip
soit aussi incrémenté par pas de 10 et non par pas de 2000.
L’exemple illustre l’appel à la première page avec $skip
affecté à 0. Pour les pages suivantes, il faudra incrémenter cptPage
de 1 à chaque fois pour que $skip
contienne 10 puis 20, etc. et non 2000 puis 4000, etc.
var options=[];
var maxPaginate = 10;
var cptPage = 0;
options["$select"] = "intitule,numero"
options["$orderby"] = "numero"
options["$top"] = maxPaginate;
options["$skip"] = cptPage*maxPaginate;
Ecritures et registres de taxes
La récupération des registres de taxes via un $expand=registreTaxes
depuis la ressource ecritures
est très couteuse en temps de traitement en raison d’une contrainte des outils appelés par l’API.
De plus il est inutile de lire pour chaque ligne d’écritures le registre de taxes puisqu’il est commun à toutes les lignes d’une même pièce comptable.
La recommandation est de lire au préalable les écritures, de les stocker dans une table d’une base de données puis, uniquement une fois pour chaque pièce de l’écriture, rechercher le registre de taxes via /ecritures('{Id}')/registreTaxes
en passant dans le paramètre {Id}
l’id de la première ligne d’écriture de chaque pièce.
Astuce pour trouver dans une table SQL la première ligne de chaque pièce comptable :
WITH one_line_by_documentNumber AS (
SELECT
*,
ROW_NUMBER() OVER(PARTITION BY DocumentNumber ORDER BY Id) AS row_number
FROM AccountingEntry
)
SELECT
*
FROM one_line_by_documentNumber
WHERE row_number = 1;
Autre solution, laisser le $expand=registreTaxes
mais réduire la pagination, par exemple avec un $top=200
pour éviter les expirations de délai avec la valeur par défaut de 2000.
Boucler pour récupérer tous les enregistrements
Pour certains usages, il peut être nécessaire de récupérer la totalité des enregistrements ou tout du moins plus que la limite de 2000 enregistrements renvoyés par appel.
Ce peut être le cas lorsque l’on sait que le nombre total d’enregistrements restera raisonnable mais néanmoins risque de dépasser 2000.
A l’exemple d’un plan comptable des comptes généraux que l’on souhaiterait récupérer en totalité pour afficher le plan comptable sur une seule page.
Dans ce cas bien évidemment il est possible de boucler pour récupérer des lots de 2000 enregistrements à chaque itération et stocker en mémoire dans une collection le nombre total des enregistrements.
// Crée un objet JObject à partir de la réponse d'une requête.
JObject GetJSONResult(HttpResponseMessage message)
{
string response = message.Content.ReadAsStringAsync().Result;
return string.IsNullOrEmpty(response) ? new JObject() : JObject.Parse(response);
}
string token = "{token}";
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
const string API_URL = "{apiUrl}";
string companyId = "{datasetId}";
// Renseignez ici le nom de la ressource souhaitée.
string resource = "comptes";
string uriBase = string.Concat(API_URL, '/', companyId, '/', resource);
// Récupération du NOMBRE de comptes 'Detail' et actifs.
Dictionary<string, string> optionsCount = new Dictionary<string, string>();
string filter = "type eq 'Detail' and sommeil eq false";
optionsCount.Add("$count", "true");
optionsCount.Add("$filter", filter);
optionsCount.Add("$top", "0");
// Création de l'adresse et exécution de la requête.
string uriCount = (null != optionsCount) ? Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(uriBase, optionsCount) : uriBase;
HttpResponseMessage message = client.GetAsync(uriCount).Result;
string odataCount = GetJSONResult(message)["@odata.count"].ToString();
int accountNumber = Int32.Parse(odataCount);
// Récupération des COMPTES 'Detail' et actifs.
Dictionary<string, string> options = new Dictionary<string, string>();
options.Add("$select", "numero,intitule,id");
options.Add("$filter", filter);
options.Add("$skip", "0");
List<JToken> results = new List<JToken>();
int skip = 0;
// Boucle permettant la récupération des éléments d'une ressource.
for (int count = 0; count < accountNumber; skip += 2000)
{
// A chaque itération, le $skip augmente de 2000.
options["$skip"] = skip.ToString();
// Éléments du dictionnaire ajoutés à l'adresse.
string uriAccounts = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(uriBase, options);
// Exécution de la requête.
message = client.GetAsync(uriAccounts).Result;
JObject result = GetJSONResult(message);
// Ajout du nombre de comptes obtenus lors de cette itération.
count += result["value"].Count();
// Ajout à la liste contenant les résultats de chaque requête.
results.AddRange(result["value"]);
}
ViewBag.Results = results;
Attention ! Il vous appartient d’estimer l’approche selon les ressources à lire.
Par exemple pour choisir un compte auxiliaire client dans une liste déroulante, il faudra plutôt une approche liste déroulante avec champ de saisie de filtre qui provoquera un appel serveur à chaque changement du filtre saisi par l’utilisateur pour récupérer au maximum les 50 ou 100 premiers enregistrements répondant aux critères de filtre.