SqlAlchemy
Jedną z najpopularniejszych bibliotek ORM (Object-Relational Mapping) w Pythonie jest SqlAlchemy. Pozwala ona na tworzenie modeli bazodanowych w Pythonie, które są mapowane na tabele w bazie danych. SqlAlchemy pozwala na tworzenie zapytań SQL w Pythonie, bez konieczności pisania czystego SQL-a. Działa to trochę jak Django ORM.
Minimalny przykład
Omówienie minimalnego przykładu (bazującego na dokumentacji FastAPI)
Layout projektu:
.
└── sql_app
├── __init__.py
├── database.py - konfiguracja bazy danych
├── models.py - opisy modeli
├── crud.py - Create, Read, Update, and Delete. Funkcje do obsługi bazy danych
├── main.py
Plik database.py
:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
#określenie bazy z którą będziemy się łączyć
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
#I utworzenie silnika bazy danych
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
#II utworzenie lokalnej sesji
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
- I - silnik jest obiektem służącym min. do tworzenia połączeń z bazą. Często jest on po prostu globalnym obiektem tworzonym raz dla danego serwera bazodanowego. (omówienie silnika)
- II - TODO nie wiem, czem nie zwykła sesja innty tutorial
PLik models.py
:
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
from sqlalchemy.orm import DeclarativeBase, relationship
# Deklaratywna klasa bazowa
class Base(DeclarativeBase):
pass
# konkretne opisy klas
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
email = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
items = relationship("Item", back_populates="owner")
class Item(Base):
__tablename__ = "items"
id = Column(Integer, primary_key=True)
title = Column(String, index=True)
description = Column(String, index=True)
owner_id = Column(Integer, ForeignKey("users.id"))
owner = relationship("User", back_populates="items")
Deklaratywna klasa bazowa, wykorzystywana do deklaratywnego definiowania obiektów (możliwe też imperatywne). (Deklaratywne mapowanie w SQLAlchemy). Kiedy tworzona jest klasa dziedzicząca po niej to baza zapamiętuje informacje o swoich dzieciach.
Wcześniej do tego samego celu zamiast DeclarativeBase używano funkcji declarative_base()
Plik crud.py
:
from sqlalchemy.orm import Session
from . import models
def get_user(db: Session, user_id: int):
return db.query(models.User).filter(models.User.id == user_id).first()
def get_user_by_email(db: Session, email: str):
return db.query(models.User).filter(models.User.email == email).first()
def get_users(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.User).offset(skip).limit(limit).all()
def create_user(db: Session, email, password):
fake_hashed_password = password + "notreallyhashed"
db_user = models.User(email=email, hashed_password=fake_hashed_password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
def get_items(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.Item).offset(skip).limit(limit).all()
Plik main.py
:
from . import models
from . import crud
from .database import SessionLocal, engine
models.Base.metadata.create_all(bind=engine)
db = SessionLocal()
crud.create_user(db, "j@gmail.com","xxx")
crud.create_user(db, "anna@gmail.com","xxx")
print(crud.get_users(db))
Do wygodnego otwarcia bazy danych można użyć narzędzia DB Browser for SQLite
Zaś do weryfikacji wprowadzanych danych może się przydać pydantic (użyty w oryginalnym przykładzie FastAPI)
Mapowanie klas
SQLAlchemy pozwala na mapowanie klas na 2 sposoby:
- Deklaratywne - opisuje się klasę, a SQLAlchemy tworzy mapowanie do tabeli
- Imperatywne - tworzy się mapowanie wprost
Mapowanie deklaratywne
from sqlalchemy import Integer, String, ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
# declarative base class
class Base(DeclarativeBase):
pass
# an example mapping using the base
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
fullname: Mapped[str] = mapped_column(String(30))
nickname: Mapped[Optional[str]]
Wykorzystujemy tutaj bazową klasę jako podstawę dla klas mapowanych w bazie danych.
Pola dla klasy możemy zdefiniować bezpośrednio (ORM declarative table) używając metody Mapped_column()
:
class User(Base):
__tablename__ = "user"
id = mapped_column(Integer, primary_key=True)
name = mapped_column(String(50), nullable=False)
fullname = mapped_column(String)
nickname = mapped_column(String(30))
Lub używając annotacji typu pomocniczego Mapped
:
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
fullname: Mapped[Optional[str]]
nickname: Mapped[Optional[str]] = mapped_column(String(30))
W wypadku relacji można używać relationship()
lub mapped_column()
z parametrem ForeignKey
. (Link do relacji)
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Child"]] = relationship(back_populates="parent")
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
parent: Mapped["Parent"] = relationship(back_populates="children")
Sprawdzanie wartości i walidacja
Defining Constraints, Simple validation
Istnieje kilka sposobów na walidację danych. Najprostszym jest prosty dekorator fla funkcji walidującej
from sqlalchemy.orm import validates
class EmailAddress(Base):
__tablename__ = "address"
id = mapped_column(Integer, primary_key=True)
email = mapped_column(String)
@validates("email")
def validate_email(self, key, address):
if "@" not in address:
raise ValueError("failed simple email validation")
return address