Datenabfrage

Nachdem Sie Ihre Datenbank erstellt und mit Daten gefüllt haben, wollen wir die verschiedenen Möglichkeiten besprechen, wie Sie Daten für Ihre Anwendung abfragen können, und Ihre Dashboard-Übersichtsseite aufbauen.

Wahl der Datenabfragemethode

API-Schicht

APIs sind eine Vermittlungsschicht zwischen Ihrem Anwendungscode und der Datenbank. Es gibt einige Fälle, in denen Sie eine API verwenden könnten:

  • Wenn Sie Dienste von Drittanbietern nutzen, die eine API bereitstellen.
  • Wenn Sie Daten vom Client abfragen, sollten Sie eine API-Schicht haben, die auf dem Server läuft, um Ihre Datenbankgeheimnisse nicht dem Client preiszugeben.

In Next.js können Sie API-Endpunkte mit Route Handlers erstellen.

Datenbankabfragen

Wenn Sie eine Full-Stack-Anwendung erstellen, müssen Sie auch Logik schreiben, um mit Ihrer Datenbank zu interagieren. Für relationale Datenbanken wie Postgres können Sie dies mit SQL oder einem ORM tun.

Es gibt einige Fälle, in denen Sie Datenbankabfragen schreiben müssen:

  • Beim Erstellen Ihrer API-Endpunkte müssen Sie Logik schreiben, um mit Ihrer Datenbank zu interagieren.
  • Wenn Sie React Server Components verwenden (Datenabfrage auf dem Server), können Sie die API-Schicht überspringen und direkt Ihre Datenbank abfragen, ohne Risiko, Ihre Datenbankgeheimnisse dem Client preiszugeben.

Lassen Sie uns mehr über React Server Components erfahren.

Verwendung von Server Components für Datenabfragen

Standardmäßig verwenden Next.js-Anwendungen React Server Components. Die Datenabfrage mit Server Components ist ein relativ neuer Ansatz und bietet einige Vorteile:

  • Server Components unterstützen JavaScript Promises und bieten eine native Lösung für asynchrone Aufgaben wie Datenabfragen. Sie können die async/await-Syntax verwenden, ohne useEffect, useState oder andere Datenabfrage-Bibliotheken zu benötigen.
  • Server Components laufen auf dem Server, sodass Sie aufwändige Datenabfragen und Logik auf dem Server belassen können und nur das Ergebnis an den Client senden.
  • Da Server Components auf dem Server laufen, können Sie direkt die Datenbank abfragen, ohne eine zusätzliche API-Schicht. Dies erspart Ihnen das Schreiben und Warten von zusätzlichem Code.

Verwendung von SQL

Für Ihre Dashboard-Anwendung werden Sie Datenbankabfragen mit der postgres.js-Bibliothek und SQL schreiben. Es gibt einige Gründe, warum wir SQL verwenden:

  • SQL ist der Industriestandard für Abfragen relationaler Datenbanken (z.B. generieren ORMs SQL im Hintergrund).
  • Ein grundlegendes Verständnis von SQL kann Ihnen helfen, die Grundlagen relationaler Datenbanken zu verstehen, sodass Sie Ihr Wissen auf andere Tools anwenden können.
  • SQL ist vielseitig und ermöglicht es Ihnen, spezifische Daten abzufragen und zu manipulieren.
  • Die postgres.js-Bibliothek bietet Schutz vor SQL-Injections.

Machen Sie sich keine Sorgen, wenn Sie SQL noch nicht verwendet haben – wir haben die Abfragen für Sie vorbereitet.

Gehen Sie zu /app/lib/data.ts. Hier sehen Sie, dass wir postgres verwenden. Die sql-Funktion ermöglicht es Ihnen, Ihre Datenbank abzufragen:

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

Sie können sql überall auf dem Server aufrufen, z.B. in einer Server Component. Aber um Ihnen die Navigation durch die Komponenten zu erleichtern, haben wir alle Datenabfragen in der data.ts-Datei belassen, und Sie können sie in die Komponenten importieren.

Hinweis: Wenn Sie in Kapitel 6 Ihren eigenen Datenbankanbieter verwendet haben, müssen Sie die Datenbankabfragen an Ihren Anbieter anpassen. Sie finden die Abfragen in /app/lib/data.ts.

