Récupération des données

Maintenant que vous avez créé et peuplé votre base de données, parlons des différentes méthodes pour récupérer des données pour votre application, et construisons la page de vue d'ensemble de votre tableau de bord.

Choisir comment récupérer les données

Couche API

Les API servent d'intermédiaire entre le code de votre application et la base de données. Voici quelques cas où vous pourriez utiliser une API :

  • Si vous utilisez des services tiers qui fournissent une API.
  • Si vous récupérez des données depuis le client, vous voulez avoir une couche API qui s'exécute sur le serveur pour éviter d'exposer les secrets de votre base de données au client.

Dans Next.js, vous pouvez créer des endpoints API en utilisant les Route Handlers.

Requêtes de base de données

Lorsque vous créez une application full-stack, vous devez également écrire une logique pour interagir avec votre base de données. Pour les bases de données relationnelles comme Postgres, vous pouvez le faire avec SQL ou avec un ORM.

Voici quelques cas où vous devez écrire des requêtes de base de données :

  • Lors de la création de vos endpoints API, vous devez écrire une logique pour interagir avec votre base de données.
  • Si vous utilisez les React Server Components (récupération de données côté serveur), vous pouvez contourner la couche API et interroger directement votre base de données sans risquer d'exposer vos secrets de base de données au client.

Apprenons-en plus sur les React Server Components.

Utilisation des composants serveur pour récupérer des données

Par défaut, les applications Next.js utilisent les React Server Components. La récupération de données avec les composants serveur est une approche relativement nouvelle qui présente plusieurs avantages :

  • Les composants serveur prennent en charge les Promises JavaScript, offrant une solution native pour les tâches asynchrones comme la récupération de données. Vous pouvez utiliser la syntaxe async/await sans avoir besoin de useEffect, useState ou d'autres bibliothèques de récupération de données.
  • Les composants serveur s'exécutent sur le serveur, vous pouvez donc garder les récupérations de données coûteuses et la logique sur le serveur, en envoyant uniquement le résultat au client.
  • Comme les composants serveur s'exécutent sur le serveur, vous pouvez interroger directement la base de données sans couche API supplémentaire. Cela vous évite d'écrire et de maintenir du code supplémentaire.

Utilisation de SQL

Pour votre application de tableau de bord, vous écrirez des requêtes de base de données en utilisant la bibliothèque postgres.js et SQL. Voici quelques raisons pour lesquelles nous utiliserons SQL :

  • SQL est la norme industrielle pour interroger les bases de données relationnelles (par exemple, les ORMs génèrent du SQL en arrière-plan).
  • Avoir une compréhension basique de SQL peut vous aider à comprendre les fondamentaux des bases de données relationnelles, vous permettant d'appliquer vos connaissances à d'autres outils.
  • SQL est polyvalent, vous permettant de récupérer et manipuler des données spécifiques.
  • La bibliothèque postgres.js offre une protection contre les injections SQL.

Ne vous inquiétez pas si vous n'avez jamais utilisé SQL auparavant - nous avons fourni les requêtes pour vous.

Allez dans /app/lib/data.ts. Ici, vous verrez que nous utilisons postgres. La fonction sql fonction vous permet d'interroger votre base de données :

/app/lib/data.ts
import postgres from 'postgres';
 
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
 
// ...

Vous pouvez appeler sql n'importe où sur le serveur, comme dans un composant serveur. Mais pour vous permettre de naviguer plus facilement entre les composants, nous avons gardé toutes les requêtes de données dans le fichier data.ts, et vous pouvez les importer dans les composants.

Remarque : Si vous avez utilisé votre propre fournisseur de base de données dans le chapitre 6, vous devrez mettre à jour les requêtes de base de données pour qu'elles fonctionnent avec votre fournisseur. Vous pouvez trouver les requêtes dans /app/lib/data.ts.

Récupération des données pour la page de vue d'ensemble du tableau de bord

Maintenant que vous comprenez les différentes méthodes de récupération de données, récupérons les données pour la page de vue d'ensemble du tableau de bord. Naviguez vers /app/dashboard/page.tsx, collez le code suivant et prenez le temps de l'explorer :

