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.
