Kodėl SQLModel keičia žaidimo taisykles
Jei dirbate su FastAPI ir duomenų bazėmis, tikriausiai jau susidūrėte su SQLAlchemy arba kitais ORM sprendimais. Bet štai problema – dažnai tenka rašyti tuos pačius modelius kelis kartus: vieną kartą Pydantic validacijai, kitą kartą SQLAlchemy duomenų bazės lentelėms. Tai ne tik nuobodu, bet ir klaidas skatinantis procesas. Čia ir ateina į pagalbą SQLModel – biblioteka, kurią sukūrė tas pats žmogus kaip ir FastAPI (Sebastián Ramírez).
SQLModel iš esmės sujungia Pydantic ir SQLAlchemy geriausias savybes į vieną elegantišką paketą. Tai reiškia, kad galite aprašyti savo duomenų struktūrą vieną kartą ir naudoti ją tiek API validacijai, tiek duomenų bazės operacijoms. Skamba per gerai, kad būtų tiesa? Bet tai tikrai veikia, ir veikia puikiai.
Pats naudoju SQLModel jau pusantrų metų įvairiuose projektuose – nuo mažų startup’ų iki vidutinio dydžio enterprise sistemų. Ir galiu pasakyti, kad tai vienas iš tų sprendimų, kurie realiai sutaupo laiko ir nervų ląstelių.
Kaip pradėti: pirmieji žingsniai
Pradėkime nuo paprasčiausio pavyzdžio. Pirma, reikia įsidiegti reikalingas bibliotekas:
„`bash
pip install fastapi sqlmodel uvicorn
„`
Dabar sukurkime paprastą modelį. Tarkime, kuriame sistemą, kuri seka knygas:
„`python
from typing import Optional
from sqlmodel import Field, SQLModel
class Book(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
title: str = Field(index=True)
author: str
year: int
isbn: str = Field(unique=True)
rating: Optional[float] = None
„`
Matote tą `table=True`? Tai ir yra magija – šis parametras sako SQLModel, kad šis modelis turi būti ir duomenų bazės lentelė. Be šio parametro, modelis būtų tik Pydantic schema validacijai.
Vienas dalykas, kurį pastebėjau praktikoje – žmonės dažnai pamiršta `Optional` tipą ID laukui. Tai svarbu, nes kuriant naują įrašą, ID dar neegzistuoja (jį sugeneruoja duomenų bazė). Be `Optional`, gausit validacijos klaidas.
Duomenų bazės sujungimas ir sesijų valdymas
Gerai, turime modelį. Dabar reikia prijungti duomenų bazę. Pradėkime nuo SQLite, nes tai paprasčiausia:
„`python
from sqlmodel import create_engine, Session
DATABASE_URL = „sqlite:///./books.db”
engine = create_engine(DATABASE_URL, echo=True)
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
„`
Tas `echo=True` parametras yra jūsų geriausias draugas derinimo metu – jis išspausdina visus SQL užklausas į konsolę. Gamyboje, žinoma, tai išjunkite.
Dabar svarbi dalis – sesijų valdymas. Yra keletas būdų, kaip tai padaryti, bet man labiausiai patinka dependency injection per FastAPI:
„`python
from fastapi import Depends
def get_session():
with Session(engine) as session:
yield session
@app.post(„/books/”)
def create_book(book: Book, session: Session = Depends(get_session)):
session.add(book)
session.commit()
session.refresh(book)
return book
„`
Kodėl taip? Nes FastAPI automatiškai uždarys sesiją po kiekvieno request’o, net jei įvyks klaida. Tai apsaugo nuo memory leak’ų ir užkabintų duomenų bazės connection’ų.
Skirtingi modeliai skirtingiems tikslams
Štai kur daugelis suklumpa. Nors SQLModel leidžia naudoti tą patį modelį visur, praktikoje dažnai reikia skirtingų modelių skirtingoms situacijoms. Pavyzdžiui:
„`python
# Bazinis modelis su bendromis savybėmis
class BookBase(SQLModel):
title: str = Field(index=True, min_length=1, max_length=200)
author: str = Field(min_length=1, max_length=100)
year: int = Field(ge=1000, le=2100)
isbn: str = Field(regex=r”^\d{13}$”)
rating: Optional[float] = Field(default=None, ge=0, le=10)
# Modelis duomenų bazei
class Book(BookBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
# Modelis kuriant naują knygą (be ID)
class BookCreate(BookBase):
pass
# Modelis atsakymams (su ID)
class BookRead(BookBase):
id: int
# Modelis atnaujinimams (visi laukai optional)
class BookUpdate(SQLModel):
title: Optional[str] = None
author: Optional[str] = None
year: Optional[int] = None
isbn: Optional[str] = None
rating: Optional[float] = None
„`
Taip, tai daugiau kodo, bet pasitikėkite – ilgalaikėje perspektyvoje tai išgelbės jus nuo galvos skausmo. Ypač kai reikia skirtingų validacijos taisyklių skirtingoms operacijoms.
Ryšiai tarp lentelių: vienas-daug ir daug-daug
Realybėje retai kada turite tik vieną izoliuotą lentelę. Papildykime mūsų pavyzdį autorių lentele ir ryšiu:
„`python
from typing import List
from sqlmodel import Relationship
class Author(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
birth_year: Optional[int] = None
books: List[„Book”] = Relationship(back_populates=”author_obj”)
class Book(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
title: str = Field(index=True)
year: int
isbn: str = Field(unique=True)
rating: Optional[float] = None
author_id: Optional[int] = Field(default=None, foreign_key=”author.id”)
author_obj: Optional[Author] = Relationship(back_populates=”books”)
„`
Pastebėjote `back_populates`? Tai svarbu – tai nustato dvikryptį ryšį. Dabar galite pasiekti autoriaus knygas per `author.books` ir knygos autorių per `book.author_obj`.
Daug-daug ryšiams reikia tarpinės lentelės:
„`python
class BookTagLink(SQLModel, table=True):
book_id: Optional[int] = Field(
default=None, foreign_key=”book.id”, primary_key=True
)
tag_id: Optional[int] = Field(
default=None, foreign_key=”tag.id”, primary_key=True
)
class Tag(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(unique=True, index=True)
books: List[Book] = Relationship(
back_populates=”tags”,
link_model=BookTagLink
)
class Book(SQLModel, table=True):
# … ankstesni laukai …
tags: List[Tag] = Relationship(
back_populates=”books”,
link_model=BookTagLink
)
„`
Užklausos ir filtravimas: kaip išgauti tai, ko reikia
SQLModel naudoja SQLAlchemy select sintaksę, kuri iš pradžių gali atrodyti keista, bet iš tikrųjų yra labai galinga:
„`python
from sqlmodel import select
@app.get(„/books/”)
def read_books(
session: Session = Depends(get_session),
offset: int = 0,
limit: int = 100,
min_rating: Optional[float] = None
):
query = select(Book)
if min_rating:
query = query.where(Book.rating >= min_rating)
query = query.offset(offset).limit(limit)
books = session.exec(query).all()
return books
„`
Norite sudėtingesnių užklausų? Štai kaip gauti visus autorius su jų knygų skaičiumi:
„`python
from sqlalchemy import func
def get_authors_with_book_count(session: Session):
query = (
select(Author, func.count(Book.id).label(„book_count”))
.join(Book)
.group_by(Author.id)
)
results = session.exec(query).all()
return results
„`
Vienas patarimas – jei rašote sudėtingas užklausas, naudokite `echo=True` engine konfigūracijoje ir pažiūrėkite, kokį SQL generuoja. Kartais gali būti netikėtumų, ypač su eager loading.
Migracijos su Alembic
SQLModel puikiai veikia su Alembic – tai standartinis įrankis SQLAlchemy migracijoms. Štai kaip tai nustatyti:
„`bash
pip install alembic
alembic init migrations
„`
Redaguokite `alembic.ini` ir nustatykite savo duomenų bazės URL. Tada `migrations/env.py` faile:
„`python
from sqlmodel import SQLModel
from your_app.models import Book, Author, Tag # importuokite visus modelius
target_metadata = SQLModel.metadata
„`
Dabar galite kurti migracijas:
„`bash
alembic revision –autogenerate -m „Add books table”
alembic upgrade head
„`
Vienas dalykas, kurį išmokau sunkiuoju būdu – visada peržiūrėkite automatiškai sugeneruotas migracijas prieš jas vykdydami. Alembic kartais nepastebi visų pakeitimų arba sugeneruoja ne visai tai, ko tikitės. Ypač su indeksais ir constraints.
Praktiniai patarimai iš realių projektų
Po kelių projektų su SQLModel, surinkau keletą patarimų, kurie gali sutaupyti jums laiko:
**Async palaikymas:** SQLModel palaiko async, bet reikia naudoti `sqlalchemy.ext.asyncio`. Tai atrodo taip:
„`python
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlmodel.ext.asyncio.session import AsyncSession
DATABASE_URL = „postgresql+asyncpg://user:pass@localhost/db”
engine = create_async_engine(DATABASE_URL, echo=True)
async def get_session():
async with AsyncSession(engine) as session:
yield session
„`
**Testavimas:** Testams naudokite in-memory SQLite bazę:
„`python
@pytest.fixture
def session():
engine = create_engine(„sqlite:///:memory:”)
SQLModel.metadata.create_all(engine)
with Session(engine) as session:
yield session
„`
**N+1 problema:** Tai klasikinė ORM problema. Jei turite knygų sąrašą ir norite parodyti kiekvienos knygos autorių, naivus kodas padarys vieną užklausą knygoms ir po vieną kiekvienam autoriui:
„`python
# Blogai – N+1 problema
books = session.exec(select(Book)).all()
for book in books:
print(book.author_obj.name) # Kiekviena iteracija – nauja užklausa!
# Gerai – eager loading
from sqlmodel import selectinload
query = select(Book).options(selectinload(Book.author_obj))
books = session.exec(query).all()
for book in books:
print(book.author_obj.name) # Visi autoriai jau užkrauti
„`
**Pagination:** Visada implementuokite puslapiavimą dideliems duomenų rinkiniams:
„`python
def paginate(query, page: int = 1, per_page: int = 50):
offset = (page – 1) * per_page
return query.offset(offset).limit(per_page)
„`
Ką daryti, kai viskas eina ne taip
SQLModel yra puikus įrankis, bet kaip ir bet kas, jis turi savo keblumų. Štai keletas dažniausių problemų ir jų sprendimai:
**Circular imports:** Kai modeliai nurodo vienas į kitą, galite gauti circular import klaidą. Sprendimas – naudokite string anotacijas:
„`python
class Author(SQLModel, table=True):
books: List[„Book”] = Relationship(back_populates=”author_obj”)
„`
**Serialization problemos:** Kartais FastAPI nesugeba automatiškai serializuoti SQLModel objektų su ryšiais. Sprendimas – naudokite `response_model`:
„`python
@app.get(„/books/{book_id}”, response_model=BookRead)
def get_book(book_id: int, session: Session = Depends(get_session)):
book = session.get(Book, book_id)
return book
„`
**Lėtos užklausos:** Jei pastebite, kad užklausos lėtos, pirmiausia patikrinkite:
– Ar turite indeksus dažnai naudojamuose laukuose?
– Ar neturite N+1 problemos?
– Ar naudojate `limit()` dideliems rezultatų rinkiniams?
Naudokite `EXPLAIN ANALYZE` PostgreSQL arba `.explain()` SQLAlchemy, kad pamatytumėte, kas vyksta:
„`python
query = select(Book).where(Book.rating > 8)
print(query.compile(compile_kwargs={„literal_binds”: True}))
„`
**Konkurencijos problemos:** Jei keli vartotojai vienu metu redaguoja tą patį įrašą, gali kilti problemų. Sprendimas – optimistic locking su version laukais arba pesimistic locking su `with_for_update()`.
Dar vienas dalykas – jei naudojate PostgreSQL, įjunkite connection pooling. SQLAlchemy turi puikų built-in pooling, bet reikia jį teisingai sukonfigūruoti:
„`python
engine = create_engine(
DATABASE_URL,
pool_size=20,
max_overflow=0,
pool_pre_ping=True # Patikrina connection prieš naudojant
)
„`
Kodėl verta investuoti laiką į SQLModel
Po viso šito kodo ir paaiškinimų, grįžkime prie esmės. SQLModel nėra tobulas – joks įrankis nėra. Bet jis sprendžia realią problemą: sumažina kodo dubliavimąsi ir padaro duomenų bazės darbą su FastAPI daug paprastesnį.
Ar turėtumėte jį naudoti kiekviename projekte? Ne būtinai. Jei kuriate super paprastą API su viena ar dviem lentelėmis, galbūt užteks ir raw SQL. Jei jums reikia maksimalios kontrolės ir optimizacijos, galbūt norėsite naudoti gryną SQLAlchemy.
Bet jei kuriate tipišką FastAPI aplikaciją su vidutinio sudėtingumo duomenų modeliu, SQLModel yra puikus pasirinkimas. Jis sutaupo laiko, sumažina klaidų tikimybę ir leidžia sutelkti dėmesį į verslo logiką, o ne į boilerplate kodą.
Mano patirtis rodo, kad SQLModel ypač gerai tinka startup’ams ir MVP projektams, kur greitis yra svarbus. Galite greitai sukurti veikiančią sistemą, o vėliau, jei reikės, optimizuoti kritinius dalykus. Ir net tada, SQLModel netrukdys – jis pakankamai lankstus, kad leistų rašyti raw SQL užklausas, kai reikia.
Taigi, jei dar neišbandėte SQLModel su FastAPI, rekomenduoju skirti savaitgalį ir sukurti mažą projektą. Pamatysite, kaip greitai viskas susidėlioja į vietas. Ir kas žino – galbūt tai taps jūsų default stack’u būsimiems projektams.
