SQLAlchemy ORM Python: duomenų bazių abstrakcija

Kai pradedi dirbti su duomenų bazėmis Python projekte, greičiausiai pirmiausia susipažįsti su raw SQL užklausomis. Rašai SELECT, INSERT, UPDATE sakinius, jungiesi prie duomenų bazės per psycopg2 ar panašią biblioteką, ir viskas veikia. Bet kai projektas auga, pradedi jausti, kad kažkas čia ne taip. Kodas tampa sunkiai skaitomas, SQL užklausos išsimėto po visą projektą, o duomenų bazės schemos pakeitimai virsta košmaru.

Čia į pagalbą ateina SQLAlchemy – viena galingiausių Python bibliotekų darbui su duomenų bazėmis. Tai ne tik ORM (Object-Relational Mapping) įrankis, bet ir išsami duomenų bazių abstrakcijos sistema, leidžianti rašyti Python kodą vietoj SQL ir išlaikyti projektą tvarkingą bei lengvai prižiūrimą.

Kodėl SQLAlchemy, o ne tiesiog SQL?

Pirmą kartą susidūrus su SQLAlchemy, gali kilti klausimas – kam man dar vienas abstrakcijos sluoksnis? SQL juk puikiai veikia, ir aš jį jau mokau. Tai teisinga, bet yra keletas svarbių priežasčių, kodėl verta įdėti laiko į SQLAlchemy išmokimą.

Pirma, duomenų bazės nepriklausomybė. Jei rašai raw SQL, tavo kodas dažnai būna susietas su konkrečia duomenų baze – PostgreSQL, MySQL ar SQLite. Kai nori pakeisti duomenų bazę, tenka perrašyti nemažą kodo dalį. SQLAlchemy leidžia rašyti kodą, kuris veikia su bet kuria palaikoma duomenų baze, tiesiog pakeitus connection string.

Antra, saugumas. SQL injection atakos vis dar yra viena dažniausių saugumo spragų. SQLAlchemy automatiškai tvarkosi su parametrų ekranavimų ir apsaugo nuo tokių grėsmių. Žinoma, galima ir su raw SQL naudoti parametrizuotas užklausas, bet SQLAlchemy tai daro by default.

Trečia, Python objektai vietoj žodynų. Dirbant su raw SQL rezultatais, gauni tuple ar dict struktūras. Su ORM gauni pilnaverčius Python objektus su metodais, savybėmis ir visa kita, kas daro kodą daug skaitomesnį ir lengviau prižiūrimą.

Core vs ORM: du skirtingi pasauliai

SQLAlchemy iš tikrųjų susideda iš dviejų dalių, ir tai svarbu suprasti nuo pat pradžių. Yra SQLAlchemy Core – žemo lygio SQL abstrakcija, kuri leidžia konstruoti SQL užklausas Python sintakse, bet vis dar operuoja lentelėmis ir eilutėmis. O yra SQLAlchemy ORM – aukštesnio lygio abstrakcija, kuri leidžia dirbti su Python klasėmis ir objektais.

Core yra geresnis pasirinkimas, kai reikia maksimalios kontrolės ir našumo. Jei rašai sudėtingas analitines užklausas ar dirbi su dideliais duomenų kiekiais, Core gali būti efektyvesnis. ORM puikiai tinka verslo logikai, kur svarbiau kodo skaitomumas ir palaikomumas nei paskutiniai našumo procentai.

Gera žinia – nereikia rinktis vieno ar kito. Galima naudoti abu tame pačiame projekte, net tame pačiame faile. Pradėti su ORM kasdieniam darbui, o kai reikia optimizuoti kritines užklausas, panaudoti Core.

Pirmieji žingsniai su SQLAlchemy ORM

Pradėkime nuo paprasto pavyzdžio. Tarkime, kuriame blogą ir mums reikia saugoti straipsnius ir autorius. Pirmiausia reikia įdiegti SQLAlchemy:

pip install sqlalchemy

Dabar sukuriame modelius. SQLAlchemy 2.0 versijoje rekomenduojama naudoti deklaratyvų stilių su dataclass-like sintakse:

from sqlalchemy import create_engine, String, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from typing import List

class Base(DeclarativeBase):
    pass

class Author(Base):
    __tablename__ = "authors"
    
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(100))
    email: Mapped[str] = mapped_column(String(100), unique=True)
    
    posts: Mapped[List["Post"]] = relationship(back_populates="author")

class Post(Base):
    __tablename__ = "posts"
    
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(200))
    content: Mapped[str]
    author_id: Mapped[int] = mapped_column(ForeignKey("authors.id"))
    
    author: Mapped["Author"] = relationship(back_populates="posts")

Šis kodas atrodo gana aiškus net tiems, kas niekada nematė SQLAlchemy. Turime dvi klases – Author ir Post, kurios atitinka duomenų bazės lenteles. Mapped tipai nurodo, kokie duomenų tipai bus duomenų bazėje, o relationship() apibrėžia ryšius tarp lentelių.

