7.3 Adatbázis szerepe

← 7. Systems thinking – A nagy kép

7.3 Adatbázis szerepe a rendszerben – nem csak tároló

Áttekintő

Ha valaki azt mondja: „az adatbázis csak tárolja az adatokat" – az már félig elveszett. Az adatbázis az alkalmazásod gerince. Minden adat, amit a felhasználó beír, megjelenít, módosít – végül az adatbázisban él. De a fejlesztők gyakran csak felszínesen értik ezt a kapcsolatot.

Ez az alfejezet nem DBA-képzés. Nem fogsz indexeket hangolni production-ban. De miután végigolvasod, érteni fogod:

  • Mikor érdemes relációs és mikor NoSQL adatbázist választani
  • Mit csinál az ORM és miért nem varázslat
  • Hogyan keletkezik az N+1 query probléma – és hogyan ismerd fel
  • Mire valók a tranzakciók és mikor kell használni őket
  • Miért tilos plaintext jelszót tárolni (és mi a helyes alternatíva)

Ez az a tudás, amit minden junior fejlesztőnek el kellene hoznia az első munkanapjára.


Részletes leírás

Relációs vs. NoSQL – mikor melyik?

Két nagy adatbázis-típus létezik, amellyel a legtöbb munkahelyen találkozol:

Relációs adatbázisok (SQL)

  • Táblák, sorok, oszlopok, kapcsolatok
  • ACID tulajdonságok (erről később)
  • Példák: PostgreSQL, MySQL, SQLite
  • Erősség: strukturált, összefüggő adatok; összetett lekérdezések; adatintegritás
  • Gyengeség: sémaváltozás nehézkes, horizontális skálázás bonyolultabb

NoSQL adatbázisok

  • Különböző modellek: dokumentum (MongoDB), kulcs-érték (Redis), oszlopalapú (Cassandra), gráf (Neo4j)
  • Nincs fix séma
  • Erősség: rugalmas struktúra, gyors írás/olvasás bizonyos mintáknál, könnyebb horizontális skálázás
  • Gyengeség: ACID garantálása nehezebb, összetett relációk kezelése fájdalmas

Mikor melyiket válasszák a cégek?

HelyzetJobb választás
Felhasználók, rendelések, számlákRelációs (PostgreSQL)
Munkamenet tárolás, cacheRedis (NoSQL, kulcs-érték)
Rugalmas sémájú termékadatokMongoDB (NoSQL, dokumentum)
Közösségi kapcsolatokNeo4j (NoSQL, gráf)

Juniorként valószínűleg relációs adatbázissal fogsz dolgozni. PostgreSQL vagy MySQL a legelterjedtebb. Ezeket tanuld meg jól, a NoSQL-t majd megismered, ha szükség lesz rá.


Mit csinál az ORM?

Az ORM (Object-Relational Mapper) egy réteg a kódod és az adatbázis között. Ahelyett hogy SQL lekérdezéseket írna, objektumként kezeli az adatokat.

Nélküle (nyers SQL):

# Python – psycopg2 közvetlen SQL
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
user = cursor.fetchone()

Vele (ORM – Django ORM):

# Python – Django ORM
user = User.objects.get(id=user_id)

JavaScript – Prisma ORM:

const user = await prisma.user.findUnique({
  where: { id: userId }
});

PHP – Eloquent (Laravel):

$user = User::find($userId);

Az ORM előnyei:

  • Nem kell SQL-t írni az egyszerű esetekhez
  • A kód közelebb van a programozási nyelvhez
  • Sémaváltozásoknál segít a migration rendszer

Az ORM hátrányai (amit juniorok sokszor nem tudnak):

  • Nem látod, milyen SQL fut a háttérben
  • Rosszul használva nagyon lassú lekérdezéseket generál (→ N+1)
  • Komplex lekérdezéseknél néha jobb az explicit SQL

A legfontosabb tanács: az ORM-et használd, de időnként nézd meg, milyen SQL-t generál. Minden ORM-nek van query logolási lehetősége – kapcsold be fejlesztés közben.


Az N+1 query probléma

Ez az egyik leggyakoribb performance hiba, amit juniorok elkövetnek. A neve abból jön, hogy 1 lekérdezés helyett N+1-et futtat le.

A szituáció:

Van egy blog alkalmazás. Listáznád az összes bejegyzést, minden bejegyzés mellé a szerző nevével.

