FastApi - לעבוד עם מסד נתונים - ORM ואלכימיה
אחרי שבמדריכים הקודמים בסדרת המוקדשת ל-FastApi למדנו ליצור API והקמנו מסד נתונים של mySQL. נא לעיין במדריכים לפני שאתם מתחילים עם המדריך הנוכחי. במדריך הנוכחי נחבר את ה-API עם מסד הנתונים דרך ספרייה מתווכת מסוג ORM שתאפשר לנו לבצע את השאילתות למסד הנתונים בפייתונית במקום באמצעות SQL.
האפליקציה שנכתוב בסדרת המדריכים תחזיק מידע על מכוניות ומשתמשים. להורדת הקוד אותו נפתח במדריך
הקמת האפליקציה
רגע לפני שנחזור לעבוד עם הסביבה הוירטואלית נתקין את החבילה הבאה בהתקנה גלובלית:
$ sudo apt-get install libmysqlclient-dev
כדי לעבוד עם הסביבה הוירטואלית בתוכה היתקנו את האפליקציה במדריך המבוא ל-Fast Api נכנס לתיקיית הפרויקט ונריץ את הפקודה הבאה:
$ source venv/bin/activate
נתקין בתוך הסביבה הוירטואלית את החבילות להם זקוק FastApi כדי לעבוד עם מסד נתונים mySQL:
(venv) $ pip3 install mysqlclient
(venv) $ pip3 install mysql-connector-python
(venv) $ pip3 install pymysql
(venv) $ pip3 install sqlalchemy
-
3 החבילות הראשונות מאפשרות לפייתון לעבוד עם mySQL.
-
sqlalchemy הוא ORM ראשי תיבות של Object Relational Mapping המאפשר לעבוד עם מסד הנתונים בשפה שלך (במקרה שלנו, פייתון) במקום ב-SQL. במקום לעבוד עם טבלאות במסד הנתונים עובדים עם קלאסים המייצגים אותם. בהתאם השדות של הקלאס הם שמות העמודות. הודות לשימוש ב-ORM הקוד נקי, קריא ובטוח יותר, ואף ניתן להחליף את מסד הנתונים בקלות רבה (לדוגמה, לעבור מ-mySQL ל- PostgreSQL).
מבנה תיקיית הפרויקט:
HELLO_FASTAPI
├── __init__.py
├── database.py
├── main.py
├── models.py
├── requirements.txt
├── schemas.py
├── settings.py
├── routers
│ ├── __init__.py
│ └── cars.py
├── .gitignore
- __init__.py - הופך את הפרויקט לחבילת תוכנה package.
- .gitignore - לתוכו נוסיף את שמות הקבצים שאנחנו לא רוצים שיעלו ל-git. לדוגמה, לא נרצה להעלות את הקובץ settings.py שמכיל את פרטי הגישה למסד הנתונים.
- database.py - יוצר את הקשר עם מסד הנתונים.
- main.py - הוא קובץ ניתוב routing המקשר בין חלקי האפלקיציה.
- models.py - מודלים של ORM שמחזיקים בתוכם את המידע על הטבלאות במסד הנתונים: שם הטבלאות והטורים.
- schemas.py - המודלים של pydantic שמגדירים את סוג הנתונים באפליקציה. לדוגמה, Cars או Users.
- התיקייה routers תכיל את הלוגיקה של ה-CRUD עבור כל אחד מהקלאסים.
נוסיף את פריט הגישה למסד הנתונים לקובץ settings.py בתוך מילון ששמו DATABASE:
settings.py
DATABASE = {
'username': 'joe',
'password': '12345',
'db_name': 'cars_db'
}
נוסיף את שם הקובץ settings.py לקובץ .gitignore כדי שלא נעלה בטעות את פרטי ההתחברות לאפליקציה ל- git:
.gitignore
settings.py
בתוך database.py נכתוב את הקוד שיאפשר לנו לתקשר עם מסד הנתונים. את הקוד לקחתי עם שינויים קלים משני מקורות:
- https://fastapi.tiangolo.com/tutorial/sql-databases/
- https://docs.sqlalchemy.org/en/14/core/engines.html
database.py
# set the db connection
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import settings
# SQLALCHEMY_DATABASE_URL = "sqlite:///./cars.db"
SQLALCHEMY_DATABASE_URL = f"mysql+mysqlconnector://{settings.DATABASE['username']}:{settings.DATABASE['password']}@localhost/{settings.DATABASE['db_name']}"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
- SQLALCHEMY_DATABASE_URL - מכיל את פרטי הגישה למסד הנתונים. נקפיד לא לקודד את פרטי הגישה למסד הנתונים ישירות לקבצים שעשויים להגיע ל-git אלא שולפים אותם מקובץ settings.py.
- במשתנה engine נשתמש עוד מעט.
- מקלאס Base ירשו כל המודלים של ה-ORM.
- במתודה get_db נשתמש כדי להתחבר למסד הנתונים מהמתודות שלנו. בסיום השימוש המתודה סוגרת את החיבור.
בקובץ models.py נכתוב את המודלים של ה-ORM. בשלב זה קיים אחד בלבד:
models.py
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.sql import func
from database import Base
class Car(Base):
__tablename__ = "cars"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(80), unique=False, index=False)
price = Column(Integer)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
active = Column(Integer, default=0)
- המשתנה __tablename__ = "cars" מכיל את שם הטבלה במסד הנתונים.
- השדות של הקלאס מכילים את שם העמודה וסוג הנתונים בטבלת ה-SQL במסד הנתונים.
הסכימות של pydantic
הסכימות של pydantic הם קלאסים שמגדירים עם איזה מידע ה-API רשאי לעבוד. הם אינם עובדים ישירות עם מסד הנתונים כי הרבה פעמים אנחנו רוצים להסתיר חלק מהמידע למשתמשים.
את הסכימות נרכז בקובץ schemas.py:
schemas.py
from pydantic import BaseModel
from typing import Optional
class Car(BaseModel):
name: str
price: int
active: Optional[bool]
class Config:
orm_mode = True
- הקלאס כולל רק חלק מהשדות.
- הקלאס Config מאפשר לסכימות של pydantic לעבוד עם מבני הנתונים שמספקים המודלים של ה-ORM.
אפשר ליצור יותר מקלאס אחד עבור אותה טבלה במסד הנתונים עבור מקרי שימוש שונים. נוסיף את הקלאס CarOut שמאפשר לעבוד עם השדות שם ומחיר בלבד מתוך כל השדות האפשריים:
schemas.py
class CarOut(BaseModel):
name: str
price: int
class Config:
orm_mode = True
קבצי router שמרכזים את הלוגיקה
הקבצים בתיקייה ששמה router מרכזים את הלוגיקה של כל אחד מהקלאסים. בתוך הקובץ routers/cars.py נכתוב את המתודות שאחראיות לעשות CRUD - הוספה, עדכון, קריאה ומחיקה עבור מכוניות.
נייבא את התלויות:
routers/cars.py
from fastapi import APIRouter, Depends, HTTPException
from typing import Optional, List
from sqlalchemy import asc, desc
from sqlalchemy.orm import Session
import models, schemas, database
נוסיף את הסט שמגדיר את התכונות הכלליות עבור כל המתודות:
routers/cars.py
router = APIRouter(
prefix="/cars",
tags=["cars"]
)
- הקידומת cars תבוא לפני הנתיב של כל אחד מהנתיבים בקובץ.
- tags מקבץ את המתודות ביחד בדף התיעוד docs. כפי שנראה בהמשך.
הוספת פריט
המתודה create_car מוסיפה פריטים למסד הנתונים:
routers/cars.py
@router.post('/', status_code=201)
def create_car(request: schemas.Car):
pass
- הדקורטור app מורה לאפליקציה לעבוד ב-post ולהחזיר קוד תגובה 201 במקרה של הצלחה בהוספת הפריט.
- הנתיב הוא /cars אבל אנחנו מסתפקים ב-/ כי הגדרנו את הקידומת בסט router.
- סוג המידע שהמתודה מצפה לקבל היא על פי המודל של schemas.Car - מה שאומר שצריך שיהיו לו שדות name, price ו- active.
את המידע שמגיע מה-API צריך לתרגם לפריט מידע שאותו נזין ל-ORM:
routers/cars.py
@router.post('/', status_code=201)
def create_car(request: schemas.Car):
db_item = models.Car()
db_item.name = request.name
db_item.price = request.price
db_item.active = request.active
את המידע צריך להעביר לתוך מסד הנתונים אז נשתמש ב-session שיצרנו בקובץ database.py:
routers/cars.py
@router.post('/', status_code=201)
def create_car(request: schemas.Car, db: Session = Depends(database.get_db)):
db_item = models.Car()
db_item.name = request.name
db_item.price = request.price
db_item.active = request.active
עכשיו, אחרי שיש לנו גישה למתודה get_db, אנחנו יכולים לתקשר עם ה-ORM שמתרגם את קוד הפייתון שלנו ל-SQL - שפת מסד הנתונים:
routers/cars.py
@router.post('/cars', status_code=201)
def create_car(request: schemas.Car, db: Session = Depends(database.get_db)):
db_item = models.Car()
db_item.name = request.name
db_item.price = request.price
db_item.active = request.active
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
- הפקודה add מורה על הוספת רשומה חדשה למסד הנתונים.
- הפקודה commit מבצעת את הפעולה.
- refresh מאפשרת לקבל חזרה את הרשומה שזה עתה הוספנו.
- הפונקציה מחזירה את הרשומה שהוזנה למסד הנתונים.
שליפת רשימת הפריטים
המתודה get_cars שולפת רשימה של מכוניות ממסד הנתונים:
routers/cars.py
@router.get('/', response_model=List[schemas.CarOut])
def get_cars(db: Session = Depends(database.get_db)):
return db.query(models.Car).all()
- השליפה היא ב-get. כל אחד מפריטי הרשימה המוחזרת צריך להתאים לסכימה CarOut.
- המתודה תלויה ב-session המקשר עם ה-ORM.
- את השאילתה הוצאתי מדף השאילתות של sqlalchemy https://docs.sqlalchemy.org/en/14/orm/query.html והיא שולפת את רשימת התוצאות באמצעות המתודה all.
אפשר לסנן, לדוגמה, לשלוף רק רשומות פעילות באמצעות המתודה filter:
routers/cars.py
@router.get('/cars', response_model=List[schemas.CarOut])
def get_cars(active: bool = True, sort: Optional[str] = None, db: Session = Depends(database.get_db)):
return db.query(models.Car).filter(models.Car.active == active).all()
אפשר להגביל את כמות התוצאות המוחזרות. הדוגמה הבאה תחזיר לכל היותר 10 רשומות:
routers/cars.py
@router.get('/cars', response_model=List[schemas.CarOut])
def get_cars(limit = 10, active: bool = True, sort: Optional[str] = None, db: Session = Depends(database.get_db)):
return db.query(models.Car).limit(limit).filter(models.Car.active == active).all()
אפשר לסדר לפי הסדר בתוך העמודה. בדוגמה הבאה נעביר את שם העמודה לפיה אנחנו מעוניינים לסדר ואם אנחנו מצפים לסדר עולה asc או יורד desc:
routers/cars.py
@router.get('/')
def get_cars(limit = 10, active: bool = True, sort_by: Optional[str] = None, sort_dir: Optional[str] = "desc", db: Session = Depends(database.get_db)):
#return db.query(models.Car).all()
cars = db.query(models.Car).filter(models.Car.active == active)
if sort_by:
direction = desc if sort_dir == 'desc' else asc
cars = cars.order_by(direction(getattr(models.Car, sort_by)))
cars = cars.limit(limit).all()
return cars
שליפת פריט נבחר
ניתן לשלוף פריט אחד בלבד. לדוגמה על פי המזהה הייחודי - id.
routers/cars.py
@router.get('/{id}', status_code = 200, response_model=schemas.CarOut)
def get_car(id: int, db: Session = Depends(database.get_db)):
car = db.query(models.Car).filter(models.Car.id == id).first()
if not car:
raise HTTPException(status_code=404, detail=f"Item #{id} not found")
return car
- המתודה get_car מוצאת את הפריט במסד הנתונים על פי ה-id שלו. אם הכול תקין יוחזר קוד 200.
- הפרמטר response_model מגדיר את הסכימה של התגובה. במקרה זה, הסכימה מגבילה לשדות name ו-price בלבד.
- משתמשים במתודה first() במקום ב- all() כדי להחזיר רשומה בודדת במקום רשימה.
- במצב שלא תמצא הרשומה, המתודה תזרוק Exception, קוד 404 עם הודעה שהפריט לא נמצא.
עדכון רשומה יחידה
update_car היא המתודה שתשמש לעדכון רשומה אחת:
routers/cars.py
@router.put("/{id}", status_code=202)
def update_car(id: int, request: schemas.Car, db: Session = Depends(database.get_db)):
car = db.query(models.Car).filter(models.Car.id == id)
if not car.first():
raise HTTPException(status_code=404, detail=f"Item #{id} not found")
car.update({'name': request.name, 'price': request.price})
db.commit()
- במקרה של הצלחה נחזיר קוד תגובה 202 או 404 במידה והפריט לא קיים.
עדכון שדות ספציפיים בפריט הנבחר
כפי שניתן לעדכן את כל השדות של פריט, אפשר לעדכן שדה מסוים בלבד. לדוגמה, עדכון השדה active:
routers/cars.py
@router.put("/{id}/set-activity", status_code=202)
def update_car(id: int, active: bool, db: Session = Depends(database.get_db)):
car = db.query(models.Car).filter(models.Car.id == id)
if not car.first():
raise HTTPException(status_code=404, detail=f"Item #{id} not found")
car.update({'active': active})
db.commit()
מחיקה
המתודה delete_car מוחקת פריט במסד הנתונים ומחזירה קוד תגובה 204:
routers/cars.py
@router.delete('/{id}', status_code=204)
def delete_car(id: int, db: Session = Depends(database.get_db)):
car = db.query(models.Car).filter(models.Car.id == id)
if not car.first():
raise HTTPException(status_code=404, detail=f"Item #{id} not found")
car.delete(synchronize_session=False)
db.commit()
קובץ הניווט והפעלת האפליקציה
נייבא את המודלים מ-models.py לקובץ main.py. בתוכו נוסיף את שורת הקוד הבאה כדי ליצור את הטבלאות במסד הנתונים על סמך המודלים.
main.py
# Core packages
import uvicorn
from fastapi import FastAPI
from database import engine
# our scripts
from routers import cars
import models
# create the database
models.Base.metadata.create_all(bind=engine)
# init app
app = FastAPI()
app.include_router(cars.router)
if __name__ == '__main__':
uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True, access_log=False)
השורה:
models.Base.metadata.create_all(bind=engine)
מייצרת את הטבלאות במסד הנתונים על פי המידע שמגיע מ-models.py.
השורה:
app.include_router(cars.router)
קוראת לקובץ הניתוב (router) של cars.
נפעיל את האפליקציה בתוך הסביבה הוירטואלית:
(venv) $ python3 main.py
נוודא שהטבלה התווספה למסד הנתונים:
mysql> SHOW TABLES;
+-------------------+
| Tables_in_cars_db |
+-------------------+
| cars |
+-------------------+
1 row in set (0.00 sec)
במדריכים הבאים בסדרה
- נוסיף ">קלאס לניהול משתמשים ונלמד כיצד ליצור יחסים בין מודלים.
- נאפשר ניהול משתמשים.
- נוסיף תבניות.
- נעבוד עם ה-api דרך אפליקצית צד משתמש הכתובה ב-JavaScript.
לכל המדריכים בסדרה ללימוד פייתון
אהבתם? לא אהבתם? דרגו!
0 הצבעות, ממוצע 0 מתוך 5 כוכבים
המדריכים באתר עוסקים בנושאי תכנות ופיתוח אישי. הקוד שמוצג משמש להדגמה ולצרכי לימוד. התוכן והקוד המוצגים באתר נבדקו בקפידה ונמצאו תקינים. אבל ייתכן ששימוש במערכות שונות, דוגמת דפדפן או מערכת הפעלה שונה ולאור השינויים הטכנולוגיים התכופים בעולם שבו אנו חיים יגרום לתוצאות שונות מהמצופה. בכל מקרה, אין בעל האתר נושא באחריות לכל שיבוש או שימוש לא אחראי בתכנים הלימודיים באתר.
למרות האמור לעיל, ומתוך רצון טוב, אם נתקלת בקשיים ביישום הקוד באתר מפאת מה שנראה לך כשגיאה או כחוסר עקביות נא להשאיר תגובה עם פירוט הבעיה באזור התגובות בתחתית המדריכים. זה יכול לעזור למשתמשים אחרים שנתקלו באותה בעיה ואם אני רואה שהבעיה עקרונית אני עשוי לערוך התאמה במדריך או להסיר אותו כדי להימנע מהטעיית הציבור.
שימו לב! הסקריפטים במדריכים מיועדים למטרות לימוד בלבד. כשאתם עובדים על הפרויקטים שלכם אתם צריכים להשתמש בספריות וסביבות פיתוח מוכחות, מהירות ובטוחות.
המשתמש באתר צריך להיות מודע לכך שאם וכאשר הוא מפתח קוד בשביל פרויקט הוא חייב לשים לב ולהשתמש בסביבת הפיתוח המתאימה ביותר, הבטוחה ביותר, היעילה ביותר וכמובן שהוא צריך לבדוק את הקוד בהיבטים של יעילות ואבטחה. מי אמר שלהיות מפתח זו עבודה קלה ?
השימוש שלך באתר מהווה ראייה להסכמתך עם הכללים והתקנות שנוסחו בהסכם תנאי השימוש.