Kurs Next.js · Moduł 4: Bazy danych i pobieranie danych

Przegląd opcji baz danych dla Next.js (SQL, NoSQL)

7 min czytania
W tej lekcji5

Wybór odpowiedniej bazy danych jest jedną z kluczowych decyzji przy tworzeniu aplikacji w Next.js. W tym module poznamy najpopularniejsze opcje baz danych oraz ich zalety i wady w kontekście aplikacji Next.js.

Rodzaje baz danych

Bazy danych możemy podzielić na dwie główne kategorie: SQL (relacyjne) i NoSQL (nierelacyjne). Każda z nich ma swoje charakterystyczne cechy, które wpływają na to, do jakich projektów najlepiej się nadają.

Bazy danych SQL (relacyjne)

Bazy relacyjne organizują dane w tabelach z predefiniowaną strukturą i relacjami między nimi. Przykładami popularnych baz SQL są:

  1. PostgreSQL

    • Darmowa, open-source
    • Zaawansowane funkcje (np. typy JSON, full-text search)
    • Świetna wydajność i skalowalność
    • Doskonała integralność danych
  2. MySQL/MariaDB

    • Szeroko stosowana, darmowa baza danych
    • Dobra wydajność dla operacji odczytu
    • Mniejsze wymagania zasobów niż PostgreSQL
  3. SQLite

    • Lekka baza danych (plik) bez osobnego serwera
    • Idealna do prototypowania i testów
    • Nie nadaje się do produkcyjnych aplikacji z dużym ruchem

Bazy danych NoSQL (nierelacyjne)

Bazy NoSQL oferują większą elastyczność struktury danych i zazwyczaj lepszą skalowalność poziomą. Główne typy to:

  1. Dokumentowe (MongoDB, Firebase Firestore)

    • Przechowują dane jako dokumenty (obiekty JSON/BSON)
    • Elastyczna struktura bez sztywnego schematu
    • Świetne do szybkiego prototypowania i danych nieustrukturyzowanych
  2. Key-Value (Redis, DynamoDB)

    • Przechowują proste pary klucz-wartość
    • Ekstremalnie szybkie operacje odczytu/zapisu
    • Często używane jako cache
  3. Wide-Column (Cassandra, HBase)

    • Przechowują dane w kolumnach zamiast wierszach
    • Wysoka skalowalność i wydajność dla ogromnych zbiorów danych
  4. Grafowe (Neo4j, Amazon Neptune)

    • Specjalizują się w relacjach między danymi
    • Idealne do złożonych relacji i analizy połączeń

Popularne opcje baz danych dla Next.js

1. PostgreSQL + Prisma

PostgreSQL w połączeniu z ORM Prisma to jedna z najpopularniejszych opcji dla aplikacji Next.js:

1// Przykład modelu Prisma dla PostgreSQL
2model User {
3  id        Int      @id @default(autoincrement())
4  email     String   @unique
5  name      String?
6  posts     Post[]
7  profile   Profile?
8  createdAt DateTime @default(now())
9  updatedAt DateTime @updatedAt
10}
11
12model Post {
13  id        Int      @id @default(autoincrement())
14  title     String
15  content   String?
16  published Boolean  @default(false)
17  author    User     @relation(fields: [authorId], references: [id])
18  authorId  Int
19  createdAt DateTime @default(now())
20  updatedAt DateTime @updatedAt
21}
22
23model Profile {
24  id     Int    @id @default(autoincrement())
25  bio    String
26  user   User   @relation(fields: [userId], references: [id])
27  userId Int    @unique
28}

Zalety:

  • Silna integralność danych dzięki relacjom
  • Wysoka wydajność i niezawodność
  • Dobra skalowalność
  • Świetne wsparcie TypeScript dzięki Prisma

Wady:

  • Wymaga serwera bazy danych
  • Sztywna struktura danych (schema)

2. MongoDB + Mongoose

MongoDB to popularna nierelacyjna baza dokumentowa, często używana z Next.js:

1// Przykładowy model Mongoose dla MongoDB
2import mongoose from 'mongoose';
3
4const UserSchema = new mongoose.Schema({
5  name: {
6    type: String,
7    required: [true, 'Proszę podać imię'],
8    maxlength: [50, 'Imię nie może być dłuższe niż 50 znaków']
9  },
10  email: {
11    type: String,
12    required: [true, 'Proszę podać email'],
13    unique: true,
14    match: [
15      /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/,
16      'Proszę podać poprawny adres email'
17    ]
18  },
19  posts: [
20    {
21      type: mongoose.Schema.Types.ObjectId,
22      ref: 'Post'
23    }
24  ],
25  createdAt: {
26    type: Date,
27    default: Date.now
28  }
29});
30
31export default mongoose.models.User || mongoose.model('User', UserSchema);

Zalety:

  • Elastyczna struktura danych
  • Łatwa skalowalność pozioma
  • Format danych podobny do JSON
  • Dobre wsparcie dla TypeScript

Wady:

  • Słabsza integralność danych niż w bazach SQL
  • Trudniejsze złożone zapytania

3. Serverless Databases (PlanetScale, Neon, Supabase)

Serverless databases to nowy trend idealnie pasujący do architektury Next.js:

1// Przykład użycia PlanetScale z Next.js
2import { PrismaClient } from '@prisma/client';
3
4const prisma = new PrismaClient();
5
6// W API route lub Server Component
7export async function getUsers() {
8  try {
9    const users = await prisma.user.findMany({
10      include: { posts: true }
11    });
12    return users;
13  } catch (error) {
14    console.error('Database error:', error);
15    throw new Error('Nie udało się pobrać użytkowników');
16  }
17}

Popularne opcje:

  • PlanetScale - MySQL-kompatybilna baza danych serverless
  • Neon - PostgreSQL w modelu serverless
  • Supabase - platforma backend-as-a-service z PostgreSQL
  • Firebase - kompleksowe rozwiązanie z Firestore jako bazą dokumentową

Zalety:

  • Brak konieczności zarządzania infrastrukturą
  • Pay-per-use (płacisz tylko za to, co wykorzystasz)
  • Automatyczne skalowanie
  • Często darmowy plan dla małych projektów

Wady:

  • Potencjalnie wyższe koszty przy dużej skali
  • Mniejsza kontrola nad infrastrukturą
  • Zależność od zewnętrznego dostawcy

4. Edge Databases (Cloudflare D1, Fauna)

Nowością w ekosystemie Next.js są bazy danych działające na edge:

1// Przykład użycia Cloudflare D1 z Next.js
2// W API Route (Pages Router) lub Route Handler (App Router)
3import { D1Database } from '@cloudflare/workers-types';
4
5export const runtime = 'edge';
6
7export async function GET() {
8  const db = process.env.DB as unknown as D1Database;
9
10  const { results } = await db
11    .prepare('SELECT * FROM users WHERE status = ?')
12    .bind('active')
13    .all();
14
15  return Response.json({ users: results });
16}

Zalety:

  • Minimalne opóźnienia dla użytkowników globalnych
  • Integracja z funkcjami edge
  • Często niższe koszty

Wady:

  • Ograniczone możliwości w porównaniu z tradycyjnymi bazami danych
  • Relatywnie nowa technologia

Wybór bazy danych dla Next.js - czynniki do rozważenia

1. Struktura danych

  • Jeśli masz jasno zdefiniowaną strukturę danych z relacjami → PostgreSQL lub MySQL
  • Jeśli struktura danych jest płynna i może się zmieniać → MongoDB lub Firestore

2. Skalowalność

  • Dla aplikacji, które muszą obsłużyć miliony użytkowników → Serverless options (PlanetScale, Neon)
  • Dla aplikacji o przewidywalnym ruchu → Tradycyjne rozwiązania jak PostgreSQL

3. Zespół i doświadczenie

  • Jeśli zespół ma doświadczenie z SQL → PostgreSQL/MySQL
  • Jeśli zespół preferuje podejście JavaScript-first → MongoDB