Rossz megközelítés (N+1):

# Python pszeudokód
posts = Post.objects.all()          # 1 lekérdezés

for post in posts:
    author = post.author            # Ez N darab lekérdezést indít!
    print(f"{post.title} – {author.name}")

Ha 100 bejegyzés van, ez 101 lekérdezés lesz az adatbázis felé. Minden egyes iterációban elmegy a kérés az adatbázishoz, hogy lekérje a szerzőt.

Jó megközelítés (eager loading):

# Python – Django ORM
posts = Post.objects.select_related('author').all()  # 1 lekérdezés (JOIN)

for post in posts:
    author = post.author            # Már betöltve, nincs új lekérdezés
    print(f"{post.title} – {author.name}")
// JavaScript – Prisma
const posts = await prisma.post.findMany({
  include: { author: true }   // JOIN egyetlen lekérdezésben
});
// PHP – Laravel Eloquent
$posts = Post::with('author')->get();  // eager loading

Hogyan ismerd fel N+1 problémát?

  1. Bekapcsolod a query logot az ORM-ben
  2. Listázáskor 1-2 lekérdezés helyett N+100 jelenik meg
  3. Az oldalad lassabb, mint várni lehetne

Az N+1 nem csak ORM-mel fordulhat elő – bármilyen ciklusban, ahol adatbázis-hívás van, gyanakodj rá.


Tranzakciók – mire valók?

Képzeld el: banki utalás. Levonod Péter számlájáról a pénzt, majd hozzáadod Mária számlájához. Mi történik, ha a rendszer leáll a kettő között?

Péter pénze eltűnt, Mária nem kapta meg. Kész a katasztrófa.

A tranzakció garantálja: vagy mindkét művelet sikeres, vagy egyik sem hajtódik végre.

Ez az ACID elvek egyik pillére:

  • Atomicity – vagy minden, vagy semmi
  • Consistency – az adatbázis érvényes állapotban marad
  • Isolation – párhuzamos tranzakciók nem zavarják egymást
  • Durability – sikeres tranzakció után az adat megmarad

Tranzakció kódban:

# Python – Django
from django.db import transaction

with transaction.atomic():
    peter.balance -= 10000
    peter.save()
    
    maria.balance += 10000
    maria.save()
    # Ha itt hiba volt, mindkét művelet visszagörög
// JavaScript – Prisma
await prisma.$transaction([
  prisma.account.update({
    where: { id: peterId },
    data: { balance: { decrement: 10000 } }
  }),
  prisma.account.update({
    where: { id: mariaId },
    data: { balance: { increment: 10000 } }
  })
]);
// PHP – Laravel
DB::transaction(function () use ($peter, $maria) {
    $peter->balance -= 10000;
    $peter->save();
    
    $maria->balance += 10000;
    $maria->save();
});

Mikor használj tranzakciót?

  • Több tábla vagy több sor módosítása, ami együtt kell hogy sikerüljön
  • Pénzügyi műveletek
  • Státuszváltozások, ahol részleges végrehajtás káros lenne

Juniorként nem kell minden írást tranzakcióba csomagolni – de ha két dolgot egyszerre kell módosítani és az összefügg, automatikusan gondolj rá.


Miért nem tároljuk a jelszót plaintext-ben?

Ez nem csupán jó tanács – ez alapvető biztonsági követelmény.

A probléma: ha az adatbázist feltörik (SQL injection, rossz backup hozzáférés, belső fenyegetés), a jelszavak rögtön nyilvánosak. A felhasználók általában ugyanazt a jelszót használják több helyen is → katasztrófa.

Helyes megközelítés: jelszó hashelés

A hash egy egyirányú függvény. A jelszóból előállít egy karakterláncot, amiből az eredeti jelszó nem nyerhető vissza. Bejelentkezéskor nem a jelszót hasonlítod össze, hanem a hasheket.

Helyes algoritmusok: bcrypt, argon2, scrypt

Miért NEM md5 vagy sha256? Ezek gyorsak – ami az adatbázisban hasznos, de jelszó hashinghez rossz, mert brute force-szal gyorsan feltörhetők.

# Python – bcrypt
import bcrypt

# Jelszó tároláskor
password = "titkos123"
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
# Mentsd a hashed értéket az adatbázisba – nem a password-ot!

