GammaCyber

מסדי נתונים

פיתוח Fullstack מתקדם · 4–5 שעות

// סקירה

מסדי נתונים — הזיכרון לטווח ארוך

השרת שלנו סובל מ"אמנזיה" — כל הנתונים נעלמים ברגע שמפעילים אותו מחדש. בשיעור זה נחבר חדר אחסון קבוע ונלמד את שתי הפילוסופיות המרכזיות: SQL הנוקשה ו-NoSQL הגמישה.

✓נושאי השיעור
  • למה צריך מסד נתונים? RAM מול Disk — נדיף לעומת קבוע
  • SQL — מודל רלציוני עם טבלאות וSchema נוקשה
  • NoSQL / MongoDB — מסמכי JSON גמישים ללא Schema
  • CRUD — 4 פעולות היסוד: Create, Read, Update, Delete
  • Mongoose — ספרייה שמוסיפה Schema ל-MongoDB
  • חיבור שרת Express למסד הנתונים — פעולת Create ב-POST
  • אבטחה: Mass Assignment ו-Schema כחומת הגנה
נושא טכנישלב במיוןביטוי במבחן
SQL vs NoSQLראיונות מקצועייםאיזה מסד לבחור לכל סוג מערכת?
פעולות CRUDהשאלון המקווןקריאת שאילתות, JOINs, Aggregations
DB Injectionsמבחני C ו-CTFאיתור וניצול SQL/NoSQL Injection
// פרק 1

למה צריך מסד נתונים?

השרת שומר נתונים ב-RAM — זיכרון זמני שנמחק בכל Restart. מסד נתונים שומר על הדיסק הקשיח לצמיתות, ועושה זאת בצורה שמאפשרת שליפה מהירה.

RAM (זיכרון שרת)Database (דיסק)
נדיף — נמחק בכל Restartקבוע — שורד כיבויים והפעלות מחדש
גישה מהירה מאודגישה מהירה עם Indexes
קיבולת מוגבלתקיבולת כמעט ללא הגבלה
אין שאילתות מובנותשפות שאילתה (SQL, MongoDB Query)
💡 Index — כמו תוכן עניינים בספר
מסד נתונים יוצר אינדקסים כדי למצוא נתונים בלי לסרוק את כל הקובץ. חיפוש משתמש מתוך מיליון רשומות לוקח חלקיקי שנייה — לא דקות.
✏תרגול עצמי2 שאלות
שאלה 1.1
מדוע לא ניתן לשמור נתונים לצמיתות במשתנים ב-Node.js?
✓ תשובה: ב) — RAM הוא זיכרון נדיף (Volatile). ברגע שהשרת נסגר, כל הנתונים אובדים. מסד נתונים שומר על הדיסק (Disk) שממשיך להחזיק את המידע.
שאלה 1.2
מדוע מסד נתונים יכול לחפש משתמש מתוך מיליון רשומות תוך חלקיק שנייה?
✓ תשובה: ד) — Index הוא כמו תוכן עניינים בספר. במקום לקרוא את כל הספר כדי למצוא פרק, אתה הולך ישירות לעמוד. Index מאפשר למסד לדלג ישירות לרשומה הנכונה.
// פרק 2

SQL מול NoSQL — שתי פילוסופיות

שתי גישות שונות לניהול מידע. SQL — הרואה חשבון הנוקשה. NoSQL — המחסנאי הגמיש. הבחירה ביניהן תלויה בסוג האפליקציה.

האנלוגיה: המסעדה

SQL (הרואה חשבון הנוקשה): דורש טבלאות מושלמות. עמודת "גיל" מקבלת רק מספרים — כתיבת "צעיר" תגרום לשגיאה ודחייה. הטבלאות מקושרות — לא ניתן למחוק לקוח שיש לו הזמנה פתוחה. קפדן, מדויק, מתאים לבנקים.

NoSQL/MongoDB (המחסנאי הגמיש): עובד עם "קולקציות" של מסמכי JSON. בפתק אחד יש גיל, בפתק שני אין גיל אבל יש העדפות אלרגיה. למחסנאי לא אכפת. מתאים לרשתות חברתיות ואפליקציות דינמיות.

קריטריוןSQLNoSQL (MongoDB)
מודל נתוניםטבלאות (Relations)מסמכי JSON (Documents)
Schemaנוקשה — חייב להגדיר מראשגמיש — Schemaless
שאילתותSQL (SELECT, JOIN, WHERE)MongoDB Query Language
יתרוןאמינות, עסקאות (Transactions)גמישות, מהירות פיתוח
מתאים לבנקים, ERP, מערכות פיננסיותרשתות חברתיות, real-time apps
🛡️ שאלת דיון — סייבר הגנתי

