# חיבור Routes לפעולות Database — הארכיטקטורה<no value>

## מה נלמד?

היום **מחברים הכול**: השרת מנושא Backend ו-FastAPI, המסד מימים 2–4. השיעור הזה מתמקד ב**ארכיטקטורה**, כלומר איך החלקים מתחברים, וממש את שני ה-Endpoints הראשונים (קריאה). בשיעור הבא, את השאר.

## למה צריך את זה?

בנושא Backend ו-FastAPI השרת שמר סטודנטים ב-**RAM**, והם נעלמו בכל הפעלה מחדש. עכשיו השרת ישמור **במסד**: נתונים שנשארים, עם אילוצים ו-Transactions. זו הארכיטקטורה הבסיסית של כמעט כל Backend בעולם: **Client → API → Database**.

## הסבר מהבסיס

### שני קבצים, שתי אחריויות

```text
students-api/
├── main.py     ← the API layer: routes, HTTP, validation, status codes
└── db.py       ← the data layer: SQL, connections, commit (from the databases topic)

MySQL in Docker (kodkod-mysql)  ← where the data lives
```

| | `main.py` | `db.py` |
|---|---|---|
| יודע על | HTTP, Requests, Status Codes, JSON, Pydantic | SQL, mysql.connector, טבלאות, commit |
| **לא** יודע על | SQL, חיבורים | HTTP, 404, FastAPI |
| שפת השגיאות | `HTTPException(404)` | `None`, `False`, `ValueError` |

זו **"הפרדת קוד בסיסית בין Route לבין קוד הגישה למסד"** שהסילבוס מבקש. ולמה זה טוב?

- **כל קובץ קטן וממוקד** (SRP, נושא Python–3).
- **בודקים כל שכבה לבד**: את `db.py` כבר בדקנו אתמול, בלי שרת.
- **מחליפים שכבה בלי לגעת בשנייה**: מעבר ל-PostgreSQL ישנה רק את `db.py`. ממשק אחר (אפליקציית שורת פקודה) ישתמש באותו `db.py`.

### טבלת המיפוי

| Endpoint | פונקציה ב-db.py | SQL | הצלחה | כישלונות |
|---|---|---|---|---|
| `GET /students` | `get_all_students()` | SELECT | 200 + list | — |
| `GET /students/{id}` | `get_student(id)` | SELECT WHERE id | 200 + dict | `None` → **404** |
| `POST /students` | `create_student(...)` | INSERT | **201** + dict | `ValueError` ‏←‏ **409**. ולידציה ‏←‏ **400** / **422** |
| `PUT /students/{id}` | `update_student(id, ...)` | UPDATE WHERE id | 200 + dict | `None` → **404**. `ValueError` → **409** |
| `DELETE /students/{id}` | `delete_student(id)` | DELETE WHERE id | **204** | `False` → **404** |

**כל Route עושה שלושה דברים:** (1) מקבל ומוודא קלט (FastAPI + ולידציה), (2) קורא לפונקציה אחת ב-`db.py`, (3) מתרגם את התוצאה ל-HTTP.

### בקשה אחת, מקצה לקצה

```text
GET /students/2
   │
   ▼  Uvicorn receives the HTTP request (port 8000)
   ▼  FastAPI finds the route, converts "2" → 2 (int), calls get_student(2)
   ▼  main.get_student  →  db.get_student(2)
   ▼  db: open connection → SELECT ... WHERE id = %s (2) → fetchone → dict → close
   ▼  main: dict is not None → return it
   ▼  FastAPI: dict → JSON, status 200
   ▼  Uvicorn sends the HTTP response
{"id": 2, "name": "Avi", "age": 31, "email": "avi@mail.com"}
```

**כל השבועות במסלול אחד:** HTTP ו-Port (נושא מחשב, מערכת הפעלה ורשת), פונקציות ו-dict ו-JSON (נושא Python), Classes ו-Pydantic (נושא OOP), SQL (נושא Backend ומסדי נתונים).

{{< box "analogy" "אנלוגיה: מסעדה, שוב" >}}
**`main.py`** = המלצר: מקבל הזמנות מהסועדים, בודק שהן הגיוניות ("אין לנו פיצה בתפריט"), מעביר למטבח, ומגיש. **`db.py`** = המחסנאי במטבח: יודע איפה כל מצרך נמצא, מוציא ומכניס. המלצר **לא** נכנס למחסן, והמחסנאי **לא** מדבר עם הסועדים. כל אחד בתפקיד שלו.
{{< /box >}}