4. Hosting i deployment

  • Jeśli używasz Vercel do hostingu Next.js → Dobrze integruje się z PlanetScale, Neon, Supabase
  • Jeśli używasz Cloudflare Pages → Cloudflare D1 lub Cloudflare KV

Przykłady integracji w Next.js

Przykład 1: PostgreSQL + Prisma w Next.js

1// lib/prisma.ts
2import { PrismaClient } from '@prisma/client';
3
4let prisma: PrismaClient;
5
6if (process.env.NODE_ENV === 'production') {
7  prisma = new PrismaClient();
8} else {
9  // Zapobieganie wielu instancjom w trybie dev
10  if (!global.prisma) {
11    global.prisma = new PrismaClient();
12  }
13  prisma = global.prisma;
14}
15
16export default prisma;
17
18// W Server Component (App Router)
19// app/users/page.tsx
20import prisma from '@/lib/prisma';
21
22export default async function UsersPage() {
23  const users = await prisma.user.findMany({
24    include: {
25      posts: true,
26    },
27  });
28
29  return (
30    <div>
31      <h1>Użytkownicy</h1>
32      <ul>
33        {users.map((user) => (
34          <li key={user.id}>
35            {user.name} - {user.posts.length} postów
36          </li>
37        ))}
38      </ul>
39    </div>
40  );
41}

Przykład 2: MongoDB w Next.js (Pages Router)

1// lib/mongodb.js
2import { MongoClient } from 'mongodb';
3
4const MONGODB_URI = process.env.MONGODB_URI;
5const MONGODB_DB = process.env.MONGODB_DB;
6
7if (!MONGODB_URI) {
8  throw new Error('Zdefiniuj MONGODB_URI w pliku .env.local');
9}
10
11if (!MONGODB_DB) {
12  throw new Error('Zdefiniuj MONGODB_DB w pliku .env.local');
13}
14
15let cachedClient = null;
16let cachedDb = null;
17
18export async function connectToDatabase() {
19  if (cachedClient && cachedDb) {
20    return { client: cachedClient, db: cachedDb };
21  }
22
23  const client = await MongoClient.connect(MONGODB_URI, {
24    useNewUrlParser: true,
25    useUnifiedTopology: true,
26  });
27
28  const db = await client.db(MONGODB_DB);
29
30  cachedClient = client;
31  cachedDb = db;
32
33  return { client, db };
34}
35
36// W API Route
37// pages/api/users.js
38import { connectToDatabase } from '@/lib/mongodb';
39
40export default async function handler(req, res) {
41  const { db } = await connectToDatabase();
42
43  const users = await db
44    .collection('users')
45    .find({})
46    .limit(20)
47    .toArray();
48
49  res.status(200).json(users);
50}

Podsumowanie

Przy wyborze bazy danych dla Next.js warto kierować się następującymi zasadami:

  1. Aplikacje z jasno zdefiniowanymi relacjami: PostgreSQL + Prisma
  2. Szybkie prototypowanie i elastyczne struktury danych: MongoDB lub Firestore
  3. Aplikacje serverless: PlanetScale, Neon, lub Supabase
  4. Aplikacje globalne z niskim opóźnieniem: Edge Databases jak Cloudflare D1 lub FaunaDB

Niezależnie od wyboru, Next.js oferuje świetne mechanizmy integracji z wszystkimi popularnymi bazami danych. W kolejnym module dowiemy się, jak skonfigurować ORM dla wybranego rozwiązania, aby jeszcze bardziej usprawnić pracę z bazą danych.