למדנו ש-MongoDB מקבל כל אובייקט JSON. אם לקוח ממלא טופס הרשמה ואנחנו שולחים את האובייקט ישירות למסד, מה ימנע מתוקף להוסיף שדה:

{ "username": "hacker99", "password": "123", "isAdmin": true }

התשובה: כלום — ללא הגנה. זהו Mass Assignment Attack. הפתרון הוא Mongoose Schema שמסנן שדות לא מורשים.

✏תרגול עצמי3 שאלות
שאלה 2.1
אתה בונה מערכת בנקאית שמנהלת העברות כספים. איזה מסד נתונים מתאים יותר ולמה?
✓ תשובה: ג) — מערכת בנקאית דורשת Transactions: אם העברה נכשלת באמצע (ניכוי מחשבון א' הצליח אבל הזיכוי לחשבון ב' נכשל), SQL יחזיר הכל למצב הקודם. NoSQL מסורתי לא תמיד ערב לכך.
שאלה 2.2
מה פירוש "Schemaless" ב-MongoDB?
✓ תשובה: ב) — ב-SQL כל שורה בטבלה חייבת לכלול את אותן עמודות. ב-MongoDB מסמך אחד יכול לכלול "גיל" ומסמך שני לא — גמישות מלאה, אבל דורש זהירות מהמפתח.
שאלה 2.3 — סייבר
תוקף שולח בקשת הרשמה עם השדה "isAdmin": true בתוך ה-JSON. מה יקרה אם השרת מעביר את ה-Body ישירות ל-User.create(req.body) ב-MongoDB?
✓ תשובה: ד) — זוהי מתקפת Mass Assignment. ללא Schema, MongoDB מקבל ושומר כל שדה שנשלח. ה-isAdmin: true יישמר ויעניק לתוקף הרשאות מנהל. הפתרון הוא Mongoose Schema.
// פרק 3

CRUD — 4 פעולות היסוד

כל מערכת בעולם מתנקזת ל-4 פעולות בסיסיות. הכרתן היא בסיס לכל עבודה עם מסדי נתונים.

פעולהתיאורHTTP Methodדוגמה
Createיצירת רשומה חדשהPOSTהוספת משתמש חדש
Readקריאת/שליפת נתון — ללא שינויGETשליפת רשימת משתמשים
Updateעדכון רשומה קיימתPUT / PATCHשינוי שם משתמש
Deleteמחיקת רשומהDELETEמחיקת חשבון
הרחבה PUT לעומת PATCH — מה ההבדל? ›

PUT — מחליף את כל הרשומה. אם שולחים PUT עם שם בלבד, שאר השדות ייאפסו.

PATCH — מעדכן רק את השדות שנשלחו. שאר השדות נשמרים כמות שהם.

בפרקטיקה: PATCH עדיף לרוב — לא מסכן מחיקת נתונים בטעות.

✏תרגול עצמי2 שאלות
שאלה 3.1
משתמש רוצה לשנות רק את כתובת האימייל שלו. איזו HTTP Method מתאימה ביותר?
✓ תשובה: ד) — PATCH מעדכן רק את השדות שנשלחו. PUT יחליף את כל הרשומה — כל שדה שלא נשלח ייאפס. לעדכון חלקי, PATCH הוא הבחירה הנכונה.
שאלה 3.2
API מחזיר קוד סטאטוס 201. מה המשמעות?
✓ תשובה: א) — 200 OK = הצליח. 201 Created = הצליח ונוצר משאב חדש (בדרך כלל כתשובה ל-POST). 400 = שגיאה בבקשה. 500 = שגיאת שרת.
// פרק 4

Mongoose — Schema כחוזה אבטחה

MongoDB גמיש מדי ועלול להיות מסוכן. Mongoose מוסיף Schema — חוזה מחייב שמסנן כל JSON לפני כניסה למסד. שדה לא מורשה? נמחק אוטומטית.