### מתי נוצרת הטבלה?

`db.init_db()` נקראת **פעם אחת, כשהשרת עולה** (ברמה הראשית של `main.py`). בגלל `IF NOT EXISTS`, זה בטוח בכל הפעלה: בפעם הראשונה הטבלה נוצרת, ובשאר הפעמים לא קורה כלום.

## תחביר

```python
from fastapi import FastAPI, HTTPException

import db                                   # our data layer (same folder)

app = FastAPI(title="Students API")
db.init_db()                                # create the table once, on startup


@app.get("/students/{student_id}")
def get_student(student_id: int):
    student = db.get_student(student_id)    # 1 call to the data layer
    if student is None:                     # translate "not found" to HTTP
        raise HTTPException(status_code=404, detail="student not found")
    return student                          # dict → JSON (200)
```

**שמות זהים בשתי שכבות:** הפונקציה ב-`main.py` נקראת `get_student`, וגם זו ב-`db.py`. אין התנגשות, כי קוראים לזו של המסד **דרך שם המודול**: `db.get_student(...)` (נושא Python!).

## דוגמת קוד ראשונה

**הקמת הפרויקט** (הקונטיינר של MySQL צריך לרוץ — `docker start kodkod-mysql`):

```powershell
mkdir ~\dev\kodkod6\week-4\students-api
cd ~\dev\kodkod6\week-4\students-api
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install "fastapi[standard]" mysql-connector-python
git init
```

`.gitignore`:

```text
.venv/
__pycache__/
.env
```

**העתיקו את `db.py` מנושא מסדי הנתונים** (CRUD מלא) לתיקייה. ו-`main.py`, בשלב הזה רק שני Endpoints:

```python
from fastapi import FastAPI, HTTPException

import db

app = FastAPI(title="Students API")
db.init_db()


@app.get("/students")
def list_students():
    return db.get_all_students()


@app.get("/students/{student_id}")
def get_student(student_id: int):
    student = db.get_student(student_id)
    if student is None:
        raise HTTPException(status_code=404, detail="student not found")
    return student
```

**הוספת נתונים לבדיקה** (המסד ריק! עוד אין POST). `seed.py`:

```python
import db

db.init_db()
for name, age, email in [("Dana", 24, "dana@mail.com"), ("Avi", 31, "avi@mail.com")]:
    try:
        print(db.create_student(name, age, email))
    except ValueError as error:
        print("Skipped:", error)
```

```powershell
python seed.py
uvicorn main:app --reload
```

**בדפדפן:**

- `http://127.0.0.1:8000/students`
- `http://127.0.0.1:8000/students/2`
- `http://127.0.0.1:8000/students/99`

**תשובות:**

```json
[{"id":1,"name":"Dana","age":24,"email":"dana@mail.com"},{"id":2,"name":"Avi","age":31,"email":"avi@mail.com"}]
```

```json
{"id":2,"name":"Avi","age":31,"email":"avi@mail.com"}
```

```json
{"detail":"student not found"}
```

## פירוק הקוד שורה אחר שורה

| קוד | הסבר |
|---|---|
| `import db` | מייבאים את שכבת הנתונים. `main.py` ו-`db.py` באותה תיקייה (נושא Python). |
| `db.init_db()` | רץ פעם אחת כש-Uvicorn טוען את `main.py`: הטבלה מוכנה לפני הבקשה הראשונה. |
| `def list_students(): return db.get_all_students()` | **Route בשורה אחת.** הרשימה (של dicts) הופכת ל-JSON. |
| `student_id: int` | FastAPI ממיר וּבודק (`/students/abc` → 422, נושא Backend ו-FastAPI). |
| `db.get_student(student_id)` | **קריאה אחת** לשכבת הנתונים. |
| `if student is None: raise HTTPException(404)` | **תרגום**: "לא נמצא" של `db.py` → 404 של HTTP. |
| `seed.py` | **Seed** = נתוני התחלה. סקריפט נפרד, ש**משתמש באותו `db.py`**. הוכחה שהשכבה לא תלויה בשרת. |

**ועכשיו הניסוי החשוב:** עצרו את השרת (`Ctrl + C`), והפעילו שוב. רעננו את `/students`. **הנתונים שם!** בניגוד לנושא Backend ו-FastAPI, הם במסד — בשרת ה-MySQL שבקונטיינר, שכותב אותם ל-Storage (נושא מחשב, מערכת הפעלה ורשת!). אפשר גם לראות אותם ב-Database Client.