/app/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
 
export default async function Page() {
  return (
    <main>
      <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
        Dashboard
      </h1>
      <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
        {/* <Card title="Collected" value={totalPaidInvoices} type="collected" /> */}
        {/* <Card title="Pending" value={totalPendingInvoices} type="pending" /> */}
        {/* <Card title="Total Invoices" value={numberOfInvoices} type="invoices" /> */}
        {/* <Card
          title="Total Customers"
          value={numberOfCustomers}
          type="customers"
        /> */}
      </div>
      <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
        {/* <RevenueChart revenue={revenue}  /> */}
        {/* <LatestInvoices latestInvoices={latestInvoices} /> */}
      </div>
    </main>
  );
}

Le code ci-dessus est intentionnellement commenté. Nous allons maintenant commencer à examiner chaque partie.

  • La page est un composant serveur async. Cela vous permet d'utiliser await pour récupérer des données.
  • Il y a aussi 3 composants qui reçoivent des données : <Card>, <RevenueChart>, et <LatestInvoices>. Ils sont actuellement commentés et non encore implémentés.

Récupération des données pour <RevenueChart/>

Pour récupérer les données pour le composant <RevenueChart/>, importez la fonction fetchRevenue depuis data.ts et appelez-la dans votre composant :

/app/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue } from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  // ...
}

Ensuite, faisons ce qui suit :

  1. Décommentez le composant <RevenueChart/>.
  2. Naviguez vers le fichier du composant (/app/ui/dashboard/revenue-chart.tsx) et décommentez le code à l'intérieur.
  3. Vérifiez localhost:3000 et vous devriez voir un graphique qui utilise les données revenue.
Graphique des revenus montrant le revenu total des 12 derniers mois

Continuons en important plus de données et en les affichant sur le tableau de bord.

Récupération des données pour <LatestInvoices/>

Pour le composant <LatestInvoices />, nous devons obtenir les 5 dernières factures, triées par date.

Vous pourriez récupérer toutes les factures et les trier en utilisant JavaScript. Ce n'est pas un problème car nos données sont petites, mais à mesure que votre application grandit, cela peut considérablement augmenter la quantité de données transférées à chaque requête et le JavaScript nécessaire pour les trier.

Au lieu de trier les dernières factures en mémoire, vous pouvez utiliser une requête SQL pour récupérer uniquement les 5 dernières factures. Par exemple, voici la requête SQL depuis votre fichier data.ts :

/app/lib/data.ts
// Récupère les 5 dernières factures, triées par date
const data = await sql<LatestInvoiceRaw[]>`
  SELECT invoices.amount, customers.name, customers.image_url, customers.email
  FROM invoices
  JOIN customers ON invoices.customer_id = customers.id
  ORDER BY invoices.date DESC
  LIMIT 5`;

Dans votre page, importez la fonction fetchLatestInvoices :

/app/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue, fetchLatestInvoices } from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  const latestInvoices = await fetchLatestInvoices();
  // ...
}

Ensuite, décommentez le composant <LatestInvoices />. Vous devrez également décommenter le code pertinent dans le composant <LatestInvoices /> lui-même, situé dans /app/ui/dashboard/latest-invoices.

Si vous visitez votre localhost, vous devriez voir que seules les 5 dernières factures sont retournées par la base de données. J'espère que vous commencez à voir les avantages d'interroger directement votre base de données !

Composant des dernières factures à côté du graphique des revenus

Exercice : Récupérer les données pour les composants <Card>

Maintenant, c'est à vous de récupérer les données pour les composants <Card>. Les cartes afficheront les données suivantes :

  • Montant total des factures collectées.
  • Montant total des factures en attente.
  • Nombre total de factures.
  • Nombre total de clients.

Encore une fois, vous pourriez être tenté de récupérer toutes les factures et tous les clients, et d'utiliser JavaScript pour manipuler les données. Par exemple, vous pourriez utiliser Array.length pour obtenir le nombre total de factures et de clients :