// 1. ייבוא הספרייה const mongoose = require('mongoose'); // 2. יצירת Schema — חוזה האבטחה const userSchema = new mongoose.Schema({ username: { type: String, required: true }, password: { type: String, required: true }, age: { type: Number }, // שדה אופציונלי isAdmin: { type: Boolean, default: false }, // ברירת מחדל בטוחה! }); // 3. הפיכת Schema למודל פעיל const User = mongoose.model('User', userSchema);
🔑מה הגנה הזו עושה
  • שדות שלא מוגדרים ב-Schema נמחקים אוטומטית — isAdmin: true של תוקף? נעלם.
  • required: true — MongoDB יסרב לשמור רשומה ללא שדה זה
  • default: false — גם אם הלקוח לא שלח את השדה, ערך ברירת המחדל הבטוח נשמר

חיבור שרת Express למסד — פעולת Create

app.post('/api/users', async (req, res) => { try { // המודל מסנן דרך ה-Schema ושומר — פעולה אסינכרונית! const newUser = await User.create(req.body); // מחזיר את המשתמש שנוצר (כולל ה-_id שמסד הנתונים הוסיף) res.status(201).json(newUser); } catch (error) { // Schema דחה את הנתונים (למשל, חסרה סיסמה) res.status(400).json({ error: error.message }); } });
! באג נפוץ — שכחת await ›
// ❌ קוד פגום — שולח Promise ריק, לא נתונים אמיתיים app.get('/api/users', (req, res) => { const allUsers = User.find({}); // חסר await! res.json(allUsers); // שולח Promise object }); // ✅ קוד תקין app.get('/api/users', async (req, res) => { const allUsers = await User.find({}); res.json(allUsers); // שולח מידע אמיתי });
הסיבה: פעולות מסד נתונים הן אסינכרוניות — לוקח זמן לקרוא מהדיסק. ללא await, JavaScript ממשיך לשורה הבאה לפני שהנתונים הגיעו.
✏תרגול עצמי3 שאלות
שאלה 4.1
תוקף שולח { username: "hack", password: "123", isAdmin: true } לשרת עם Mongoose Schema. מה יקרה?
✓ תשובה: א) — Mongoose מסנן שדות שלא בSchema. ה-isAdmin שנשלח מהלקוח יימחק, וה-isAdmin שיישמר יהיה false (ברירת המחדל שהגדרנו). זו הגנת Trust Boundary.
שאלה 4.2
מדוע פעולות מסד הנתונים (כמו User.find({})) דורשות async/await?
✓ תשובה: ב) — I/O (קריאה מדיסק, רשת) הוא אסינכרוני ב-Node.js. ללא await, הקוד ממשיך לשורה הבאה לפני שהנתונים הגיעו, ומחזיר Promise ריק במקום נתונים אמיתיים.
שאלה 4.3
מה ה-_id שמסד הנתונים מוסיף לכל רשומה?
✓ תשובה: ג) — MongoDB יוצר ObjectId ייחודי לכל מסמך אוטומטית. הוא מבוסס על זמן + מזהה מכונה + מספר רנדומלי, ומבטיח ייחודיות גם בסביבה מבוזרת.
// תרגול נוסף

תרגול נוסף — SQL: JOINs, Injection ו-Aggregation

שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.

✏תרגול נוסף3 שאלות
שאלה 5.1 [קל]
מה ההבדל בין INNER JOIN ל-LEFT JOIN?
✓ תשובה נכונה: א — INNER = חיתוך. LEFT = כל השמאלית + התאמות (NULL כשאין). RIGHT ו-FULL משלימים. ב-Debug: LEFT חושף שורות 'יתומות'.
שאלה 5.2 [בינוני]
מה תוציא השאילתה כאשר user_input = "admin'--"?query = f"SELECT * FROM users WHERE name='{user_input}' AND active=1"
✓ תשובה נכונה: ב — admin'-- → WHERE name='admin'-- AND active=1. ה--' הופך את השאר להערה. תוצאה: כל שורות admin, גם לא-פעילות. Login Bypass קלאסי.
שאלה 5.3 [קשה]
מה ה-OUTPUT של SELECT COUNT(*), AVG(age) FROM users WHERE age BETWEEN 18 AND 30 על [(Alice,25),(Bob,17),(Carol,28),(Dave,30)]?
✓ תשובה נכונה: ד — BETWEEN 18 AND 30 כולל קצוות. Bob(17) בחוץ. Alice(25)+Carol(28)+Dave(30) = 3 שורות. AVG = 83/3 ≈ 27.67.
// אתגר מעשי

אתגר מעשי — מסדי נתונים — Schema ו-SQL בטוח

שיא השיעור: לבנות רכיב עובד עם זווית אבטחה. ההגשה נבדקת אוטומטית.