## דוגמה נוספת

**Endpoint נוסף, שמשתמש ב-db.py עם Query Parameter: חיפוש.** הוסיפו ל-`db.py` את `search_students` מתרגול אתמול, ול-`main.py`:

```python
@app.get("/students/search")
def search_students(name: str):
    if len(name.strip()) < 2:
        raise HTTPException(status_code=400, detail="search text must have at least 2 characters")
    return db.search_students(name)
```

**איפה בקובץ?** `/students/search` הוא נתיב **קבוע**, ולכן הוא חייב להיות **לפני** `/students/{student_id}` (נושא Backend ו-FastAPI, טעות 3!). אחרת `search` ייתפס כ-student_id → 422.

```text
GET /students/search?name=av   → 200 [{"id": 2, "name": "Avi", ...}]
GET /students/search?name=a    → 400 {"detail": "search text must have at least 2 characters"}
GET /students/search           → 422 (name is required)
```

**שימו לב לחלוקת העבודה:** ה**ולידציה** (לפחות 2 תווים) ב-`main.py`, כי זה כלל של ה-API. ה**SQL** (`LIKE`, `lower`) ב-`db.py`.

## טעות נפוצה

**הטעות 1: SQL בתוך ה-Route.**

```python
@app.get("/students/{student_id}")
def get_student(student_id: int):
    connection = mysql.connector.connect(
        host="localhost", user="root", password="kodkod", database="school")
    cursor = connection.cursor()
    cursor.execute("SELECT * FROM students WHERE id = %s", (student_id,))
    ...
```

"עובד", אבל עכשיו יש SQL בשני מקומות, פרטי חיבור בשני מקומות, ו-Route שעושה הכול. **Route = קלט + קריאה ל-db + תרגום.**

**הטעות 2: להחזיר `None` מה-Route.**

```python
@app.get("/students/{student_id}")
def get_student(student_id: int):
    return db.get_student(student_id)
```

לסטודנט שלא קיים מוחזר **200** עם `null`. ה-Client יחשוב שההצלחה. **"לא נמצא" = 404.**

**הטעות 3: השרת לא עולה — הקונטיינר של MySQL לא רץ.** `db.init_db()` רץ כש-Uvicorn טוען את `main.py`, ולכן השרת נופל כבר בהפעלה:

```text
mysql.connector.errors.InterfaceError: 2003: Can't connect to MySQL server on 'localhost:3306' ...
```

בודקים עם `docker ps`, ומפעילים: `docker start kodkod-mysql` (נושא מסדי נתונים ו-MySQL!).

**הטעות 4: `ModuleNotFoundError: No module named 'db'`.** `db.py` לא באותה תיקייה כמו `main.py`, או שמריצים את uvicorn מתיקייה אחרת. מריצים מתיקיית הפרויקט.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## תרגול

התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.

### תרגיל 1: שתי שכבות

לכל שורת קוד — האם היא שייכת ל-`main.py` (שכבת ה-HTTP) או ל-`db.py` (שכבת הנתונים)?

1. `cursor.execute("SELECT * FROM students WHERE id = %s", (student_id,))`
2. `raise HTTPException(status_code=404, detail="student not found")`
3. `@app.get("/students/{student_id}")`
4. `mysql.connector.connect(host="localhost", user="root", ...)`
5. `return db.get_student(student_id)`

{{< solution >}}
1. **db.py** — SQL.
2. **main.py** — קוד HTTP.
3. **main.py** — Route.
4. **db.py** — פרטי החיבור למסד.
5. **main.py** — השרת קורא לשכבת הנתונים.

הכלל: **main.py מדבר HTTP, db.py מדבר SQL**. אף אחד מהם לא יודע על הפרטים של השני.
{{< /solution >}}

### תרגיל 2: Routes לקריאה

יש לכם את `db.py` מהשיעור הקודם (עם `init_db`, `get_all_students` ו-`get_student`). כתבו `main.py` עם:

- `db.init_db()` בהפעלת השרת;
- `GET /students` — כל הסטודנטים;
- `GET /students/{student_id}` — סטודנט אחד, או 404 עם `"student not found"`.

{{< solution >}}
**main.py**

