קישור אובייקטים של מסד הנתונים כשעובדים עם FastApi - קצר ולעניין
SQLAlchemy מאפשר ליצור 3 סוגי קשרים (Relationships) בין קלאסים וטבלאות במסד הנתונים:
-
One-to-Many: הנפוץ ביותר
-
Many-to-Many: נפוץ פחות
-
One-to-One : כמעט לא בשימוש
איך מתחילים?
נייבא את ה-relationships מתוך ספריית SQLAlchemy:
models.py
from sqlalchemy.orm import relationship
from sqlalchemy import Column, Integer, String, ForeignKey
דבר המאפשר להוסיף לכל אחד מהמודלים את ה- relationship למודל האחר.
1. One-to-Many : יחיד לרבים
הקשר הנפוץ ביותר הוא One-to-Many "יחיד לרבים".
לדוגמה: לכל יוזר יכולים להיות כמה פרויקטים. כל פרויקט יכול להיות שייך רק ליוזר אחד.
הדרך המומלצת ליצירת קשר One-to-Many מיושמת באמצעות back_populates:
models.py
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship, declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
user_name = Column(String)
projects = relationship("Project", back_populates="owner", cascade="all, delete")
class Project(Base):
__tablename__ = "projects"
id = Column(Integer, primary_key=True)
name = Column(String)
owner_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"))
owner = relationship("User", back_populates="projects")
נסביר:
-
התכונה `owner` שהוספנו לקלאס `Project` יוצרת את ה-relationship ל-`User`, מה שמאפשר לנו לגשת למידע על המשתמש שהפרויקט שייך לו באמצעות `project.owner`.
מצד שני, התכונה `projects` שהוספנו לקלאס `User` מאפשרת לשלוף את רשימת הפרויקטים השייכים למשתמש.
# in User projects = relationship("Project", back_populates="owner", cascade="all, delete") # in Project owner = relationship("User", back_populates="projects")מאפשר יצירת קשר דו כווני: מיוזר לפרויקטים שלו, ומפרויקט ליוזר שהוא הבעלים. מה שמועיל ל-API.
-
המפתח הזר, Foreign Key, מתווסף כשדה למסד הנתונים.
-
השינויים במודלים חייבים להתבטא במסד הנתונים. בשביל הלימוד והאימון אפשר למחוק את הטבלאות ולהריץ מחדש את האפליקציה כדי שייבנו מחדש אולם בסביבת עבודה אמיתית מומלץ להשתמש בכלי migration כמו Alembic.
-
back_populates מגדיר קשר דו-כיווני מפורש אשר נדרש כאשר רוצים שליטה על כל צד בנפרד.
מצד אחד, `User.projects` יודע אילו פרויקטים שייכים למשתמש.
מצד שני, `Project.owner` יודע מי המשתמש שהוא הבעלים של הפרויקטים.
זה מועיל במיוחד כשרוצים גישה פרטנית לכל אחד מהצדדים. לדוגמה, לצורך מתן הרשאות, מיון פריטים, או פורמט JSON שונה לכל צד. כך למשך, לא רוצים להכליל אימייל בפרטי פרויקט מטעמי אבטחת מידע.
-
cascade="all, delete" מבטיח שמחיקת יוזר מוחקת אוטומטית את כל הפרויקטים שלו. פה המקום של המפתח לשאול: "האם זה מה שבאמת צריך?"
נקודה שחשוב להבין היא ש cascade="all, delete" עובד ברמת הקוד של פייתון. בשביל שיעבוד גם ברמת מסד הנתונים צריך להוסיף ondelete="CASCADE" על ה-ForeignKey של Project.
כתיבת קוד המודל אינה מספיקה! עלינו לציין בסכימות את הקשרים שיצרנו בין האובייקטים (User ו-Project).
לשם כך, נוסיף לסכימה את התלות List כי ייתכן יוזר שלו יותר מפרויקט אחד:
scheme.py
from typing import List, Optional
from pydantic import BaseModel
# Shared attributes between Project input/output
class ProjectBase(BaseModel):
name: str # Project name only, no ID here
# For creating a new project
class ProjectCreate(ProjectBase):
owner_id: int # Foreign key to associate with a user
# Minimal representation of the user (public-safe)
class UserPublic(BaseModel):
user_name: Optional[str] # Public field only
# Output schema for project, includes its owner
class ProjectOut(ProjectBase):
id: int
owner: Optional[UserPublic] # Nested minimal user info
class Config:
from_attributes = True # Enables ORM-to-Pydantic conversion
# Input schema for creating a user
class UserCreate(BaseModel):
user_name: str
# Output schema for user, includes their projects
class UserOut(UserPublic):
projects: List[ProjectOut] = [] # A user can have many projects
class Config:
from_attributes = True
נסביר:
-
הסדר חשוב! כיוון ש-UserOut תלוי ב-ProjectOut צריך למקם את הקלאס ProjectOut לפניו כי פייתון מפרש את הקוד שורה אחר שורה.
-
from_attributes=True מאפשר להשתמש באובייקטי ORM ישירות ב-FastApi, מה שמפשט ומאיץ את התהליך.
-
בנוסף, כדי שהסכימה ProjectOut תציג מידע על הבעלים נוסיף לה שדה owner כשם השדה במודל אשר יכיל מידע על המשתמש.
-
מטעמי בטחון הגדרנו UserPublic מינימלי אשר יורש את BaseModel ומכיל אך ורק את השדות ההכרחיים בלי להסגיר פרטים אישיים.
להורדת קוד הבדיקה של One-to-many
2. Many-to-Many : רבים לרבים
הקשר מסוג Many-to-Many מאפשר לשני הצדדים בקשר להכיל מידע של הצד השני.
לדוגמה: משתמשים יכולים להשתתף בכמה קבוצות (Groups), וכל קבוצה יכולה להכיל כמה משתמשים.
בשונה מקשר One-to-Many, הקשר Many-to-Many מצריך יצירת טבלה מקשרת (association table) לאחסון ה-id הייחודי של כל אחד מקלאסים בקשר.
הדרך המומלצת ליישום קשר Many-to-Many מיושמת באמצעות relationship עם secondary:
models.py
from sqlalchemy import Column, Integer, String, ForeignKey, Table
from sqlalchemy.orm import relationship, declarative_base
Base = declarative_base()
# Association table
user_group_table = Table(
"user_group",
Base.metadata,
Column("user_id", ForeignKey("users.id", ondelete="CASCADE"), primary_key=True),
Column("group_id", ForeignKey("groups.id", ondelete="CASCADE"), primary_key=True)
)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
user_name = Column(String)
groups = relationship("Group", secondary=user_group_table, back_populates="members")
class Group(Base):
__tablename__ = "groups"
id = Column(Integer, primary_key=True)
name = Column(String)
members = relationship("User", secondary=user_group_table, back_populates="groups")
נסביר:
- התכונה groups שנוספה למודל User מאפשרת לנו לשלוף את הקבוצות בהן המשתמש חבר. מצד שני, members במודל Group מכילה את המשתמשים החברים בקבוצה.
- במקום ForeignKey רגיל, קשר מסוג Many-to-Many מבוסס על association table שבה כל שורה מייצגת זוג: יוזר - קבוצה.
- שימוש ב־ondelete="CASCADE" בשדות ForeignKey של טבלת הקשר מבטיח שניקוי בטבלאות הראשיות ינקה גם את טבלת הקשר (במסד תומך).
- back_populates מגדיר קשר דו-כיווני כמו בקשר One-to-Many, רק שכאן באמצעות טבלת הקשר.
כדי שהשינויים יתבטאו במסד הנתונים, יש למחוק ולבנות מחדש את הטבלאות (או להשתמש בכלי migration כמו Alembic).
כפי שכבר הבנו, כתיבת קוד המודל אינה מספיקה! יש לעדכן גם את הסכימות (schemas.py) כדי לאפשר עבודה עם האובייקטים Group ו־User בצורה נוחה ובטוחה.
נייבא את התלוית הדרושות וניישם את הסכימות:
schemas.py
from typing import List, Optional
from pydantic import BaseModel
# Public User: Used when nesting inside Group output
class UserPublic(BaseModel):
user_name: str
class Config:
from_attributes = True
# Public Group: Used when nesting inside User output
class GroupPublic(BaseModel):
id: int
name: str
class Config:
from_attributes = True
# Input schema for creating a new user
class UserCreate(BaseModel):
user_name: str
# Input schema for creating a new group
class GroupCreate(BaseModel):
name: str
# Output for a full user, including the groups they belong to
class UserOut(BaseModel):
id: int
user_name: str
groups: List[GroupPublic] = []
class Config:
from_attributes = True
# Output schema for a full group, including its members
class GroupOut(BaseModel):
id: int
name: str
members: List[UserPublic] = []
class Config:
from_attributes = True
נסביר:
-
למה הפיצול בין UserPublic ו-UserOut? כדי למנוע הפניות מעגליות: UserOut > GroupOut > UserOut > GroupOut > ... השימוש ב-UserPublic (חשוב שזו תהיה גרסה מינימלית מטעמי אבטחה) בתוך GroupOut מונעת את היווצרות הלולאה.
להורדת קוד הבדיקה של Many-to-many
3. One-to-One : אחד לאחד
קשר מסוג One-to-One מגדיר קשר חד-חד ערכי בין שתי טבלאות, והשימוש בו נדיר למדי. דוגמה (קצת מלאכותית): לכל יוזר יכולה להיות רק כתובת אחת, ולכל כתובת רק יוזר אחד.
זהו קשר דו-כיווני, שבו כל שורה בטבלה הראשונה מתאימה בדיוק לשורה אחת בטבלה השנייה.
כדי לממש קשר כזה, נשתמש גם כאן ב־relationship עם back_populates, אך נוסיף עליו גם uselist=False כדי לציין שהקשר הוא יחיד ולא רשימה.
נתחיל מכתיבת קוד המודל:
models.py
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship, declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
user_name = Column(String)
address = relationship("Address", back_populates="resident", uselist=False, cascade="all, delete")
class Address(Base):
__tablename__ = "addresses"
id = Column(Integer, primary_key=True)
street = Column(String)
city = Column(String)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), unique=True)
resident = relationship("User", back_populates="address")
נסביר:
-
השדה address במודל User מקשר לכתובת אחת בלבד, ולכן יש להשתמש ב־uselist=False.
-
השדה resident במודל Address מצביע חזרה ליוזר שאליו היא שייכת.
-
unique=True על ה־ForeignKey מבטיח ברמת מסד הנתונים שאין יותר מכתובת אחת לאותו יוזר.
-
כרגיל, back_populates מגדיר את הקשר ההדדי באופן מפורש.
-
השימוש ב־ondelete="CASCADE" יחד עם cascade="all, delete" מבטיח שכתובת תימחק אוטומטית אם המשתמש נמחק גם במסד הנתונים וגם בקוד.
גם כאן, כתיבת קוד המודל אינה מספיקה!
עלינו לעדכן את הסכימות כדי לאפשר עבודה נוחה ובטוחה עם המידע.
נוסיף את סכימות הבסיס:
from pydantic import BaseModel
from typing import Optional
# === Base Schemas ===
class UserBase(BaseModel):
user_name: str
class AddressBase(BaseModel):
street: str
city: str
# === Input Schemas ===
class UserCreate(UserBase):
pass
class AddressCreate(AddressBase):
user_id: int
# === Public/Minimal Schemas for nesting ===
class UserPublic(BaseModel):
id: int
user_name: str
class Config:
from_attributes = True
class AddressPublic(BaseModel):
id: int
street: str
city: str
class Config:
from_attributes = True
# === Output Schemas ===
class AddressOut(AddressPublic):
resident: Optional[UserPublic] = None
class Config:
from_attributes = True
class UserOut(UserPublic):
address: Optional[AddressPublic] = None
class Config:
from_attributes = True
נסביר:
-
הסכימה AddressOut כוללת את השדה resident, כדי לדעת למי שייכת הכתובת.
-
לעומת זאת, ב־ UserOut כללנו את הכתובת (address) אך ללא מידע על המשתמש כדי להימנע מלולאה אין סופית.
-
גם כאן יצרנו סכימה פומבית UserPublic שלא כוללת שדות רגישים.
-
הסדר חשוב! AddressOut תלויה ב־ UserPublic, לכן היא חייבת להופיע אחריה.
לסיכום:
קשר One-to-One מייצר התאמה בלעדית בין שני אובייקטים, אך מחייב אותנו לדייק בהגדרות:
- uselist=False
- מפתח ייחודי (unique=True)
במקרים רבים עדיף לשקול האם הקשר הזה באמת דרוש, או שאפשר לממש אותו כ־ One-to-Many שבו הצד השני לא יכיל יותר מרשומה אחת בפועל.
סיכום
חיווט relationships בין מודלים ב־ SQLAlchemy אולי נראה מסורבל בהתחלה, אבל עם קצת אימון זה יבוא, ויותר חשוב פרקטיקה זו משפרת את הקריאות של הקוד, מקצרת את זמן הדיבוג, ומאפשרת שליטה מדויקת על איזה מידע נחשף ואיך.
שלושת סוגי הקשרים:
-
One-to-Many: הקשר הנפוץ והקל ביותר להבנה
-
Many-to-Many: מצריך טבלה מתווכת
-
One-to-One: נדיר אך שימושי כשצריך התאמה חד-חד ערכית
כמו שראינו, כל קשר דורש מיפוי גם במודלים וגם בסכימות, וזה מה שמאפשר ל-FastApi להחזיר תוצאות מדויקות ומבוקרות מצד אחד, ולשלוח קלטים מסודרים מצד שני.
אם תקפידו על הגדרות נכונות (back_populates, uselist, cascade, ondelete) תחסכו שעות של עמל ותסכול בהמשך.
מדריכים נוספים בסדרה על FastApi
אפליקצית אינטרנט עם FastApi - הקמת מסד הנתונים
FastApi - לעבוד עם מסד נתונים - ORM ואלכימיה
שליחת מייל עם FastApi עבודה עם פונקציה א-סינכרונית
לכל המדריכים בסדרה ללימוד פייתון
אהבתם? לא אהבתם? דרגו!
0 הצבעות, ממוצע 0 מתוך 5 כוכבים
המדריכים באתר עוסקים בנושאי תכנות ופיתוח אישי. הקוד שמוצג משמש להדגמה ולצרכי לימוד. התוכן והקוד המוצגים באתר נבדקו בקפידה ונמצאו תקינים. אבל ייתכן ששימוש במערכות שונות, דוגמת דפדפן או מערכת הפעלה שונה ולאור השינויים הטכנולוגיים התכופים בעולם שבו אנו חיים יגרום לתוצאות שונות מהמצופה. בכל מקרה, אין בעל האתר נושא באחריות לכל שיבוש או שימוש לא אחראי בתכנים הלימודיים באתר.
למרות האמור לעיל, ומתוך רצון טוב, אם נתקלת בקשיים ביישום הקוד באתר מפאת מה שנראה לך כשגיאה או כחוסר עקביות נא להשאיר תגובה עם פירוט הבעיה באזור התגובות בתחתית המדריכים. זה יכול לעזור למשתמשים אחרים שנתקלו באותה בעיה ואם אני רואה שהבעיה עקרונית אני עשוי לערוך התאמה במדריך או להסיר אותו כדי להימנע מהטעיית הציבור.
שימו לב! הסקריפטים במדריכים מיועדים למטרות לימוד בלבד. כשאתם עובדים על הפרויקטים שלכם אתם צריכים להשתמש בספריות וסביבות פיתוח מוכחות, מהירות ובטוחות.
המשתמש באתר צריך להיות מודע לכך שאם וכאשר הוא מפתח קוד בשביל פרויקט הוא חייב לשים לב ולהשתמש בסביבת הפיתוח המתאימה ביותר, הבטוחה ביותר, היעילה ביותר וכמובן שהוא צריך לבדוק את הקוד בהיבטים של יעילות ואבטחה. מי אמר שלהיות מפתח זו עבודה קלה ?
השימוש שלך באתר מהווה ראייה להסכמתך עם הכללים והתקנות שנוסחו בהסכם תנאי השימוש.