T2 · בסיסי250 נק'~18 דק'build
ולידציית Schema (סגנון Mongoose)
ממש validate(schema, doc) המחזיר רשימה ממוינת של שגיאות. schema ממפה שדה→{'type':'str'|'int'|'bool', 'required':bool}. עבור כל שדה: שדה חובה חסר → "missing:FIELD"; טיפוס שגוי → "type:FIELD". שדה במסמך שאינו ב-schema → "extra:FIELD" (הגנה מפני mass-assignment). שים לב: True/False אינם int תקין.
דוגמאות
validate(S, {'name':'x','age':5}) → []
validate(S, {'age':5}) → ['missing:name']
validate(S, {'name':'x','role':'admin'}) → ['extra:role']
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס ולידציית Schema (סגנון Mongoose) ›
def validate(schema: dict, doc: dict) -> list: errors = []; types = {'str': str, 'int': int, 'bool': bool} for f, rule in schema.items(): if f not in doc: if rule.get('required'): errors.append(f"missing:{f}") else: t = types[rule['type']]; val = doc[f] if t is int and isinstance(val, bool): errors.append(f"type:{f}") elif not isinstance(val, t): errors.append(f"type:{f}") for f in doc: if f not in schema: errors.append(f"extra:{f}") # מונע mass-assignment return sorted(errors)
T3 · בינוני400 נק'~25 דק'build
בניית שאילתה בטוחה (נגד SQL Injection)
ממש build_query(table, filters) המחזיר (sql, params) לשאילתת SELECT מפורמטרת. ערכי המשתמש חייבים להפוך ל-placeholders (?) ולעולם לא לשרשור למחרוזת — כך מונעים SQL injection. עמודות ממוינות לפי א"ב. מזהי טבלה/עמודה (שאי אפשר לפרמטר) חייבים לעבור ולידציה מול ^[A-Za-z_]\w*$; מזהה פסול → זרוק ValueError. ללא פילטרים → "SELECT * FROM table".
דוגמאות
build_query('users', {'id':5}) → ('SELECT * FROM users WHERE id = ?', [5])
build_query('users', {'name':'a','age':2}) → ('SELECT * FROM users WHERE age = ? AND name = ?', [2, 'a'])
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס בניית שאילתה בטוחה (נגד SQL Injection) ›
import re def build_query(table: str, filters: dict): ident = re.compile(r'^[A-Za-z_]\w*$') if not ident.match(table): raise ValueError("bad table") # מזהים לא ניתן לפרמטר cols = sorted(filters) for c in cols: if not ident.match(c): raise ValueError("bad column") if not cols: return (f"SELECT * FROM {table}", []) where = " AND ".join(f"{c} = ?" for c in cols) # ערכים = placeholders return (f"SELECT * FROM {table} WHERE {where}", [filters[c] for c in cols])
// סיכום

סיכום — שיעור 20

ציידנו את המערכת שלנו בזיכרון לטווח ארוך. המערכת כעת Full Stack אמיתית — קולטת נתונים, מאמתת אותם ושומרת אותם לצמיתות.

✓מה למדנו בשיעור 20
  • RAM מול Disk — נתונים במשתנים נמחקים; מסד נתונים שומר לצמיתות
  • SQL — טבלאות, Schema נוקשה, Transactions. מתאים לבנקים ו-ERP
  • NoSQL/MongoDB — מסמכי JSON גמישים. מתאים לרשתות חברתיות
  • CRUD — Create (POST), Read (GET), Update (PUT/PATCH), Delete (DELETE)
  • Mongoose Schema — חוזה שמסנן שדות לא מורשים ומגן מ-Mass Assignment
  • async/await — פעולות DB לוקחות זמן; חובה להמתין לתוצאה
  • 201 Created — קוד סטאטוס מתאים לאחרי יצירת רשומה חדשה
📌 לפני שיעור 21 (סדנת קוד)
  1. חבר את הפרויקט שלך ל-MongoDB Atlas (Free Tier)
  2. צור Schema למשתמשים ולנתוני הפרויקט
  3. הוסף נתיבי CRUD לפרויקט (POST, GET לפחות)
שאלות תרגול ותשובות (10)

שאלה 1 · מה ההבדל המהותי בין SQL ל-NoSQL?

  • א. SQL מהיר יותר תמיד
  • ב. SQL עם סכמה קשיחה וטבלאות, NoSQL עם מסמכים גמישים
  • ג. NoSQL לא שומר לדיסק
  • ד. SQL לא תומך באינדקסים
תשובה: ב

