FastApi - לעבוד עם מסד נתונים - ORM ואלכימיה

מחבר:
בתאריך:

אחרי שבמדריכים הקודמים בסדרת המוקדשת ל-FastApi למדנו ליצור API והקמנו מסד נתונים של mySQL. נא לעיין במדריכים לפני שאתם מתחילים עם המדריך הנוכחי. במדריך הנוכחי נחבר את ה-API עם מסד הנתונים דרך ספרייה מתווכת מסוג ORM שתאפשר לנו לבצע את השאילתות למסד הנתונים בפייתונית במקום באמצעות SQL.

fastapi orm sqlalchemy

האפליקציה שנכתוב בסדרת המדריכים תחזיק מידע על מכוניות ומשתמשים. להורדת הקוד אותו נפתח במדריך

 

הקמת האפליקציה

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

$ 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 נכתוב את הקוד שיאפשר לנו לתקשר עם מסד הנתונים. את הקוד לקחתי עם שינויים קלים משני מקורות:

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)

 

 

במדריכים הבאים בסדרה

  1. נוסיף ">קלאס לניהול משתמשים ונלמד כיצד ליצור יחסים בין מודלים.
  2. נאפשר ניהול משתמשים.
  3. נוסיף תבניות.
  4. נעבוד עם ה-api דרך אפליקצית צד משתמש הכתובה ב-JavaScript.

לכל המדריכים בסדרה ללימוד פייתון

 

אהבתם? לא אהבתם? דרגו!

0 הצבעות, ממוצע 0 מתוך 5 כוכבים

 

 

הוסף תגובה חדשה

 

= 4 + 7