בעמוד הזה
CRUD מלא מתוך Python: מודול db.py שלם ובדיקת כל ארבע הפעולות
- להרכיב מודול db.py שלם עם כל פעולות ה-CRUD על Entity אחת
- להסביר לכל פונקציה מה היא מחזירה בכל מצב — dict, list, None, bool או ValueError
- לבדוק את כל ארבע הפעולות ואת מקרי הקצה שלהן בתסריט אחד
- להיות מוכנים לחבר את המודול לשרת FastAPI
מה נלמד?
בסוף היום הזה, כמו שהסילבוס מגדיר, יש לנו CRUD מלא מתוך Python. השיעור מרכיב את כל החלקים מימים 2–4 למודול אחד, db.py, ובודק אותו מקצה לקצה.
| CRUD | פונקציה | SQL | מחזירה |
|---|---|---|---|
| (הכנה) | init_db() | CREATE TABLE IF NOT EXISTS | — |
| Create | create_student(name, age, email) | INSERT | dict (או ValueError על אימייל כפול) |
| Read (כולם) | get_all_students() | SELECT | list של dicts (אולי ריקה) |
| Read (אחד) | get_student(student_id) | SELECT ... WHERE id = %s | dict או None |
| Update | update_student(student_id, name, age, email) | UPDATE ... WHERE id = %s | dict, None, או ValueError |
| Delete | delete_student(student_id) | DELETE ... WHERE id = %s | True / False |
למה צריך את זה?
מחר מחברים את המודול הזה לשרת. כל Endpoint יהיה שורות ספורות, שקוראות לפונקציה אחת מכאן ומתרגמות את התוצאה ל-HTTP. כדי שזה יעבוד בלי הפתעות, המודול צריך להיות שלם, עקבי ובדוק, עם “חוזה” ברור: מה כל פונקציה מחזירה בכל מצב.
הסבר מהבסיס
“חוזה” של פונקציה
חוזה (Contract) = ההבטחה של הפונקציה למי שקורא לה: מה היא מקבלת, מה היא מחזירה, ואילו שגיאות היא עלולה לזרוק. הטבלה למעלה היא החוזה של db.py. השרת יסתמך עליה:
get_student(...) returned None → 404
create_student(...) raised ValueError → 409
delete_student(...) returned False → 404עקרונות שמשותפים לכל הפונקציות
- חיבור לכל פעולה —
get_connection(), ו-close()ב-finally. - פרמטרים בלבד —
%s, לעולם לא שרשור. - commit אחרי כל שינוי.
- dict החוצה —
cursor(dictionary=True). - “לא נמצא” =
None/False, ולא Exception. - הפרת אילוץ =
ValueErrorעם הודעה ברורה. - אין HTTP, אין
print— המודול לא יודע מי משתמש בו (שרת, סקריפט, תפריט).
תחביר
המבנה של המודול:
db.py
├── get_connection() ← one place that opens connections (host, user, password, database)
├── init_db() ← create the table if needed
├── get_all_students() ← R (list)
├── get_student(student_id) ← R (one)
├── create_student(...) ← C
├── update_student(...) ← U
└── delete_student(student_id) ← Dדוגמת קוד ראשונה
db.py המלא (בפרויקט week-4/school-db):
import mysql.connector
def get_connection():
return mysql.connector.connect(
host="localhost", user="root", password="kodkod", database="school")
def init_db():
connection = get_connection()
try:
cursor = connection.cursor()
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS students (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
age INT NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE
)
"""
)
finally:
connection.close()
def get_all_students():
connection = get_connection()
try:
cursor = connection.cursor(dictionary=True)
cursor.execute("SELECT id, name, age, email FROM students ORDER BY id")
return cursor.fetchall()
finally:
connection.close()
def get_student(student_id):
connection = get_connection()
try:
cursor = connection.cursor(dictionary=True)
cursor.execute(
"SELECT id, name, age, email FROM students WHERE id = %s", (student_id,))
return cursor.fetchone()
finally:
connection.close()
def create_student(name, age, email):
connection = get_connection()
try:
cursor = connection.cursor()
cursor.execute(
"INSERT INTO students (name, age, email) VALUES (%s, %s, %s)",
(name, age, email))
connection.commit()
new_id = cursor.lastrowid
except mysql.connector.IntegrityError:
raise ValueError("a student with email " + email + " already exists")
finally:
connection.close()
return get_student(new_id)
def update_student(student_id, name, age, email):
connection = get_connection()
try:
cursor = connection.cursor()
cursor.execute(
"UPDATE students SET name = %s, age = %s, email = %s WHERE id = %s",
(name, age, email, student_id))
connection.commit()
except mysql.connector.IntegrityError:
raise ValueError("a student with email " + email + " already exists")
finally:
connection.close()
return get_student(student_id)
def delete_student(student_id):
connection = get_connection()
try:
cursor = connection.cursor()
cursor.execute("DELETE FROM students WHERE id = %s", (student_id,))
connection.commit()
return cursor.rowcount > 0
finally:
connection.close()פירוק הקוד שורה אחר שורה
| פונקציה | מה חדש בה / מה לשים לב |
|---|---|
get_connection | המקום היחיד שפותח חיבור — וגם היחיד שמכיר את פרטי החיבור. |
init_db | IF NOT EXISTS ← בטוח לקרוא לה בכל הפעלה של השרת. |
get_all_students | ORDER BY id לסדר יציב. רשימה ריקה היא תשובה תקינה. |
get_student | None כשלא נמצא. כל השאר משתמשים בה כדי להחזיר “את מה שבמסד”. |
create_student | mysql.connector.IntegrityError → ValueError. new_id נשמר בתוך ה-try, ו-get_student נקראת אחרי הסגירה. |
update_student | שלושה מצבים: dict / None / ValueError (אימייל תפוס). את “לא נמצא” קובעת get_student אחרי העדכון — לא rowcount, כי ב-MySQL עדכון לאותם ערכים מחזיר rowcount 0. |
delete_student | True / False. לא מחזירה את מה שנמחק (ב-API: 204, בלי גוף). |
שימו לב לחזרה על "SELECT id, name, age, email ...": בפונקציות של נושא מסדי נתונים ו-MySQL בתרגול העצמאי ראינו קבוע COLUMNS שמונע את החזרה. כאן השארנו את השאילתות מלאות, כדי שכל פונקציה תהיה קריאה לבד, ללמידה. שתי הגישות לגיטימיות.
דוגמה נוספת
תסריט בדיקה לכל ארבע הפעולות — “תרגול כל ארבע הפעולות על Entity אחת” מהסילבוס. check_crud.py:
import db
connection = db.get_connection()
connection.cursor().execute("DROP TABLE IF EXISTS students")
connection.close()
db.init_db()
print("--- CREATE ---")
dana = db.create_student("Dana", 24, "dana@mail.com")
avi = db.create_student("Avi", 31, "avi@mail.com")
print(dana)
print(avi)
try:
db.create_student("Dana Copy", 20, "dana@mail.com")
except ValueError as error:
print("Duplicate rejected:", error)
print("--- READ ---")
print("All:", db.get_all_students())
print("Id 2:", db.get_student(2))
print("Id 99:", db.get_student(99))
print("--- UPDATE ---")
print("Update id 2:", db.update_student(2, "Avi Levi", 32, "avi@mail.com"))
print("Update id 2 again:", db.update_student(2, "Avi Levi", 32, "avi@mail.com"))
print("Update id 99:", db.update_student(99, "Ghost", 30, "ghost@mail.com"))
try:
db.update_student(2, "Avi Levi", 32, "dana@mail.com")
except ValueError as error:
print("Email clash rejected:", error)
print("--- DELETE ---")
print("Delete id 1:", db.delete_student(1))
print("Delete id 1 again:", db.delete_student(1))
print("All at the end:", db.get_all_students())פלט:
--- CREATE ---
{'id': 1, 'name': 'Dana', 'age': 24, 'email': 'dana@mail.com'}
{'id': 2, 'name': 'Avi', 'age': 31, 'email': 'avi@mail.com'}
Duplicate rejected: a student with email dana@mail.com already exists
--- READ ---
All: [{'id': 1, 'name': 'Dana', 'age': 24, 'email': 'dana@mail.com'}, {'id': 2, 'name': 'Avi', 'age': 31, 'email': 'avi@mail.com'}]
Id 2: {'id': 2, 'name': 'Avi', 'age': 31, 'email': 'avi@mail.com'}
Id 99: None
--- UPDATE ---
Update id 2: {'id': 2, 'name': 'Avi Levi', 'age': 32, 'email': 'avi@mail.com'}
Update id 2 again: {'id': 2, 'name': 'Avi Levi', 'age': 32, 'email': 'avi@mail.com'}
Update id 99: None
Email clash rejected: a student with email dana@mail.com already exists
--- DELETE ---
Delete id 1: True
Delete id 1 again: False
All at the end: [{'id': 2, 'name': 'Avi Levi', 'age': 32, 'email': 'avi@mail.com'}]מה נבדק: כל אחת מארבע הפעולות, גם במסלול השמח וגם בכל מקרה קצה: כפילות, לא נמצא, עדכון חוזר לאותם ערכים, התנגשות ומחיקה חוזרת. כל שורה בפלט היא שורה ב"חוזה". ומחר, כל שורה כזו תהפוך לבקשת HTTP עם קוד סטטוס.
ופתחו את טבלת students ב-Database Client: נשאר רק Avi Levi, עם id 2.
טעות נפוצה
הטעות 1: חוזה לא עקבי. get_student מחזירה None כשלא נמצא, אבל update_student זורקת KeyError ו-delete_student מחזירה 0. השרת יצטרך לטפל בכל פונקציה אחרת. מצב “לא נמצא” באותה צורה בכל המודול.
הטעות 2: print בתוך db.py. print("Student created!") בתוך create_student. בשרת זה ילך לשום מקום, ובבדיקות זה “רעש”. שכבת הנתונים מחזירה ערכים. מי שקורא לה מחליט מה להציג. (ללוג ← logger.)
הטעות 3: מחיקה בתסריט הבדיקה בלי לחשוב. DROP TABLE IF EXISTS students בראש check_crud.py מוחק את כל הנתונים. מעולה לבדיקה, אסון אם מריצים בטעות מול מסד אמיתי. (בהמשך המסלול תלמדו להריץ בדיקות על מסד נפרד, למשל school_test.)
הטעות 4: לבדוק רק את ה-Create וה-Read. הבאגים הכי מעניינים מתחבאים ב-UPDATE (סדר פרמטרים, WHERE) וב-DELETE (rowcount). כל ארבע הפעולות, עם מקרי קצה.
תרגול
התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.
תרגיל 1: החוזה של המודול
כתבו טבלת “חוזה” לחמש הפונקציות של db.py: מה כל אחת מקבלת, מה היא מחזירה, ומה קורה כשאין סטודנט כזה.
get_all_students get_student create_student update_student delete_studentהצג פתרון
| פונקציה | מקבלת | מחזירה | אם אין סטודנט |
|---|---|---|---|
get_all_students() | — | רשימה של מילונים | רשימה ריקה |
get_student(id) | id | מילון | None |
create_student(name, age, email) | הנתונים | מילון (עם id) | — (אימייל כפול ← ValueError) |
update_student(id, name, age, email) | id + הנתונים | מילון מעודכן | None |
delete_student(id) | id | True | False |
חוזה אחיד = השרת יודע בדיוק מה לצפות מכל פונקציה, ואיך לתרגם כל תוצאה לקוד HTTP.
תרגיל 2: create_student עם תרגום שגיאה
כתבו את get_connection, init_db, get_student ו-create_student, שמתרגמת אימייל כפול ל-ValueError.
init_db()
print(create_student("Dana", 24, "dana@mail.com"))
try:
create_student("Copy", 20, "dana@mail.com")
except ValueError as error:
print("Duplicate rejected:", error)פלט צפוי:
{'id': 1, 'name': 'Dana', 'age': 24, 'email': 'dana@mail.com'}
Duplicate rejected: a student with email dana@mail.com already existsהצג פתרון
import mysql.connector
def get_connection():
return mysql.connector.connect(
host="localhost", user="root", password="kodkod", database="school")
def init_db():
connection = get_connection()
try:
cursor = connection.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS students (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
age INT NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE
)
""")
finally:
connection.close()
def get_student(student_id):
connection = get_connection()
try:
cursor = connection.cursor(dictionary=True)
cursor.execute("SELECT * FROM students WHERE id = %s", (student_id,))
return cursor.fetchone()
finally:
connection.close()
def create_student(name, age, email):
connection = get_connection()
try:
cursor = connection.cursor()
cursor.execute(
"INSERT INTO students (name, age, email) VALUES (%s, %s, %s)",
(name, age, email))
connection.commit()
new_id = cursor.lastrowid
except mysql.connector.IntegrityError:
raise ValueError("a student with email " + email + " already exists")
finally:
connection.close()
return get_student(new_id)
init_db()
print(create_student("Dana", 24, "dana@mail.com"))
try:
create_student("Copy", 20, "dana@mail.com")
except ValueError as error:
print("Duplicate rejected:", error){'id': 1, 'name': 'Dana', 'age': 24, 'email': 'dana@mail.com'}
Duplicate rejected: a student with email dana@mail.com already existscreate_student לא שולפת בעצמה — היא קוראת ל-get_student עם ה-id החדש. ה-finally סוגר את החיבור גם כשנזרקת ValueError.
תרגיל 3: המודול המלא
השלימו את db.py עם get_all_students, update_student ו-delete_student (לפי החוזה), ובדקו את כל הפעולות.
init_db()
dana = create_student("Dana", 24, "dana@mail.com")
avi = create_student("Avi", 31, "avi@mail.com")
print(len(get_all_students()))
print(update_student(avi["id"], "Avi Levi", 32, "avi@mail.com"))
print(update_student(99, "X", 1, "x@mail.com"))
print(delete_student(dana["id"]), delete_student(dana["id"]))
print(get_student(dana["id"]))
print(get_all_students())פלט צפוי:
2
{'id': 2, 'name': 'Avi Levi', 'age': 32, 'email': 'avi@mail.com'}
None
True False
None
[{'id': 2, 'name': 'Avi Levi', 'age': 32, 'email': 'avi@mail.com'}]הצג פתרון
import mysql.connector
def get_connection():
return mysql.connector.connect(
host="localhost", user="root", password="kodkod", database="school")
def init_db():
connection = get_connection()
try:
cursor = connection.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS students (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
age INT NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE
)
""")
finally:
connection.close()
def get_all_students():
connection = get_connection()
try:
cursor = connection.cursor(dictionary=True)
cursor.execute("SELECT * FROM students ORDER BY id")
return cursor.fetchall()
finally:
connection.close()
def get_student(student_id):
connection = get_connection()
try:
cursor = connection.cursor(dictionary=True)
cursor.execute("SELECT * FROM students WHERE id = %s", (student_id,))
return cursor.fetchone()
finally:
connection.close()
def create_student(name, age, email):
connection = get_connection()
try:
cursor = connection.cursor()
cursor.execute(
"INSERT INTO students (name, age, email) VALUES (%s, %s, %s)",
(name, age, email))
connection.commit()
new_id = cursor.lastrowid
except mysql.connector.IntegrityError:
raise ValueError("a student with email " + email + " already exists")
finally:
connection.close()
return get_student(new_id)
def update_student(student_id, name, age, email):
connection = get_connection()
try:
cursor = connection.cursor()
cursor.execute(
"UPDATE students SET name = %s, age = %s, email = %s WHERE id = %s",
(name, age, email, student_id))
connection.commit()
except mysql.connector.IntegrityError:
raise ValueError("a student with email " + email + " already exists")
finally:
connection.close()
return get_student(student_id)
def delete_student(student_id):
connection = get_connection()
try:
cursor = connection.cursor()
cursor.execute("DELETE FROM students WHERE id = %s", (student_id,))
connection.commit()
return cursor.rowcount > 0
finally:
connection.close()
init_db()
dana = create_student("Dana", 24, "dana@mail.com")
avi = create_student("Avi", 31, "avi@mail.com")
print(len(get_all_students()))
print(update_student(avi["id"], "Avi Levi", 32, "avi@mail.com"))
print(update_student(99, "X", 1, "x@mail.com"))
print(delete_student(dana["id"]), delete_student(dana["id"]))
print(get_student(dana["id"]))
print(get_all_students())2
{'id': 2, 'name': 'Avi Levi', 'age': 32, 'email': 'avi@mail.com'}
None
True False
None
[{'id': 2, 'name': 'Avi Levi', 'age': 32, 'email': 'avi@mail.com'}]כל פונקציה עומדת בחוזה: מילון / None / True / False / ValueError — בלי שום דבר של mysql.connector שדולף החוצה.
תרגיל 4: חיפוש לפי שם
לתרגיל הזה צרו טבלה פשוטה, רק עם שמות — דנה, אבי ודניאל:
import mysql.connector
connection = mysql.connector.connect(
host="localhost", user="root", password="kodkod", database="school")
cursor = connection.cursor()
cursor.execute("DROP TABLE IF EXISTS students") # start clean, so ids start from 1
cursor.execute("CREATE TABLE students (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100))")
cursor.execute("INSERT INTO students (name) VALUES ('Dana'), ('Avi'), ('Daniel')")
connection.commit()
connection.close()הוסיפו search_students(text) שמחזירה רשימה של מילונים עם כל הסטודנטים שהשם שלהם מכיל את הטקסט, בעזרת LIKE ו-%, ממוינים לפי id.
print(search_students("an"))פלט צפוי:
[{'id': 1, 'name': 'Dana'}, {'id': 3, 'name': 'Daniel'}]הצג פתרון
import mysql.connector
def get_connection():
return mysql.connector.connect(
host="localhost", user="root", password="kodkod", database="school")
def search_students(text):
connection = get_connection()
try:
cursor = connection.cursor(dictionary=True)
cursor.execute(
"SELECT * FROM students WHERE name LIKE %s ORDER BY id",
("%" + text + "%",))
return cursor.fetchall()
finally:
connection.close()
connection = get_connection()
cursor = connection.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS students "
"(id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100))")
for name in ["Dana", "Avi", "Daniel"]:
cursor.execute("INSERT INTO students (name) VALUES (%s)", (name,))
connection.commit()
connection.close()
print(search_students("an"))[{'id': 1, 'name': 'Dana'}, {'id': 3, 'name': 'Daniel'}]LIKEמשווה לפי תבנית.%= “כל רצף של תווים”.- את ה-
%מוסיפים לערך — לא לפקודה — כך שגם החיפוש נשאר בטוח מ-SQL Injection.
תרגיל 5: תסריט בדיקה
למה כדאי לכתוב תסריט שבודק את כל חמש הפונקציות של db.py, ולהריץ אותו על טבלה ריקה?
הצג פתרון
- בדיקה אחת שמכסה הכול — אחרי כל שינוי במודול מריצים את התסריט ורואים מיד אם משהו נשבר, במקום לבדוק ידנית כל פונקציה.
- טבלה ריקה — התוצאות צפויות: ה-id הראשון הוא 1, הרשימה מתחילה ריקה. על טבלה עם נתונים ישנים, אותה בדיקה הייתה מחזירה תוצאות שונות בכל הרצה.
- בודקים גם כישלונות: id שלא קיים, אימייל כפול, מחיקה כפולה — שם מסתתרים רוב הבאגים.
(מתחילים את התסריט ב-DROP TABLE IF EXISTS students ואז init_db() — ועדיף על מסד בדיקות נפרד, למשל school_test, כדי לא למחוק נתונים אמיתיים.)
סיכום
db.py= CRUD מלא על Entity אחת:init_db,create,get_all,get,update,delete.- חוזה עקבי: list / dict /
None/True-False/ValueError. “לא נמצא” תמיד באותה צורה. - כל פונקציה: חיבור משלה,
%s, commit,cursor(dictionary=True), ו-finally: close(). - אין HTTP ואין print בשכבת הנתונים. היא מחזירה ערכים, ומי שקורא מחליט.
- תסריט בדיקה שעובר על כל ארבע הפעולות ועל מקרי הקצה. מחר כל שורה בו תהפוך לבקשת HTTP.
דוקומנטציה רשמית
- Connector/Python — Coding Examples — כל הדוגמאות הרשמיות במקום אחד
- Python Tutorial — Modules — מודול db.py