Astro Content Collections: sposób na dane z MySQL

Podczas migracji strony do Astro pojawiło się wyzwanie: jak wykorzystać dane z istniejącej bazy MySQL w statycznie generowanych stronach? Z pomocą przychodzi funkcjonalność Content Collections. Pozwala ona w prosty sposób zintegrować dowolne źródło danych, zamieniając je w kolekcję, z której korzysta się tak samo, jak z plików lokalnych.

Architektura rozwiązania

Mechanizm opiera się na Loaderze. Jest to obiekt z metodą load, która wykonuje się w trakcie budowania projektu (build-time). Całość procesu sprowadza się do trzech kroków:

  1. Połączenie: Nawiązanie bezpiecznego połączenia z bazą danych.
  2. Synchronizacja: Pobranie danych i przekazanie ich do lokalnego magazynu (store).
  3. Walidacja: Użycie schematu Zod, aby upewnić się, że dane z MySQL są zgodne ze strukturą oczekiwaną przez aplikację.

Implementacja Loadera

Poniżej znajduje się kod odpowiedzialny za komunikację z bazą. Wykorzystano bibliotekę mysql2/promise do obsługi asynchronicznego pobierania rekordów.

import type { Loader } from 'astro/loaders';
import mysql from 'mysql2/promise';

export function mysqlLoader(config: { connection: any, table: string }): Loader {
  return {
    name: 'mysql-loader',
    load: async ({ store, logger }) => {
      const connection = await mysql.createConnection(config.connection);
      const sql = `SELECT * FROM ${config.table} ORDER BY data DESC`;
      logger.info("Executing: " + sql);

      const [rows] = await connection.execute(sql);
      
      store.clear();

      (rows as any[]).forEach((row) => {
        store.set({
          id: String(row.idComments),
          data: row,
        });
      });

      await connection.end();
      logger.info(`Loaded ${ (rows as any[]).length } rows from MySQL.`);
    },
  };
}

Integracja z kolekcją

Dzięki zdefiniowaniu schematu w defineCollection, TypeScript wymusza poprawną strukturę danych w całym projekcie:

const comments = defineCollection({
  loader: mysqlLoader({
    connection: {
      host: import.meta.env.DB_HOST,
      user: import.meta.env.DB_USER,
      database: import.meta.env.DB_NAME,
      password: import.meta.env.DB_PASSWORD
    },
    table: import.meta.env.DB_COMMENTS_TABLE,
  }),
  schema: z.object({
    idComments: z.number(),
    name: z.string(),
    comment: z.string(),
    email: z.string(),
    web: z.string(),
    newsId: z.number(),
    data: z.string(),
  }),
});

Dlaczego to podejście jest skuteczne?

  1. Wydajność: Dane są pobierane tylko podczas budowania strony. Użytkownik końcowy otrzymuje już wygenerowaną, statyczną treść.
  2. Typowanie: Dzięki zod błędy w strukturze danych bazy są wychwytywane na etapie budowania, a nie w trakcie przeglądania strony.
  3. Czystość kodu: Logika połączenia jest częścią projektu, nie wymaga dodatkowych konfiguracji i jest w pełni przejrzysta.