Kod do tej lekcji: app/page.tsx
1// Database Integration Demo - app/page.tsx
2'use client';
3
4import { useState, useEffect } from 'react';
5import { CitizenManager } from './components/CitizenManager';
6import { EnergyMonitor } from './components/EnergyMonitor';
7import { DatabaseStatsPanel } from './components/DatabaseStatsPanel';
8import { QueryBuilder } from './components/QueryBuilder';
9
10interface DatabaseStats {
11  totalCitizens: number;
12  activeConnections: number;
13  energyRecords: number;
14  avgResponseTime: number;
15  lastBackup: string;
16}
17
18export default function DatabaseHomePage() {
19  const [activeTab, setActiveTab] = useState('overview');
20  const [isConnected, setIsConnected] = useState(false);
21  const [connectionStatus, setConnectionStatus] = useState('connecting');
22  const [stats, setStats] = useState<DatabaseStats>({
23    totalCitizens: 2847392,
24    activeConnections: 47,
25    energyRecords: 9542816,
26    avgResponseTime: 23.7,
27    lastBackup: '2024-01-15 03:00:00'
28  });
29
30  useEffect(() => {
31    console.log("Initializing database connection...");
32
33    // Symuluj połączenie z bazą danych
34    const connectToDatabase = async () => {
35      setConnectionStatus('connecting');
36
37      // Symuluj opóźnienie połączenia
38      await new Promise(resolve => setTimeout(resolve, 2000));
39
40      setIsConnected(true);
41      setConnectionStatus('connected');
42      console.log("Database connection established");
43
44      // Symuluj aktualizacje statystyk
45      const interval = setInterval(() => {
46        setStats(prev => ({
47          ...prev,
48          activeConnections: prev.activeConnections + Math.floor(Math.random() * 3) - 1,
49          avgResponseTime: Math.max(10, prev.avgResponseTime + (Math.random() - 0.5) * 5)
50        }));
51      }, 3000);
52
53      return () => clearInterval(interval);
54    };
55
56    connectToDatabase();
57  }, []);
58
59  const getConnectionColor = () => {
60    switch (connectionStatus) {
61      case 'connected': return '#4caf50';
62      case 'connecting': return '#ff9800';
63      case 'error': return '#f44336';
64      default: return '#757575';
65    }
66  };
67
68  const getConnectionIcon = () => {
69    switch (connectionStatus) {
70      case 'connected': return 'OK';
71      case 'connecting': return '↻';
72      case 'error': return '×';
73      default: return '⏳';
74    }
75  };
76
77  return (
78    <div className="database-home">
79      <section className="hero-section">
80        <h1>Quantum Database Control Center</h1>
81        <p>
82         Zarządzaj danymi Metropolis Quantum z wykorzystaniem nowoczesnych 
83          technologii bazodanowych i ORM w Next.js 16
84        </p>
85
86        <div className="connection-status">
87          <div
88            className="status-indicator"
89            style={{ backgroundColor: getConnectionColor() }}
90          >
91            <span className="status-icon">{getConnectionIcon()}</span>
92            <span className="status-text">
93              Database: {connectionStatus.toUpperCase()}
94            </span>
95          </div>
96
97          {isConnected && (
98            <div className="connection-details">
99              <span>Quantum-DB v3.2.1</span>
100              <span>Cluster: metropolis-primary</span>
101              <span>Response: {stats.avgResponseTime.toFixed(1)}ms</span>
102            </div>
103          )}
104        </div>
105      </section>
106
107      <section className="database-overview">
108        <h2>Database Overview</h2>
109
110        <div className="overview-grid">
111          <div className="overview-card">
112            <div className="card-icon">◆</div>
113            <div className="card-content">
114              <div className="card-value">{stats.totalCitizens.toLocaleString()}</div>
115              <div className="card-label">Total Citizens</div>
116              <div className="card-change positive">+127 today</div>
117            </div>
118          </div>
119
120          <div className="overview-card">
121            <div className="card-icon">↯</div>
122            <div className="card-content">
123              <div className="card-value">{stats.energyRecords.toLocaleString()}</div>
124              <div className="card-label">Energy Records</div>
125              <div className="card-change positive">+2.4k today</div>
126            </div>
127          </div>
128
129          <div className="overview-card">
130            <div className="card-icon">◆</div>
131            <div className="card-content">
132              <div className="card-value">{stats.activeConnections}</div>
133              <div className="card-label">Active Connections</div>
134              <div className="card-change neutral">Real-time</div>
135            </div>
136          </div>
137
138          <div className="overview-card">
139            <div className="card-icon">⏱</div>
140            <div className="card-content">
141              <div className="card-value">{stats.avgResponseTime.toFixed(1)}ms</div>
142              <div className="card-label">Avg Response Time</div>
143              <div className="card-change positive">-5.2ms</div>
144            </div>
145          </div>
146        </div>
147      </section>
148
149      <section className="database-operations">
150        <h2>Database Operations</h2>
151
152        <div className="operations-tabs">
153          {[
154            { id: 'overview', label: 'Overview', icon: '' },
155            { id: 'citizens', label: 'Citizens', icon: '' },
156            { id: 'energy', label: 'Energy', icon: '' },
157            { id: 'queries', label: 'Query Builder', icon: '' }
158          ].map(tab => (
159            <button
160              key={tab.id}
161              className={`tab-button ${activeTab === tab.id ? 'active' : ''}`}
162              onClick={() => setActiveTab(tab.id)}
163            >
164              <span className="tab-icon">{tab.icon}</span>
165              {tab.label}
166            </button>
167          ))}
168        </div>
169
170        <div className="tab-content">
171          {activeTab === 'overview' && (
172            <DatabaseStatsPanel stats={stats} isConnected={isConnected} />
173          )}
174
175          {activeTab === 'citizens' && (
176            <CitizenManager isConnected={isConnected} />
177          )}
178
179          {activeTab === 'energy' && (
180            <EnergyMonitor isConnected={isConnected} />
181          )}
182
183          {activeTab === 'queries' && (
184            <QueryBuilder isConnected={isConnected} />
185          )}
186        </div>
187      </section>
188
189      <section className="database-info">
190        <h2>Database Architecture</h2>
191
192        <div className="architecture-grid">
193          <div className="arch-card">
194            <h3>Database Models</h3>
195            <div className="code-block">
196              <pre><code>{`// Prisma Schema Example
197model Citizen {
198  id        String   @id @default(cuid())
199  name      String
200  email     String   @unique
201  district  District @relation(fields: [districtId], references: [id])
202  districtId String
203  energyUsage EnergyUsage[]
204  createdAt DateTime @default(now())
205  updatedAt DateTime @updatedAt
206}
207
208model District {
209  id       String    @id @default(cuid())
210  name     String    @unique
211  type     String
212  citizens Citizen[]
213  energyGrid EnergyGrid?
214}`}</code></pre>
215            </div>
216          </div>
217
218          <div className="arch-card">
219            <h3>API Integration</h3>
220            <div className="code-block">
221              <pre><code>{`// Next.js API Route
222export async function GET() {
223  try {
224    const citizens = await prisma.citizen.findMany({
225      include: {
226        district: true,
227        energyUsage: {
228          orderBy: { createdAt: 'desc' },
229          take: 5
230        }
231      }
232    });
233
234    return Response.json(citizens);
235  } catch (error) {
236    return Response.json(
237      { error: 'Database connection failed' },
238      { status: 500 }
239    );
240  }
241}`}</code></pre>
242            </div>
243          </div>
244
245          <div className="arch-card">
246            <h3>Real-time Updates</h3>
247            <div className="code-block">
248              <pre><code>{`// Server-Sent Events for real-time data
249useEffect(() => {
250  const eventSource = new EventSource('/api/events');
251
252  eventSource.onmessage = (event) => {
253    const data = JSON.parse(event.data);
254    updateEnergyData(data);
255  };
256
257  return () => eventSource.close();
258}, []);`}</code></pre>
259            </div>
260          </div>
261
262          <div className="arch-card">
263            <h3>Performance Optimization</h3>
264            <div className="features-list">
265              <div className="feature-item">
266                <span className="feature-icon">▲</span>
267                <span>Connection Pooling</span>
268              </div>
269              <div className="feature-item">
270                <span className="feature-icon">◆</span>
271                <span>Query Optimization</span>
272              </div>
273              <div className="feature-item">
274                <span className="feature-icon">■</span>
275                <span>Redis Caching</span>
276              </div>
277              <div className="feature-item">
278                <span className="feature-icon">↻</span>
279                <span>Background Jobs</span>
280              </div>
281            </div>
282          </div>
283        </div>
284      </section>
285    </div>
286  );
287}

Widzisz błąd w tej lekcji?

Przydatne artykuły