Fermer

juillet 9, 2020

Meilleures pratiques pour la récupération de données dans Telerik Reporting9 minutes de lecture


Chaque rapport a besoin de données pour être mis en ligne. Découvrez comment configurer les sources de données dans votre rapport pour garantir les meilleures performances et découvrez quelques astuces qui vous aideront dans la création de rapports.

La création d'un rapport signifie essentiellement obtenir un bloc de données, l'organiser selon un ensemble de règles donné et le présenter de manière visuelle. Les rapports font déjà partie intégrante des processus opérationnels actuels. Qu'il s'agisse d'une simple facture ou d'un tableau de vente élaboré, pratiquement chaque collection de faits et de chiffres peut être représentée de manière visuellement attrayante. Mais avant de passer à la partie visuelle des choses, nous devons nous assurer que la base de nos rapports – les données – est récupérée et conservée de la meilleure façon possible.

L'équipe de Telerik Reporting comprend la nécessité d'un moyen fiable et simple d'accéder aux données dont votre rapport a besoin. C'est pourquoi j'expliquerai ci-dessous plus en détail les différentes approches de récupération des données et leur variété— des connexions aux bases de données en passant par les collections d'objets métier jusqu'aux entités au format JSON fournies par un service Web.

Le contenu ci-dessous suppose que vous connaissez déjà Telerik Reporting, mais si vous ne l'êtes pas, veuillez d'abord consulter nos articles Mise en route . Je vais aller un peu plus technique et fournir des détails et des exemples sous le capot car je crois qu'une meilleure compréhension de la façon dont les données sont obtenues et traitées par le moteur de reporting vous aidera à créer vos rapports plus rapidement et à les utiliser sans effort dans diverses applications. .

Connexion aux données

Connexions à la base de données

Telerik Reports peut se connecter aux données de diverses sources, mais la plus utilisée est la base de données. Le composant qui effectue la connexion réelle entre le rapport et la base de données est nommé SqlDataSource. Son assistant fournit une interface simple et intuitive qui permet de créer et de tester une connexion à la base de données sélectionnée en quelques secondes.

Le SqlDataSource est indépendant de la base de données, c'est-à-dire qu'il n'a pas de routines dédiées pour les bases de données MSSQL, Oracle ou MySQL. Au lieu de cela, il relaie les commandes et les paramètres via une interface commune, fournie par la classe DbProviderFactory . Lors de la configuration de la SqlDataSource, vous sélectionnez un nom de fournisseur, qui est utilisé par l'instance DbProviderFactories pour créer la DbProviderFactory correspondante. Les DbProviders disponibles sont répertoriés dans un fichier de configuration — il peut s'agir du fichier machine.config commun ou simplement du fichier de configuration de l'application .NET. Voici à quoi ressemble la liste avec les entrées DbProviderFactory dans machine.config:


<add name = "Oracle Data Provider for .NET" invariant = "Oracle.DataAccess.Client"
description = "Oracle Data Provider for. NET "type =" Oracle.DataAccess.Client.OracleClientFactory, Oracle.DataAccess, Version = 2.112.3.0, Culture = neutral, PublicKeyToken = 89b483f429c47342 "/>
<add name =" Odbc Data Provider "invariant =" System. Data.Odbc "description =". Net Framework Data Provider for Odbc "
type =" System.Data.Odbc.OdbcFactory, System.Data, Version = 2.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089 "/>
<add name = "OleDb Data Provider" invariant = "System.Data.OleDb"
description = ". Net Framework Data Provider for OleDb" type = "System.Data.OleDb.OleDbFactory, System.Data, Version = 2.0.0.0, Culture = neutre, PublicKeyToken = b77a5c561934e089 "/>
<add name =" SqlClient Data Provider "invariant =" System.Data.SqlClient "
description =". Net Framework Data Provider for SqlServer "type =" System.Data.SqlClient.SqlClientFactory, System.Data, Version = 2.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089 "/>
<add name =" MySQL Data Provider "invariant = "MySql.Data.MySqlClient"
description = ". Net Framework Data Provider for MySQL" type = "MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version = 8.0.13.0, Culture = neutral, PublicKeyToken = c5687fc88969cc "/>

