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?
| Helyzet | Jobb választás |
|---|---|
| Felhasználók, rendelések, számlák | Relációs (PostgreSQL) |
| Munkamenet tárolás, cache | Redis (NoSQL, kulcs-érték) |
| Rugalmas sémájú termékadatok | MongoDB (NoSQL, dokumentum) |
| Közösségi kapcsolatok | Neo4j (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?
- Bekapcsolod a query logot az ORM-ben
- Listázáskor 1-2 lekérdezés helyett N+100 jelenik meg
- 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:
- Nézd meg a generált SQL-t – minden ORM-nek van debug/log módja. Fejlesztés közben kapcsold be.
- Ismerd az adatbázisod sémáját – milyen táblák vannak, hogyan kapcsolódnak. Ez nem opcionális.
- Migration = sémaváltozás verziókezelve – ne manuálisan módosítsd az adatbázist, minden változás migration fájlban legyen.
- 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.
- 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:
- Ellenőrzi, hogy a termékek készleten vannak-e
- Csökkenti a készletet
- Létrehozza a rendelés rekordot
- Leemeli a vásárló kártyáját (külső payment API)
- 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!
| Fogalom | Definíció |
|---|---|
| 1. ORM | A. Egymás után végrehajtott adatbázis-műveletek, amelyek együtt sikerülnek vagy rollback-elnek |
| 2. N+1 query | B. Egyirányú transzformáció, amellyel jelszót tárolunk visszafejthetetlen formában |
| 3. Eager loading | C. 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. Hash | E. 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
userstáblábanarchived = truelesz, a felhasználó rendelései átkerülnek azarchived_orderstáblába, és a felhasználó munkamenetei törlésre kerülnek asessionstá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:
- Milyen inkonzisztencia keletkezett?
- Mit kellett volna tennie Rékának?
- Ha a következő archiválásnál újrapróbálják, mi a kockázat?
Válaszok:
- 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.
- 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.
- Duplikált rendelések keletkezhetnek az
archived_orderstáblában, ha az átmozgatás logika nem idempotens (nem ellenőrzi, hogy a rekordok már ott vannak-e).