const totalInvoices = allInvoices.length;
const totalCustomers = allCustomers.length;

Mais avec SQL, vous pouvez récupérer uniquement les données dont vous avez besoin. C'est un peu plus long que d'utiliser Array.length, mais cela signifie que moins de données doivent être transférées pendant la requête. Voici l'alternative SQL :

/app/lib/data.ts
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;

La fonction que vous devrez importer s'appelle fetchCardData. Vous devrez déstructurer les valeurs retournées par la fonction.

Indice :

  • Vérifiez les composants de carte pour voir quelles données ils nécessitent.
  • Vérifiez le fichier data.ts pour voir ce que retourne la fonction.

Une fois prêt, développez le toggle ci-dessous pour voir le code final :

Super ! Vous avez maintenant récupéré toutes les données pour la page de vue d'ensemble du tableau de bord. Votre page devrait ressembler à ceci :

Page de tableau de bord avec toutes les données récupérées

Cependant... il y a deux choses dont vous devez être conscient :

  1. Les requêtes de données se bloquent involontairement les unes les autres, créant une cascade de requêtes.
  2. Par défaut, Next.js pré-rend les routes pour améliorer les performances, c'est ce qu'on appelle le rendu statique. Donc si vos données changent, cela ne se reflétera pas dans votre tableau de bord.

Parlons du point 1 dans ce chapitre, puis examinons en détail le point 2 dans le chapitre suivant.

Que sont les cascades de requêtes ?

Une "cascade" fait référence à une séquence de requêtes réseau qui dépendent de l'achèvement des requêtes précédentes. Dans le cas de la récupération de données, chaque requête ne peut commencer que lorsque la requête précédente a retourné des données.

Diagramme montrant le temps avec récupération séquentielle et parallèle des données

Par exemple, nous devons attendre que fetchRevenue() s'exécute avant que fetchLatestInvoices() puisse commencer à s'exécuter, et ainsi de suite.

/app/dashboard/page.tsx
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // attend que fetchRevenue() se termine
const {
  numberOfInvoices,
  numberOfCustomers,
  totalPaidInvoices,
  totalPendingInvoices,
} = await fetchCardData(); // attend que fetchLatestInvoices() se termine

Ce modèle n'est pas nécessairement mauvais. Il peut y avoir des cas où vous voulez des cascades parce que vous voulez qu'une condition soit satisfaite avant de faire la prochaine requête. Par exemple, vous pourriez vouloir récupérer d'abord l'ID et les informations de profil d'un utilisateur. Une fois que vous avez l'ID, vous pourriez ensuite procéder à la récupération de sa liste d'amis. Dans ce cas, chaque requête dépend des données retournées par la requête précédente.

Cependant, ce comportement peut aussi être involontaire et impacter les performances.

Récupération parallèle des données

Une méthode courante pour éviter les cascades est de lancer toutes les requêtes de données en même temps - en parallèle.

En JavaScript, vous pouvez utiliser les fonctions Promise.all() ou Promise.allSettled() pour lancer toutes les promesses en même temps. Par exemple, dans data.ts, nous utilisons Promise.all() dans la fonction fetchCardData() :

/app/lib/data.ts
export async function fetchCardData() {
  try {
    const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
    const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
    const invoiceStatusPromise = sql`SELECT
         SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
         SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
         FROM invoices`;
 
    const data = await Promise.all([
      invoiceCountPromise,
      customerCountPromise,
      invoiceStatusPromise,
    ]);
    // ...
  }
}

En utilisant ce modèle, vous pouvez :

  • Commencer à exécuter toutes les récupérations de données en même temps, ce qui est plus rapide que d'attendre que chaque requête se termine dans une cascade.
  • Utiliser un modèle JavaScript natif qui peut être appliqué à n'importe quelle bibliothèque ou framework.

Cependant, il y a un inconvénient à ne compter que sur ce modèle JavaScript : que se passe-t-il si une requête de données est plus lente que toutes les autres ? Découvrons cela dans le prochain chapitre.