Généralement, chaque fournisseur de base de données ou pilote installé sur la machine ajoute son entrée ici. De cette façon, le fournisseur est visible pour les applications qui ont besoin d'accéder à une base de données dans une approche plus générique. Ainsi, lorsque vous choisissez le fournisseur de données MySQL dans la zone de liste déroulante de l'assistant SqlDataSource, l'assistant stocke le nom invariant de ce fournisseur et l'utilise pour créer les implémentations spécifiques de DbConnection, DbCommand et le reste des classes nécessaires pour accéder à cette base de données

L'utilisation d'une telle approche nous permet de nous connecter à pratiquement n'importe quelle base de données qui possède un fournisseur de données ODBC, OleDB ou ADO.NET. Il n'est même pas nécessaire de l'installer sur la machine – il suffit de l'enregistrer dans la section du fichier de configuration pour les applications qui utilisent .NET Framework. Cependant, dans .NET Standard, la classe DbProviderFactories n'existe pas et ses entrées doivent être enregistrées manuellement, comme expliqué dans cet article de la base de connaissances .

Souvent, différents fournisseurs de données d'une base de données coexistent sur la même machine. Vous pouvez avoir un pilote ODBC et un fournisseur ADO.NET pour MSSQL Server. Il n'y a pas de règle générale qui précise laquelle vous préférez. Par exemple, le fournisseur System.Data.SqlClient est conçu pour fournir des performances optimales lors de la connexion à SQL Server 2000 et versions ultérieures, mais le pilote ODBC peut fonctionner avec des bases de données plus anciennes. Dans certains cas, les nouvelles versions de pilotes manquent de certaines fonctionnalités nécessaires au fonctionnement de notre application. C'est le cas du fournisseur MySql.Net Connector qui n'a pas implémenté une certaine méthode nommée CreateCommandBuilder, qui est utilisée par notre moteur pour obtenir le schéma de la base de données. Dans les versions antérieures à 6.4.6, la méthode était présente, puis elle est devenue obsolète et à partir de la version 8.0, elle a été réintroduite.

Le meilleur conseil dans une telle situation serait de mesurer les capacités et les performances de chaque fournisseur de base de données disponible pour s'assurer il convient à vos besoins. Bien sûr, il est toujours préférable d'avoir des options parmi lesquelles choisir et c'est pourquoi notre produit est livré avec plus de 20 pilotes ODBC qui peuvent être utilisés pour se connecter aux bases de données les plus populaires.

Telerik Reporting en propose quelques-uns. d'autres composants qui permettent de se connecter à une base de données, mais ils ciblent une base de données ou une technologie spécifique et ne sont pas aussi populaires que SqlDataSource. Ces composants incluent:

  • CubeDataSource – utilisé pour se connecter à un modèle de données de cube OLAP via une requête MDX;

  • OpenClientDataSource – spécialement conçu pour être utilisé avec les procédures ABE OpenEdgeAppServer;

  • OpenAccessDataSource – fournit des données via un modèle de données OpenAccess.

  • EntityDataSource – il n'est pas obligatoire d'avoir une vraie base de données comme stockage de données pour ce composant – permet de se connecter à un ObjectContext ou DbContext existant et d'accéder à son modèle de données. L'implémentation actuelle fonctionne avec .NET Framework et ne peut pas être utilisée dans des projets qui utilisent EntityFrameworkCore.

Bien que ces composants répondent à des besoins métier particuliers, ils partagent une logique commune avec SqlDataSource et la plupart des conseils et suggestions fournis ici sont valables pour eux en tant que

Connexions aux services Web

Bien que les scénarios où les données sont obtenues à partir d'une base de données soient encore les plus courants, les cas où la base de données n'est pas directement accessible ou même manquante sont de plus en plus utilisés. Dans ces configurations, un service Web accepte les demandes et renvoie des objets de données, au format JSON, XML ou texte. Cette approche présente des avantages importants en termes de sécurité et de configuration, et l'utilisation d'une interface standardisée pour accéder aux données permet une adoption plus facile. Telerik Reporting possède un composant dédié à cet effet: le composant WebServiceDataSource .

Comme pour les autres sources de données, il peut être configuré via son assistant pour se connecter à un service Web, s'authentifier si nécessaire, demander les données nécessaires et analyser la réponse à un ensemble d'objets de données. La configuration WebServiceDataSourceconfiguration vous permet d'affiner l'URL et le corps de la demande à l'aide de jetons qui seront remplacés au moment de l'exécution par les valeurs réelles des paramètres de rapport ou des chaînes statiques.