# Bejelentkezéskor
bcrypt.checkpw(entered_password.encode(), hashed)  # True vagy False
// JavaScript – bcrypt
const bcrypt = require('bcrypt');

// Tároláskor
const hash = await bcrypt.hash(password, 10);  // 10 = cost factor

// Ellenőrzéskor
const match = await bcrypt.compare(enteredPassword, hash);
// PHP – beépített függvények
$hash = password_hash($password, PASSWORD_BCRYPT);
$valid = password_verify($enteredPassword, $hash);

Amit soha ne csinálj:

  • Plaintext jelszó tárolása
  • MD5 vagy SHA1 hash jelszóhoz
  • Titkosított jelszó (visszafejthető!) – hash kell, nem titkosítás

A fejlesztő és az adatbázis viszonya a valóságban

A legtöbb junior fejlesztő körülbelül így gondolkodik az adatbázisról: „az ORM megcsinálja, nem kell foglalkoznom vele."

Ez a szemlélet időzített bomba.

Amit valójában tudnod kell:

  1. Nézd meg a generált SQL-t – minden ORM-nek van debug/log módja. Fejlesztés közben kapcsold be.
  1. Ismerd az adatbázisod sémáját – milyen táblák vannak, hogyan kapcsolódnak. Ez nem opcionális.
  1. Migration = sémaváltozás verziókezelve – ne manuálisan módosítsd az adatbázist, minden változás migration fájlban legyen.
  1. Ne töröld a production adatbázist – ez nyilvánvalónak hangzik, de megtörténik. Mindig kérdezz rá, mielőtt bármit törölsz.
  1. Seed adatok vs. production adatok – fejlesztői adatbázisban tesztelj, ne production-ban.

Életszerű példák

1. példa: A lassú adminfelület

Balázs junior fejlesztő megírja az adminfelületet, ami listázza az összes rendelést a vásárló nevével. Helyi gépen villámgyors, 10 rendeléssel. Production-ban, 5000 rendelésnél az oldal 30 másodpercig tölt.

Oka: N+1 query. Minden rendelésnél külön lekérdezés ment a vásárlóért.

Megoldás: select_related / with() / include eager loading hozzáadása. Az oldal azután 0.3 másodperc alatt tölt.

Tanulság: tesztelj valós méretű adathalmazon, ne csak 10 sorral.


2. példa: A félbehagyott regisztráció

Kinga implementálja a céges regisztrációs folyamatot. Létrehozza a company rekordot, majd a user rekordot, majd elküldi az üdvözlő emailt. Valami hiba miatt az email küldés elhal. A cég és a felhasználó létrejött, de az email nem ment el.

Ha a felhasználó újrapróbál regisztrálni, duplikált rekord keletkezik.

Ha tranzakciót használt volna a company + user létrehozásra, és az email küldés kívül marad (mert külső szolgáltatás, azt nem lehet tranzakcióba vonni), legalább az adatbázis konzisztens marad. Az email küldést külön kell kezelni (pl. retry mechanizmus).

Tanulság: gondold végig, mely műveletek tartoznak össze, és melyeknek kell egyszerre sikerülniük.


3. példa: A plaintext jelszó incidens

Dávid egy kis projektet épít, gyorsan megírja a regisztrációt, a jelszót egyszerűen elmenti az adatbázisba. Bemutatja a főnökének, kapja a dicséretet. Három hónappal később a projekt publikusan elérhető, és egy SQL injection miatt 200 felhasználó jelszava kiszivárog.

Az incidensből tanulság: a plaintext jelszót nem csupán a saját alkalmazásod védelme miatt tilos tárolni, hanem mert a felhasználóknak ugyanaz a jelszavuk máshol is. Ezért jogszabályi és etikai felelősséged van rá.


Tesztfeladatok

1. feladat – Fogalmak azonosítása

Melyik adatbázis-típus a legjobb választás az alábbi szituációkhoz?

a) Online áruház: termékek, vásárlók, rendelések, számlák – összetett kapcsolatokkal
b) Felhasználói munkamenetek tárolása, ahol gyors írás/olvasás kell és az adat néhány óra múlva nem releváns
c) Rugalmas sémájú termékek, ahol minden terméknek más attribútumai lehetnek

Helyes válaszok:
a) Relációs (pl. PostgreSQL) – strukturált, összetett relációk
b) Redis (NoSQL kulcs-érték) – gyors, ideiglenes tárolás
c) MongoDB (NoSQL dokumentum) – rugalmas séma