Datenabfrage für die Dashboard-Übersichtsseite

Nachdem Sie nun die verschiedenen Möglichkeiten der Datenabfrage verstanden haben, lassen Sie uns Daten für die Dashboard-Übersichtsseite abfragen. Navigieren Sie zu /app/dashboard/page.tsx, fügen Sie den folgenden Code ein und nehmen Sie sich etwas Zeit, ihn zu erkunden:

/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>
  );
}

Der obige Code ist absichtlich auskommentiert. Wir werden nun jedes Teil untersuchen.

  • Die page ist eine asynchrone Server Component. Dies ermöglicht es Ihnen, await für die Datenabfrage zu verwenden.
  • Es gibt auch 3 Komponenten, die Daten erhalten: <Card>, <RevenueChart> und <LatestInvoices>. Sie sind derzeit auskommentiert und noch nicht implementiert.

Datenabfrage für <RevenueChart/>

Um Daten für die <RevenueChart/>-Komponente abzufragen, importieren Sie die fetchRevenue-Funktion aus data.ts und rufen Sie sie in Ihrer Komponente auf:

/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();
  // ...
}

Als Nächstes führen Sie folgende Schritte aus:

  1. Heben Sie die Kommentierung der <RevenueChart/>-Komponente auf.
  2. Navigieren Sie zur Komponentendatei (/app/ui/dashboard/revenue-chart.tsx) und heben Sie die Kommentierung des Codes darin auf.
  3. Überprüfen Sie localhost:3000, und Sie sollten ein Diagramm sehen, das die revenue-Daten verwendet.
Umsatzdiagramm, das den Gesamtumsatz der letzten 12 Monate zeigt

Lassen Sie uns fortfahren und weitere Daten importieren und im Dashboard anzeigen.

Datenabfrage für <LatestInvoices/>

Für die <LatestInvoices />-Komponente müssen wir die letzten 5 Rechnungen abrufen, sortiert nach Datum.

Sie könnten alle Rechnungen abrufen und sie mit JavaScript sortieren. Dies ist bei unserer kleinen Datenmenge kein Problem, aber wenn Ihre Anwendung wächst, kann dies die übertragene Datenmenge und den benötigten JavaScript-Code für die Sortierung erheblich erhöhen.

Anstatt die letzten Rechnungen im Speicher zu sortieren, können Sie eine SQL-Abfrage verwenden, um nur die letzten 5 Rechnungen abzurufen. Zum Beispiel ist dies die SQL-Abfrage aus Ihrer data.ts-Datei:

/app/lib/data.ts
// Die letzten 5 Rechnungen abrufen, sortiert nach Datum
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`;

Importieren Sie in Ihrer Seite die fetchLatestInvoices-Funktion:

/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();
  // ...
}

Heben Sie dann die Kommentierung der <LatestInvoices />-Komponente auf. Sie müssen auch den relevanten Code in der <LatestInvoices />-Komponente selbst, die sich unter /app/ui/dashboard/latest-invoices befindet, entkommentieren.

Wenn Sie Ihren localhost aufrufen, sollten Sie sehen, dass nur die letzten 5 Rechnungen aus der Datenbank zurückgegeben werden. Hoffentlich beginnen Sie zu erkennen, welche Vorteile die direkte Abfrage Ihrer Datenbank hat!

Latest-Invoices-Komponente neben dem Umsatzdiagramm

Übung: Daten für die <Card>-Komponenten abfragen

Jetzt sind Sie an der Reihe, Daten für die <Card>-Komponenten abzurufen. Die Karten zeigen folgende Daten an:

  • Gesamtbetrag der bezahlten Rechnungen.
  • Gesamtbetrag der ausstehenden Rechnungen.
  • Gesamtzahl der Rechnungen.
  • Gesamtzahl der Kunden.

Auch hier könnten Sie versucht sein, alle Rechnungen und Kunden abzurufen und die Daten mit JavaScript zu verarbeiten. Zum Beispiel könnten Sie Array.length verwenden, um die Gesamtzahl der Rechnungen und Kunden zu erhalten:

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

Aber mit SQL können Sie nur die Daten abrufen, die Sie benötigen. Es ist etwas länger als die Verwendung von Array.length, aber es bedeutet, dass weniger Daten während der Anfrage übertragen werden müssen. Hier ist die SQL-Alternative:

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

Die Funktion, die Sie importieren müssen, heißt fetchCardData. Sie müssen die von der Funktion zurückgegebenen Werte destrukturieren.

Tipp:

  • Überprüfen Sie die Card-Komponenten, um zu sehen, welche Daten sie benötigen.
  • Überprüfen Sie die data.ts-Datei, um zu sehen, was die Funktion zurückgibt.

Wenn Sie bereit sind, erweitern Sie den folgenden Abschnitt für den endgültigen Code:

Großartig! Sie haben nun alle Daten für die Dashboard-Übersichtsseite abgerufen. Ihre Seite sollte nun so aussehen:

Dashboard-Seite mit allen abgerufenen Daten

Allerdings... gibt es zwei Dinge, die Sie beachten sollten:

  1. Die Datenanfragen blockieren sich versehentlich gegenseitig und erzeugen einen Request-Wasserfall.
  2. Standardmäßig prerendert Next.js Routen, um die Leistung zu verbessern. Dies wird als Static Rendering bezeichnet. Wenn sich Ihre Daten ändern, wird dies nicht in Ihrem Dashboard widergespiegelt.

Lassen Sie uns Punkt 1 in diesem Kapitel besprechen und dann Punkt 2 im nächsten Kapitel genauer betrachten.

Was sind Request-Wasserfälle?

Ein "Wasserfall" bezieht sich auf eine Abfolge von Netzwerkanfragen, die von der Fertigstellung vorheriger Anfragen abhängen. Im Fall der Datenabfrage kann jede Anfrage erst beginnen, wenn die vorherige Anfrage Daten zurückgegeben hat.

Diagramm, das sequenzielle und parallele Datenabfrage zeigt

Zum Beispiel müssen wir warten, bis fetchRevenue() ausgeführt wurde, bevor fetchLatestInvoices() starten kann, und so weiter.

/app/dashboard/page.tsx
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // wartet auf Fertigstellung von fetchRevenue()
const {
  numberOfInvoices,
  numberOfCustomers,
  totalPaidInvoices,
  totalPendingInvoices,
} = await fetchCardData(); // wartet auf Fertigstellung von fetchLatestInvoices()

Dieses Muster ist nicht unbedingt schlecht. Es kann Fälle geben, in denen Sie Wasserfälle wollen, weil eine Bedingung erfüllt sein muss, bevor Sie die nächste Anfrage stellen. Zum Beispiel möchten Sie möglicherweise zuerst die ID und Profilinformationen eines Benutzers abrufen. Sobald Sie die ID haben, können Sie dann die Liste seiner Freunde abrufen. In diesem Fall ist jede Anfrage von den Daten abhängig, die von der vorherigen Anfrage zurückgegeben wurden.

Dieses Verhalten kann jedoch auch unbeabsichtigt sein und die Leistung beeinträchtigen.

Parallele Datenabfrage

Eine gängige Methode, um Wasserfälle zu vermeiden, besteht darin, alle Datenanfragen gleichzeitig zu starten – parallel.

In JavaScript können Sie die Funktionen Promise.all() oder Promise.allSettled() verwenden, um alle Promises gleichzeitig zu starten. Zum Beispiel verwenden wir in data.ts Promise.all() in der fetchCardData()-Funktion:

/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,
    ]);
    // ...
  }
}

Mit diesem Muster können Sie:

  • Alle Datenabfragen gleichzeitig starten, was schneller ist, als auf jede Anfrage in einem Wasserfall zu warten.
  • Ein natives JavaScript-Muster verwenden, das auf jede Bibliothek oder jedes Framework angewendet werden kann.

Es gibt jedoch einen Nachteil, wenn Sie sich nur auf dieses JavaScript-Muster verlassen: Was passiert, wenn eine Datenanfrage langsamer ist als alle anderen? Lassen Sie uns das im nächsten Kapitel genauer untersuchen.