Session – tavo darbo erdvė

SQLAlchemy naudoja Session objektą kaip pagrindinę sąsają su duomenų baze. Session yra tarsi „darbo erdvė”, kurioje kaupiami pakeitimai ir vėliau commitinami į duomenų bazę vienu metu. Tai vadinama Unit of Work pattern.

from sqlalchemy import create_engine
from sqlalchemy.orm import Session

# Sukuriame engine - tai connection pool ir dialekto valdymas
engine = create_engine("sqlite:///blog.db", echo=True)

# Sukuriame lenteles
Base.metadata.create_all(engine)

# Dirbame su session
with Session(engine) as session:
    # Sukuriame naują autorių
    author = Author(name="Jonas Jonaitis", email="[email protected]")
    session.add(author)
    
    # Sukuriame straipsnį
    post = Post(
        title="SQLAlchemy yra nuostabu",
        content="Čia turėtų būti ilgas tekstas...",
        author=author
    )
    session.add(post)
    
    # Išsaugome viską vienu metu
    session.commit()

Pastebėk, kad naudojame context manager (with statement). Tai užtikrina, kad session bus tinkamai uždaryta net jei įvyks klaida. echo=True parametras engine kūrimo metu leidžia matyti visas SQL užklausas konsolėje – labai naudinga mokantis ir debuginant.

Užklausų rašymas: nuo paprastų iki sudėtingų

Vienas iš SQLAlchemy privalumų – galimybė rašyti užklausas įvairiais sudėtingumo lygiais. Paprasčiausioms operacijoms galima naudoti session.get() ar session.query(), o sudėtingesnėms – pilną select() sintaksę.

SQLAlchemy 2.0 versijoje rekomenduojama naudoti naują select() sintaksę:

from sqlalchemy import select

with Session(engine) as session:
    # Paprasta užklausa - gauti visus autorius
    stmt = select(Author)
    authors = session.execute(stmt).scalars().all()
    
    # Su filtru
    stmt = select(Author).where(Author.name == "Jonas Jonaitis")
    author = session.execute(stmt).scalar_one()
    
    # Su join
    stmt = select(Post).join(Post.author).where(Author.name.like("%Jonas%"))
    posts = session.execute(stmt).scalars().all()
    
    # Sudėtingesnė užklausa su keliais filtrais
    stmt = (
        select(Post)
        .join(Post.author)
        .where(Author.email.endswith("@example.com"))
        .order_by(Post.title)
        .limit(10)
    )
    recent_posts = session.execute(stmt).scalars().all()

Ši sintaksė gali atrodyti keista iš pradžių, bet greitai tampa labai intuityvi. Kiekvienas metodas grąžina naują statement objektą, todėl galima grandinėti operacijas. Tai vadinama fluent interface pattern.

Relationship ir lazy loading spąstai

Viena iš dažniausių klaidų dirbant su SQLAlchemy ORM – nesupratimas, kaip veikia relationship loading strategijos. Pagal nutylėjimą SQLAlchemy naudoja lazy loading, kas reiškia, kad susiję objektai užkraunami tik tada, kai prie jų kreipiamasi.

Tai gali sukelti N+1 užklausų problemą:

# Bloga praktika - N+1 problema
authors = session.execute(select(Author)).scalars().all()
for author in authors:
    print(f"{author.name} parašė {len(author.posts)} straipsnių")
    # Kiekvienam autoriui daromas atskiras SQL query!

Sprendimas – naudoti eager loading su joinedload() ar selectinload():

from sqlalchemy.orm import joinedload, selectinload

# Gera praktika - viena užklausa su JOIN
stmt = select(Author).options(joinedload(Author.posts))
authors = session.execute(stmt).unique().scalars().all()

# Arba su atskiru SELECT (geriau one-to-many ryšiams)
stmt = select(Author).options(selectinload(Author.posts))
authors = session.execute(stmt).scalars().all()

joinedload() naudoja SQL JOIN ir grąžina viską viename rezultate. Tai efektyvu one-to-one ar many-to-one ryšiams. selectinload() daro atskirą SELECT užklausą susijusiems objektams, bet užkrauna juos visiems parent objektams iš karto. Tai geriau tinka one-to-many ryšiams, nes išvengia dublikatų.

Migracijos su Alembic

Realybėje duomenų bazės schema keičiasi kartu su projektu. Pridedi naujų laukų, keiti tipus, kurii naujus indeksus. Rankiniu būdu tvarkyti šiuos pakeitimus yra košmaras, ypač kai dirbi komandoje ar turi production aplinką.

Alembic yra oficialus SQLAlchemy migracijos įrankis. Jis veikia panašiai kaip Django migracijos ar Rails migrations:

pip install alembic
alembic init alembic

