Kurs Next.js · Moduł 4: Bazy danych i pobieranie danych
Przegląd opcji baz danych dla Next.js (SQL, NoSQL)
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ą:
PostgreSQL
- Darmowa, open-source
- Zaawansowane funkcje (np. typy JSON, full-text search)
- Świetna wydajność i skalowalność
- Doskonała integralność danych
MySQL/MariaDB
- Szeroko stosowana, darmowa baza danych
- Dobra wydajność dla operacji odczytu
- Mniejsze wymagania zasobów niż PostgreSQL
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:
Dokumentowe (MongoDB, Firebase Firestore)
- Przechowują dane jako dokumenty (obiekty JSON/BSON)
- Elastyczna struktura bez sztywnego schematu
- Świetne do szybkiego prototypowania i danych nieustrukturyzowanych
Key-Value (Redis, DynamoDB)
- Przechowują proste pary klucz-wartość
- Ekstremalnie szybkie operacje odczytu/zapisu
- Często używane jako cache
Wide-Column (Cassandra, HBase)
- Przechowują dane w kolumnach zamiast wierszach
- Wysoka skalowalność i wydajność dla ogromnych zbiorów danych
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:
- Aplikacje z jasno zdefiniowanymi relacjami: PostgreSQL + Prisma
- Szybkie prototypowanie i elastyczne struktury danych: MongoDB lub Firestore
- Aplikacje serverless: PlanetScale, Neon, lub Supabase
- 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?