```py
from fastapi import FastAPI, HTTPException

import db

app = FastAPI(title="Students API")
db.init_db()


@app.get("/students")
def list_students():
    return db.get_all_students()


@app.get("/students/{student_id}")
def get_student(student_id: int):
    student = db.get_student(student_id)
    if student is None:
        raise HTTPException(status_code=404, detail="student not found")
    return student
```

`db.get_student` מחזירה `None` כשאין סטודנט — ו-**main.py מתרגם** את זה ל-404. זה כל התפקיד של שכבת ה-HTTP.
{{< /solution >}}

### תרגיל 3: בקשה אחת — מקצה לקצה

סדרו את מה שקורה בבקשה `GET /students/2`:

- (א) `db.get_student(2)` מריצה `SELECT ... WHERE id = %s` עם `(2,)`
- (ב) FastAPI ממיר את `"2"` מהנתיב ל-`int`
- (ג) התשובה חוזרת כ-JSON עם 200
- (ד) `main.get_student(2)` נקראת
- (ה) השורה חוזרת כמילון

{{< solution >}}
הסדר: **(ב), (ד), (א), (ה), (ג)**

```text
GET /students/2
  -> FastAPI: student_id = 2 (int)
  -> main.get_student(2)
  -> db.get_student(2)
  -> SELECT ... WHERE id = %s   (2,)
  -> {'id': 2, 'name': 'Avi', ...}
  -> JSON, 200
```
{{< /solution >}}

### תרגיל 4: Endpoint חיפוש

הוסיפו ל-`main.py` את `GET /students/search?name=...`, שמשתמש ב-`db.search_students`. שם קצר מ-2 תווים ‏←‏ 400. איפה צריך להגדיר את ה-Route, ולמה?

{{< solution >}}
**main.py** (בתוספת ל-Routes הקודמים)

```py
@app.get("/students/search")
def search_students(name: str):
    if len(name.strip()) < 2:
        raise HTTPException(status_code=400, detail="too short")
    return db.search_students(name)
```

- `?name=av` → 200 עם הרשימה. `?name=a` → **400**. בלי `name` → **422** (Query חובה, כי אין לו ברירת מחדל).
- את `/students/search` מגדירים **לפני** `/students/{student_id}` — אחרת FastAPI ינסה להמיר את `"search"` ל-int ויחזיר 422.
{{< /solution >}}

### תרגיל 5: מה לא בסדר?

```text
@app.get("/students/{student_id}")
def get_student(student_id: int):
    connection = mysql.connector.connect(
        host="localhost", user="root", password="kodkod", database="school")
    cursor = connection.cursor()
    cursor.execute("SELECT * FROM students WHERE id = %s", (student_id,))
    return cursor.fetchone()
```

מצאו שלוש בעיות בקוד הזה.

{{< solution >}}
1. **SQL בתוך ה-Route** — ערבוב שכבות. אם נשנה את מבנה הטבלה, נצטרך לחפש SQL בכל ה-Routes. המקום שלו ב-`db.py`.
2. **החיבור לא נסגר** — אין `close()`, ובוודאי לא ב-`finally`. בשרת שרץ כל הזמן, זה מצטבר.
3. **סטודנט שלא קיים חוזר כ-200 עם `null`** — צריך לבדוק `None` ולהחזיר 404. וגם בלי `dictionary=True`, הסטודנט חוזר כרשימה בלי שמות שדות, במקום מילון.

התיקון: `return db.get_student(student_id)` עם בדיקת `None` — כמו בתרגיל השני.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- **`main.py`** = שכבת ה-API: Routes, ולידציה וקודי סטטוס. **`db.py`** = שכבת הנתונים: SQL, חיבורים ו-commit. אין SQL ב-Routes.
- **מיפוי:** GET→SELECT, POST→INSERT (201), PUT→UPDATE, DELETE→DELETE (204). `None`/`False` → 404. `ValueError` → 409.
- **Route = קלט ‏←‏ קריאה אחת ל-db ‏←‏ תרגום ל-HTTP.**
- `db.init_db()` פעם אחת בעליית השרת (`IF NOT EXISTS`).
- נתיבים קבועים (`/students/search`) **לפני** `/students/{student_id}`.
- הנתונים **שורדים** הפעלה מחדש של השרת, כי הם במסד.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## דוקומנטציה רשמית

- [FastAPI — Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/) — חלוקת הקוד לקבצים
- [FastAPI — Handling Errors](https://fastapi.tiangolo.com/tutorial/handling-errors/) — HTTPException כשרשומה לא נמצאה

<!-- docs:end -->