À titre d'exemple concret, examinons le site Web restcountries.eu qui fournit des statistiques pour tous les pays du monde via l'API RESTful. Le point de terminaison qui renvoie tous les pays est https://restcountries.eu/rest/v2/all mais si notre rapport doit montrer un sous-ensemble de pays par prénom, le point de terminaison que nous devons utiliser est https://restcountries.eu/rest/v2/name/ {name} . Dans ce cas, WebServiceDataSource’sServiceUrl ressemblerait à ceci: https://restcountries.eu/rest/v2/name/@name où le caractère "@" indique un jeton qui sera remplacé. La substitution de jetons réelle est configurée dans la collection de paramètres WebServiceDataSource, et dans le cas actuel, nous aurons un paramètre en ligne qui obtient sa valeur à partir d'un paramètre de rapport:

 Connexions aux services Web — Telerik Reporting "title =" Connexions aux services Web— Telerik Reporting "/></p data-recalc-dims=

Comme vous pouvez le voir, il est important de choisir le bon point de terminaison et de demander uniquement les informations nécessaires plutôt que de récupérer les données pour tous les pays et de les filtrer plus tard côté client.

Le composant fournit également suffisamment des options d'authentification pour couvrir des scénarios simples et plus complexes d'octroi de l'accès au service Web. Le type et l'encodage de la réponse sont également configurables. La propriété DataSelector est très utile lorsque les données retournées doivent être filtrées selon des critères spécifiques, mais je couvrirai en détail dans la section "Organisation des données" ci-dessous.

Connexions de données statiques

Dans les scénarios où les données réelles ne sont pas disponibles ou ne peuvent pas être utilisés en raison d'une connexion lente ou d'un accès mesuré, les composants CSVDataSource et JSONDataSource s'avèrent très efficaces. Ils fournissent une interface simple aux données qui peuvent être stockées dans un fichier (local ou distant) ou intégrées dans la définition du rapport. Comme les noms le suggèrent, l'entrée de ces sources de données est des données textuelles, organisées en valeurs séparées par des virgules ou au format JSON. Ces composants sont parfaits pour des conceptions de rapports de preuve de concept rapides et même pour des tests à haute charge.

Notre équipe les utilise largement pour la communication de support où les utilisateurs nous envoient des rapports que nous devons tester sans avoir accès à la base de données réelle. Lorsque la définition du rapport est donnée et quelques minutes avec Excel, cela peut entraîner un ensemble de données de maquette avec des milliers d'enregistrements. Par conséquent, je suggère fortement d'utiliser ces sources de données dans les étapes précédentes du processus de conception de rapport au lieu d'utiliser des données réelles à partir d'un serveur de base de données de production ou d'un service Web.

Connexions Business Objects

Si l'une des approches d'accès aux données répertoriées ne convient pas Dans le scénario actuel, vous pouvez toujours recourir à la création de l'ensemble de données par programme et à sa transmission au rapport via le composant ObjectDataSource . Il accepte une classe qui implémente IEnumerable, IListSource ou IDbDataAdapterinterfaces, ou une méthode qui renvoie une instance de ces interfaces, ce qui la rend applicable à pratiquement n'importe quelle collection d'objets. La configuration de l'ObjectDataSource est facile, car elle n'a besoin que de deux arguments: le nom de la classe de collection et le nom de la méthode qui renvoie les données. L'assistant correspondant se charge de guider l'utilisateur à travers toutes les étapes nécessaires, y compris la configuration des paramètres de la méthode de récupération des données, le cas échéant. Au moment de l'exécution, lorsqu'un élément de données comme Table ou Graphique doit obtenir ses données à partir de l'ObjectDataSource, le moteur instanciera la classe de collecte et appellera la méthode de récupération des données, fournissant des valeurs pour ses paramètres.

Bien que très puissant, ce composant a certaines exigences qui doivent être prises en compte avant de l'utiliser dans un rapport:

La première concerne les dépendances de la classe de collection. Idéalement, les objets métier devraient être déclarés dans un assemblage distinct qui doit être enregistré dans le fichier de configuration d'application sous la section . Dans le cas où l'assembly de classe métier fait référence à d'autres assemblys qui ne font pas partie du framework .NET, ces assemblys doivent être copiés dans le répertoire d'application ou enregistrés dans GAC afin d'être résolus par le runtime lors du chargement des classes d'objets métier. Ces problèmes ne sont pas faciles à résoudre, car le runtime .NET ne fournit pas suffisamment d'informations sur ce qui s'est passé lors de la tentative de chargement de l'assembly. L'outil que nous utilisons dans ces scénarios est la visionneuse du journal de liaison d'assemblage de Microsoft nommée Fuslogvw . Comme sa description le suggère, il enregistre les liaisons d'assembly à partir des applications .NET en cours d'exécution. L'inspection des échecs de liaison nous permet de déterminer quels assemblys ne parviennent pas à charger dans une application particulière, c'est-à-dire. notre projet de rapports.

La deuxième chose à considérer lors de l'utilisation du composant ObjectDataSource est liée à l'architecture .NET. De par leur conception, les assemblys ciblant le .NET Framework ne peuvent pas référencer les assemblys qui ciblent .NET Core. Par conséquent, si vos objets métier résident dans un assembly .NET Core, ils ne peuvent pas être référencés par un projet de rapports qui cible .NET Framework. Dans un tel scénario, il existe deux options: pour changer le cadre cible du projet de rapports de .NET Framework vers .NET Core ou pour changer le cadre cible de l'assembly d'objets métier de .NET Core vers.NET Standard 2.0. Si, pour une raison quelconque, les deux options ne sont pas réalisables, les objets métier doivent être extraits dans un autre assemblage .NET Standard référencé par les deux projets.

Comme vous pouvez le voir, notre produit propose de nombreuses options de connexion à une source de données et en extraire des données. Le choix et la configuration de la source de données de rapport pour votre rapport peut sembler une tâche facile, mais c'est un facteur important qui peut affecter les performances du rapport et dégrader l'expérience utilisateur de l'application de création de rapports.

Organisation des données

Une fois que nous avons établi une connexion à la base de données, nous devons demander les données et les façonner de la manière la plus pratique à utiliser dans un rapport. Le processus d'organisation des données est vital à la fois pour le processus de conception du rapport et pour les performances du rapport et peut être divisé en trois parties.

Découpage

Indépendamment de la façon dont nous construisons notre requête de base de données, nous pouvons parfois nous retrouver avec une SqlDataSource récupère un jeu de résultats avec 30 colonnes dans un élément de tableau qui n'en utilise que 5. La recommandation générale applicable à toutes les sources de données est «uniquement ce qui est nécessaire», c'est-à-dire que les sources de données doivent récupérer le minimum des champs de données nécessaires pour le rapport, en particulier lorsque l'ensemble de résultats contient de nombreux enregistrements. Illustrons-le par un exemple: notre rapport doit montrer le prix réel des articles que nous avons vendus l'année dernière. Le prix réel est égal au prix de base moins la remise. Notre requête pourrait ressembler à ceci:

Sélectionnez base_price, discount, base_price - discount as actual_price from Sales

En supposant que le type de champ Money dans MSSQL prend 8 octets pour un million d'enregistrements dans notre table Sales nous aurons une surcharge de ~ 8 Mo uniquement à partir d'une seule colonne de notre ensemble de données. Bien sûr, dans un scénario réel, nous aurons une compression du trafic et d'autres optimisations (et 8 Mo, ce n'est pas 8 millions d'octets, non), mais cette colonne est toujours redondante. Dans ce scénario, j'utiliserais la propriété CalculatedFields de SqlDataSource et j'écrirais une expression simple qui soustrait les prix pour moi.

Les mêmes considérations sont valables pour les autres sources de données. Par exemple, ObjectDataSource utilise la réflexion pour parcourir les objets métier et rechercher les propriétés utilisées dans le rapport par leur nom. Si les modèles que nous utilisons ont beaucoup de propriétés, leur traversée prendrait plus de temps, ce qui entraînerait de mauvaises performances. Dans ce cas, il est généralement préférable d'avoir un ensemble de classes de niveau intermédiaire qui ne contiennent que les propriétés utilisées dans le rapport.

Filtrage