2. feladat – N+1 azonosítása

Nézd meg az alábbi pszeudokódot. Azonosítsd, hogy N+1 query probléma fennáll-e, és ha igen, hogyan javítanád!

# Listázzuk az összes blogbejegyzést kommentjeik számával
posts = Post.objects.all()

for post in posts:
    comment_count = Comment.objects.filter(post_id=post.id).count()
    print(f"{post.title}: {comment_count} komment")

Válasz: Igen, N+1 probléma áll fenn. Az összes post lekérése 1 query, majd minden egyes posthoz külön fut egy COUNT lekérdezés. 100 postnál ez 101 query.

Javítás: annotáció / aggregáció egyetlen lekérdezésben:

from django.db.models import Count

posts = Post.objects.annotate(comment_count=Count('comments'))
for post in posts:
    print(f"{post.title}: {post.comment_count} komment")

Ez egyetlen lekérdezés lesz.


3. feladat – Tranzakció szükséges?

Az alábbi szituációk közül melyiknél szükséges tranzakciót használni? Magyarázd meg, miért!

a) Felhasználói profilkép frissítése (csak egyetlen rekord módosítása)
b) Termék rendelésekor: csökkentsd a raktárkészletet ÉS hozd létre a rendelési rekordot
c) Blogbejegyzés olvasottsági számlálójának növelése
d) Felhasználó törlése, aki több táblában is szerepel (orders, reviews, sessions)

Helyes válaszok:

  • a) Nem szükséges – egyetlen atomikus művelet
  • b) Szükséges – ha a rendelés létrejön de a készlet nem csökken, inkonzisztens adat keletkezik
  • c) Általában nem szükséges – ha egyszer nem növekszik, nem katasztrófa
  • d) Szükséges – részleges törlés árva rekordokat hagyna maga után

4. feladat – Jelszókezelési hibák

Az alábbi kódrészletek közül melyik biztonsági szempontból helyes és melyik nem? Magyarázd meg!

# A változat
import hashlib
password_stored = hashlib.md5(password.encode()).hexdigest()

# B változat
import bcrypt
password_stored = bcrypt.hashpw(password.encode(), bcrypt.gensalt())

# C változat
from cryptography.fernet import Fernet
key = Fernet.generate_key()
password_stored = Fernet(key).encrypt(password.encode())

Válaszok:

  • A változat: Helytelen – MD5 gyors algoritmus, brute force-szal gyorsan feltörhető. Jelszóhoz nem megfelelő.
  • B változat: Helyes – bcrypt lassú, salt automatikusan generálódik, ipari standard jelszó hashinghez
  • C változat: Helytelen – titkosítás (kétirányú!), nem hash. Ha a kulcs kiszivárog, az összes jelszó visszafejthető. Jelszóhoz hash kell, nem titkosítás.

5. feladat – ORM és SQL kapcsolata

Mi igaz az ORM-ekről? Jelöld meg az összes helyes állítást!

a) Az ORM teljesen elrejti az adatbázist, nem kell SQL-t ismerned
b) Rosszul használva az ORM N+1 query problémát okozhat
c) Az ORM-ek debug módban megmutathatják a generált SQL-t
d) ORM-mel sosem lesz lassú a lekérdezés
e) Komplex lekérdezéseknél néha jobb az explicit SQL vagy raw query

Helyes válaszok: b, c, e

  • a) Hamis – az SQL ismerete segít megérteni, mi történik a háttérben
  • d) Hamis – az ORM maga nem lassít, de rossz használat (N+1, missing eager loading) igen

6. feladat – Szituációs döntés

Képzeld el, hogy egy e-commerce alkalmazásban dolgozol. A checkout folyamat az alábbi lépéseket végzi:

  1. Ellenőrzi, hogy a termékek készleten vannak-e
  2. Csökkenti a készletet
  3. Létrehozza a rendelés rekordot
  4. Leemeli a vásárló kártyáját (külső payment API)
  5. Küld egy emailt a vásárlónak

Melyik lépéseket vonnád tranzakcióba, és melyeket hagynád kívül? Miért?