SQL מתאים לנתונים מובנים עם קשרים; NoSQL לסכמה משתנה ולצמיחה אופקית.

שאלה 2 · מה תחזיר השאילתה?

SELECT name FROM users WHERE age > 18 ORDER BY name;
  • א. כל השדות של כל המשתמשים
  • ב. שמות המשתמשים מעל גיל 18, ממוינים אלפביתית
  • ג. את המשתמש הראשון בלבד
  • ד. את מספר המשתמשים
תשובה: ב

SELECT בוחר עמודה, WHERE מסנן שורות, ORDER BY ממיין את התוצאה.

שאלה 3 · מה עושה JOIN?

SELECT o.id, u.name
FROM orders o
JOIN users u ON o.user_id = u.id;
  • א. מאחד שורות משתי טבלאות לפי מפתח משותף
  • ב. מוחק שורות כפולות
  • ג. ממיין את הטבלה
  • ד. יוצר טבלה חדשה
תשובה: א

JOIN מחבר את הטבלאות דרך מפתח זר — כאן orders.user_id מול users.id.

שאלה 4 · מה מבטיח PRIMARY KEY?

  • א. שהעמודה מוצפנת
  • ב. שהערך ייחודי ואינו NULL
  • ג. שהעמודה ממוינת
  • ד. שהעמודה מסוג מספר
תשובה: ב

מפתח ראשי מזהה שורה באופן חד-ערכי ומקבל אינדקס אוטומטי.

שאלה 5 · מה הבעיה בקוד הזה?

const q = "SELECT * FROM users WHERE name = '" + input + "'";
  • א. חסר נקודה-פסיק
  • ב. SQL Injection — הקלט משורשר ישירות לשאילתה
  • ג. השאילתה איטית
  • ד. חסר ORDER BY
תשובה: ב

קלט כמו ' OR '1'='1 יחשוף את כל הטבלה. הפתרון: Prepared Statements עם פרמטרים.

שאלה 6 · מה מגדירה סכמת Mongoose?

const userSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  role: { type: String, default: "user" }
});
  • א. את מבנה המסמך והוולידציה לפני שמירה
  • ב. את כתובת השרת
  • ג. את אינדקסי הדיסק בלבד
  • ד. את הרשאות המשתמש
תשובה: א

MongoDB עצמה חסרת סכמה — Mongoose מוסיף שכבת מבנה, ברירות מחדל וּוולידציה באפליקציה.

שאלה 7 · מה הסכנה בקוד הזה?

app.post("/users", async (req, res) => {
  const user = await User.create(req.body);
  res.json(user);
});
  • א. חסר await
  • ב. Mass Assignment — המשתמש יכול לשלוח role: "admin"
  • ג. חסר סטטוס 201
  • ד. ה-JSON לא מפוענח
תשובה: ב

🎯 דגש למבחן — Mass Assignment: לעולם אל תעבירו את req.body כמו שהוא למסד. תוקף יוסיף שדות כמו isAdmin: true או role: "admin" וישיג הרשאות. תמיד בוחרים שדות מותרים במפורש (whitelist): User.create({ email: req.body.email, name: req.body.name }).

שאלה 8 · מה מטרת האינדקס?

CREATE INDEX idx_email ON users(email);
  • א. לדחוס את הטבלה
  • ב. להאיץ חיפוש על העמודה, במחיר כתיבה איטית יותר
  • ג. למנוע כפילויות
  • ד. להצפין את העמודה
תשובה: ב

אינדקס מונע סריקה מלאה של הטבלה, אך מתעדכן בכל INSERT/UPDATE.

שאלה 9 · מה מבטיחה טרנזקציה?

  • א. שהשאילתה תרוץ מהר
  • ב. שכל הפעולות מתבצעות במלואן או שאף אחת לא מתבצעת
  • ג. שהנתונים יגובו
  • ד. שהחיבור יישאר פתוח
תשובה: ב

אטומיות: אם שלב נכשל מבצעים ROLLBACK — קריטי בהעברת כסף בין חשבונות.

שאלה 10 · מה תחזיר השאילתה?

db.orders.find({ status: "paid", total: { $gt: 100 } });
  • א. הזמנות ששולמו בסכום מעל 100
  • ב. את כל ההזמנות
  • ג. הזמנות בסכום מתחת ל-100
  • ד. רק את שדה total
תשובה: א

find מקבל אובייקט תנאים; $gt הוא אופרטור "גדול מ-". התנאים מתקיימים יחד (AND).