Chaque élément de données ou groupe du rapport peut définir des règles pour filtrer ses données avant c'est traité. Ceci est utile dans quelques scénarios, mais généralement les données peuvent et doivent être filtrées côté serveur. Comme expliqué précédemment, moins les données sont extraites de la source de données, meilleures sont les performances. Presque tous les composants de source de données permettent de filtrer leurs données avant leur envoi aux éléments de données. La méthode recommandée pour appliquer le filtrage à la source de données consiste à utiliser les paramètres de rapport.

Les paramètres du rapport sont des expressions, évaluées avant le traitement du rapport. Selon leur configuration, ils peuvent être affichés dans la visionneuse de rapports, fournissant une interface utilisateur pour sélectionner ou entrer une valeur. Quelle que soit leur visibilité, ils doivent avoir une valeur valide avant le traitement du rapport. Cette valeur peut être transmise à la clause WHERE dans la requête SqlDataSource, utilisée dans l'expression JSONPath du DataSelector dans WebServiceDataSource, ou envoyée en tant que paramètre à la récupération de données d'ObjectDataSource méthode. Dans tous les cas, ils doivent être utilisés pour réduire la quantité de données extraites de la source de données lorsque cela est possible. Dans le cas du composant WebServiceDataSource, le DataSelector est appliqué côté client une fois que les données sont extraites du serveur. Par conséquent, si le service Web fournit un point de terminaison qui pourrait renvoyer uniquement les données nécessaires, il est toujours préférable de l'utiliser plutôt que d'appliquer un DataSelector par la suite.

Un bon exemple de l'importance du filtrage est un scénario avec un client qui a utilisé notre produit pour créer des rapports comptables. Son rapport comportait deux modes – détaillé et résumé, sélectionnés via un paramètre de rapport. En mode «détaillé», il imprimait chaque enregistrement du jeu de résultats – des centaines de milliers d'enregistrements – ce qui prenait un temps raisonnable. En mode «résumé», le rapport ne devait imprimer que quelques lignes avec les chiffres du résumé. Notre client a utilisé la même requête pour les deux modes et, naturellement, il a été surpris que les performances en mode «résumé» ne soient pas si bonnes que pour quelques lignes dans le document résultant. Nous lui avons expliqué que le deuxième scénario ne ferait gagner que le temps nécessaire au rendu des pages, mais la récupération, le traitement et le regroupement des données sont les mêmes dans les deux cas. Dans de tels cas, la solution recommandée serait de modifier la requête exécutée par SqlDataSource, en déplaçant le groupe de données sur le serveur de base de données. Cela peut se faire soit via des liaisons, soit en créant un nouveau rapport avec une source de données et une mise en page spécialement conçues pour le mode «résumé».

Notre client a opté pour la solution en utilisant un élément SubReport avec un rapport différent pour chaque mode et était satisfait du gain de performances.

Commande

Par «commande», je ne veux pas dire trier les données selon leurs valeurs, mais personnaliser la disposition des colonnes et des lignes dans le jeu de résultats renvoyé par la source de données. En général, il doit être aussi similaire que possible à la présentation qui sera utilisée dans le rapport. Mais d'un autre côté, si la même source de données est réutilisée par un autre élément de données, la même mise en page peut s'avérer impraticable. Envisagez d'avoir l'ensemble de résultats suivant qui représente les ventes de voitures par an:

 Exemple de commande 1 - Telerik Reporting "title =" Exemple de commande 1 - Telerik Reporting "/></p data-recalc-dims=

Une telle disposition est parfaitement logique si nous devons montrer dans un tableau, en conservant le même aspect – les colonnes représentent les années, les lignes – les types de voitures. Mais si nous voulons réutiliser le même ensemble de données dans un élément de graphique et afficher un graphique en courbes qui a l'année sur son axe X et les ventes sur son axe Y, ce serait un vrai défi. Dans ce cas, nous devons réorganiser la requête pour que la mise en page ressemble à ceci:

 Exemple de commande 2 - Telerik Reporting "title =" Exemple de commande 2 - Telerik Reporting "/ ></p data-recalc-dims=

Maintenant, les données sont bien ordonnées et peuvent être affichées dans les éléments de tableau et de graphique, mais ne correspondent toujours pas à tous les scénarios possibles – par exemple, que se passe-t-il si nous nous attendons à avoir plus de types de voitures? Dans ce scénario, la mise en page la plus universelle serait celle-ci:

 Exemple de commande 3 — Telerik Reporting "title =" Exemple de commande 3 — Telerik Reporting "/></p data-recalc-dims=