Ideális válasz:

  • Tranzakcióba: 1 + 2 + 3 (ezek az adatbázis-műveletek szorosan összefüggenek – ha bármelyik sikertelen, a többi se hajtódjon végre)
  • Kívül: 4 és 5 – ezek külső szolgáltatások, tranzakcióba vonni őket nem lehet. Kezelésük: kompenzáló műveletek (ha a payment sikertelen, rollback a készletre / rendelésre), az email küldés pedig aszinkron, retry-logikával

7. feladat – Fogalom-párosítás

Párosítsd a fogalmakat a definíciójukkal!

FogalomDefiníció
1. ORMA. Egymás után végrehajtott adatbázis-műveletek, amelyek együtt sikerülnek vagy rollback-elnek
2. N+1 queryB. Egyirányú transzformáció, amellyel jelszót tárolunk visszafejthetetlen formában
3. Eager loadingC. Réteg, amely objektumként kezeli az adatbázis-sorokat
4. TranzakcióD. Ciklusban futó extra lekérdezések, mert a kapcsolódó adatot nem töltöttük be előre
5. HashE. A kapcsolódó adatok előzetes betöltése, kevesebb lekérdezéssel

Válaszok: 1-C, 2-D, 3-E, 4-A, 5-B


8. feladat – Igaz vagy hamis?

Értékeld az alábbi állításokat!

a) A NoSQL adatbázis mindig gyorsabb, mint a relációs.
b) Ha az ORM-t használom, nem kell aggódnom az SQL teljesítmény miatt.
c) Egy jelszó MD5 hash-ét nem lehet visszafejteni, ezért biztonságos.
d) A tranzakció garantálja, hogy vagy minden módosítás megvalósul, vagy egy sem.
e) Az N+1 probléma csak nagyon nagy adatmennyiségnél jelenik meg.

Válaszok:

  • a) Hamis – a teljesítmény az adatmodell és a használati minta függvénye
  • b) Hamis – az ORM rejtett lekérdezéseket generálhat, figyelni kell rájuk
  • c) Hamis – az MD5 nem visszafejthető, DE brute force / rainbow table-lel feltörhető, mert gyors
  • d) Igaz
  • e) Hamis – kis adatmennyiségnél is megjelenik, csak kisebb hatással; fejlesztési környezetben ezért nem látni

9. feladat – Mini tervezési feladat

Tervezd meg, hogyan tárolnád az alábbi adatokat biztonságosan egy felhasználói rendszerben:

  • Felhasználónév
  • Email cím
  • Jelszó
  • Születési dátum
  • Utolsó belépés időpontja

Milyen adatot hogyan tárolnál, és van-e bármelyiknél biztonsági megfontolás?

Ideális válasz:

  • Felhasználónév: normál szöveg, egyedi index
  • Email cím: normál szöveg, egyedi index; esetleg lowercase normalizálva
  • Jelszó: bcrypt vagy argon2 hash – soha nem plaintext, soha nem titkosítva
  • Születési dátum: dátum típus; ha különösen érzékeny, lehet titkosítva tárolni (ez már GDPR/PII téma)
  • Utolsó belépés: timestamp, normál tárolás

10. feladat – Esetelemzés

Olvasd el az alábbi szituációt és válaszolj a kérdésekre!

Réka egy csapatban dolgozik. Hozzáadnak egy új funkciót: az adminok "archiválhatnak" felhasználókat. Az archiválás azt jelenti, hogy a users táblában archived = true lesz, a felhasználó rendelései átkerülnek az archived_orders táblába, és a felhasználó munkamenetei törlésre kerülnek a sessions táblából.

Réka megírja a kódot tranzakció nélkül. Az archiválás közben hálózati hiba miatt a sessions törlése nem fut le.

Kérdések:

  1. Milyen inkonzisztencia keletkezett?
  2. Mit kellett volna tennie Rékának?
  3. Ha a következő archiválásnál újrapróbálják, mi a kockázat?

Válaszok:

  1. A felhasználó archived = true, a rendelések átkerültek, DE a munkamenetek még aktívak → az "archivált" felhasználó aktív munkamenettel rendelkezik, ami biztonsági és logikai hiba.
  1. Tranzakcióba kellett volna fogni az összes adatbázis-műveletet (users update + orders insert + sessions delete). Ha bármelyik sikertelen, az összes visszagörög.
  1. Duplikált rendelések keletkezhetnek az archived_orders táblában, ha az átmozgatás logika nem idempotens (nem ellenőrzi, hogy a rekordok már ott vannak-e).
Scroll to Top