Sukonfigūruoji alembic.ini failą su savo duomenų bazės connection string, o tada env.py faile nurodai savo Base:

# alembic/env.py
from myapp.models import Base
target_metadata = Base.metadata

Dabar galima automatiškai generuoti migracijas:

alembic revision --autogenerate -m "Add published_at to posts"
alembic upgrade head

Alembic palygina tavo modelius su dabartine duomenų bazės schema ir sugeneruoja migracijos failą su reikalingais pakeitimais. Tai ne visada tobula – kartais reikia rankiniu būdu pakoreguoti sugeneruotą kodą, ypač sudėtingesnių pakeitimų atveju, bet 90% atvejų veikia puikiai.

Praktiniai patarimai darbui su production

Kai projektas pereina į production, atsiranda naujų iššūkių. Štai keletas patarimų, kurie padės išvengti dažniausių problemų.

Connection pooling – SQLAlchemy pagal nutylėjimą naudoja connection pool, bet reikia jį tinkamai sukonfigūruoti. Per mažas pool gali sukelti connection timeout klaidas, per didelis – apkrauti duomenų bazę:

engine = create_engine(
    "postgresql://user:pass@localhost/dbname",
    pool_size=20,
    max_overflow=0,
    pool_pre_ping=True,  # Patikrina connection prieš naudojant
    pool_recycle=3600    # Perkuria connections kas valandą
)

Session lifecycle – niekada nenaudok globalių session objektų. Kiekvienam request ar task reikia naujo session. Web frameworkuose naudok scoped_session ar framework integracijas:

from sqlalchemy.orm import scoped_session, sessionmaker

SessionLocal = scoped_session(sessionmaker(bind=engine))

# FastAPI pavyzdys
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

Indeksai – SQLAlchemy leidžia apibrėžti indeksus tiesiog modeliuose:

from sqlalchemy import Index

class Post(Base):
    __tablename__ = "posts"
    
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(200))
    author_id: Mapped[int] = mapped_column(ForeignKey("authors.id"))
    published_at: Mapped[datetime] = mapped_column(index=True)
    
    __table_args__ = (
        Index('ix_author_published', 'author_id', 'published_at'),
    )

Bulk operacijos – kai reikia įterpti ar atnaujinti daug įrašų, naudok bulk metodus:

# Vietoj add() cikle
posts = [Post(title=f"Post {i}", content="...") for i in range(1000)]
session.bulk_save_objects(posts)
session.commit()

# Arba dar efektyviau su Core
from sqlalchemy import insert
stmt = insert(Post).values([
    {"title": f"Post {i}", "content": "..."} for i in range(1000)
])
session.execute(stmt)

Kai SQLAlchemy tampa per daug

Būkim sąžiningi – SQLAlchemy nėra tobula visoms situacijoms. Yra atvejų, kai paprastesnis sprendimas būtų geresnis.

Jei tavo projektas labai mažas – vienas ar du modeliai, kelios paprastos užklausos – SQLAlchemy gali būti overkill. Tokiems atvejams puikiai tinka lengvesnės bibliotekos kaip peewee ar net tiesiog raw SQL su psycopg2.

Jei dirbi su labai specifinėmis duomenų bazės funkcijomis, kurios nėra gerai palaikomos SQLAlchemy – pavyzdžiui, PostgreSQL full-text search ar JSON operacijos – kartais paprasčiau parašyti raw SQL. Žinoma, SQLAlchemy leidžia naudoti text() konstruktorių tokiems atvejams, bet tada prarandamas dalis abstrakcijos pranašumų.

Jei tau reikia maksimalaus našumo ir dirbi su milijonais įrašų, gali būti verta pažiūrėti į asinchronines bibliotekas kaip asyncpg arba net tiesiog optimizuoti SQL užklausas rankiniu būdu. SQLAlchemy 2.0 turi async palaikymą, bet jis vis dar nėra toks subrendęs kaip sinchroninė versija.

Tačiau daugumai projektų – nuo mažų startup’ų iki didelių enterprise sistemų – SQLAlchemy yra puikus pasirinkimas. Jis suteikia tinkamą balansą tarp abstrakcijos ir kontrolės, tarp našumo ir kodo skaitomumo. Taip, mokymosi kreivė yra šiek tiek statesne nei paprastų ORM įrankių, bet investicija atsipirksta ilgalaikėje perspektyvoje.

Svarbiausia – pradėti naudoti ir mokytis iš klaidų. Dokumentacija yra puiki, bendruomenė aktyvi, o Stack Overflow pilna atsakymų į dažniausias problemas. Pradėk nuo paprastų dalykų – modelių kūrimo, paprastų užklausų – ir palaipsniui gilink žinias. Po kelių mėnesių darbo su SQLAlchemy pastebėsi, kad jau nebegali įsivaizduoti Python projekto be jo.

Daugiau

Logstash grok patterns: logų parserinimas