Cette représentation des données peut être facilement groupée et affichée dans un tableau ou un élément de graphique, quels que soient les types de voitures et les années. Le seul problème ici est que le jeu de résultats est maintenant beaucoup plus grand que les précédents et aurait un impact sur les performances des jeux de données plus volumineux. Comme vous pouvez le voir, il n'y a pas de solution universelle— un scénario bien adapté à un élément de données peut ne pas être applicable à un autre. Idéalement, chaque source de données doit être configurée spécifiquement pour son élément de données correspondant, mais comme inconvénient, les données ne seront pas réutilisées entre les éléments et cela entraînerait également une

Conclusion

Sur ces quelques pages, j'ai expliqué en détail comment le mécanisme de connexion à la base de données w et quelles sont les bonnes façons de récupérer les données du rapport à partir de diverses sources de données. Bien que la création de rapports soit assistée par des assistants, des éléments d'aperçu en direct et des onglets de ruban de contexte, l'étape initiale qui configure les sources de données est la base qui fournit des performances solides et un processus de conception sans effort. Étant donné que les exemples ci-dessus sont basés sur les problèmes rencontrés par certains de nos utilisateurs lors de la configuration de la récupération des données dans leurs rapports, j'espère que connaître les détails du processus vous aidera à sélectionner l'option qui offre les meilleures performances dans n'importe quel scénario.

Commencez dès aujourd'hui

Faites l'essai de nos outils de création de rapports — essayez la dernière version de Telerik Reporting aujourd'hui avec un essai GRATUIT.

Telerik Reporting est un outil complet, léger et facile à utiliser. à utiliser et puissant outil de génération de rapports .NET pour les applications Web et de bureau qui prend en charge: ASP.NET Core, Blazor, ASP.NET MVC, ASP.NET AJAX, HTML5, Angular, React, Vue, WPF, WinForms. Avec notre outil de création de rapports, tout développeur ou utilisateur de génération de rapports pourra créer, styliser, visualiser et exporter des rapports riches, interactifs et réutilisables pour présenter de manière attrayante et magnifique toutes les données analytiques et commerciales. Les rapports peuvent être ajoutés à n'importe quelle application Web et de bureau via les commandes de la visionneuse de rapports. Les rapports prêts peuvent être exportés vers plus de 15 formats.

Telerik Report Server est une solution de gestion de rapport de bout en bout qui aide à transformer les données brutes en informations commerciales exploitables, puis stocke et distribue ces informations au sein de l'entreprise. . Report Server est fourni en tant qu'application Web légère pouvant être téléchargée et exécutée sur le serveur Web local de l'utilisateur. Une fois installé, il permet à l'entreprise de stocker tous les rapports dans un seul référentiel côté serveur. Il est livré avec une variété de fonctionnalités métier telles que la planification des rapports, les alertes de données et les notifications par e-mail, la gestion des utilisateurs, l'authentification et l'autorisation et les fonctionnalités de rebranding.

Tried Telerik DevCraft?

Vous pouvez choisir Telerik Reporting et Telerik Report Server en tant que produits individuels ou profitez-en dans le cadre des grands ensembles Telerik DevCraft .

Telerik DevCraft est la meilleure collection d'outils de développement de logiciels à travers les technologies .NET et JavaScript, qui comprend des fonctionnalités modernes et des composants d'interface utilisateur riches et conçus par des professionnels pour les applications Web, de bureau et mobiles, les solutions de création de rapports et de gestion de rapports, les bibliothèques de traitement de documents, les outils de test et de simulation automatisés des suites d'interface utilisateur Telerik et Kendo. DevCraft vous fournira tout ce dont vous avez besoin pour fournir des applications exceptionnelles en moins de temps et avec moins d'efforts. Avec le soutien de notre équipe d'assistance légendaire, qui se compose des développeurs qui créent les produits, et d'une tonne de ressources et de formations, vous pouvez être assuré que vous avez un partenaire stable sur lequel compter pour vos défis quotidiens tout au long de votre parcours de développement logiciel. [19659065]



Source link

0 Partages