Essential SQLAlchemy: Chapter 7

This commit is contained in:
Pavel Vavilin
2026-09-21 11:32:53 +05:00
parent 3a0052fe19
commit 9f695901c6
+578
View File
@@ -4286,3 +4286,581 @@ Table(
session.add(o2) session.add(o2)
session.commit() session.commit()
#+END_SRC #+END_SRC
*** Joins
#+BEGIN_SRC python :results table
query = (
session.query(
Order.order_id,
User.username,
User.phone,
Cookie.cookie_name,
LineItem.quantity,
LineItem.extended_cost,
)
.join(User)
.join(LineItem)
.join(Cookie)
.filter(User.username == "cookiemon")
)
[row for row in query]
#+END_SRC
#+RESULTS[d7ab85e9d62ece5505b9bb871b863547945e9a2f]:
| 1 | cookiemon | 111-111-1111 | chocolate chip | 2 | Decimal | (1.00) |
| 1 | cookiemon | 111-111-1111 | peanut butter | 12 | Decimal | (3.00) |
*** Grouping
#+BEGIN_SRC python :results table
query = (
session.query(User.username, func.count(Order.order_id))
.outerjoin(Order)
.group_by(User.username)
.all()
)
query
#+END_SRC
#+RESULTS[0654ad9c5fb1050dbb7fe12359ee0129d974cbec]:
| cakeeater | 1 |
| cookiemon | 1 |
| pieperson | 0 |
*** Chaining
#+BEGIN_SRC python :results table
def get_orders_by_customer(
cust_name: str, shipped: bool | None = None, details: bool = False
):
query = session.query(Order.order_id, User.username, User.phone).join(User)
if details:
query = query.add_columns(
Cookie.cookie_name, LineItem.quantity, LineItem.extended_cost
)
query = query.join(LineItem).join(Cookie)
if shipped is not None:
query = query.where(Order.shipped == shipped)
results = query.filter(User.username == cust_name).all()
return results
get_orders_by_customer("cakeeater")
#+END_SRC
#+RESULTS[2bfd4ed6da2da0894cbfff4533e06629a86a5409]:
| 2 | cakeeater | 222-222-2222 |
#+BEGIN_SRC python :results table
get_orders_by_customer("cakeeater", details=True)
#+END_SRC
#+RESULTS[a16214b5f672ae8f06d8fb0d3455914cc246b4c4]:
| 2 | cakeeater | 222-222-2222 | chocolate chip | 24 | Decimal | (12.00) |
| 2 | cakeeater | 222-222-2222 | oatmeal raisin | 6 | Decimal | (6.00) |
#+BEGIN_SRC python :results raw
get_orders_by_customer("cakeeater", shipped=True)
#+END_SRC
#+RESULTS[17638e98cd1333daa06557954f633699643c838f]:
[]
#+BEGIN_SRC python :results table
get_orders_by_customer("cakeeater", shipped=False)
#+END_SRC
#+RESULTS[9d1b1a41a3b45c33583ae66a399a093ff312b571]:
| 2 | cakeeater | 222-222-2222 |
#+BEGIN_SRC python :results table
get_orders_by_customer("cakeeater", shipped=False, details=True)
#+END_SRC
#+RESULTS[39653887c276c6a3e1d1a3715346a01c5abd6cba]:
| 2 | cakeeater | 222-222-2222 | chocolate chip | 24 | Decimal | (12.00) |
| 2 | cakeeater | 222-222-2222 | oatmeal raisin | 6 | Decimal | (6.00) |
*** Raw Queries
#+BEGIN_SRC python :results output
from sqlalchemy import text
query = session.query(User).filter(text("username='cookiemon'"))
for u in query.all():
print(u)
#+END_SRC
#+RESULTS[93012273f06378bfd99097513d3d305cd839c551]:
: User(
: user_id="1",
: username="cookiemon",
: email_address="mon@cookie.com",
: phone="111-111-1111",
: password="password",
: created_on="2026-09-21 10:55:18.029598",
: updated_on="2026-09-21 10:55:18.029607",
: )
** SQLAlchemy v2
#+BEGIN_SRC python :tangle "ch7-v2.py" :shebang "#!/usr/bin/env python3" :results none
#!/usr/bin/env python3
"""Идиоматичный SQLAlchemy 2.0 пример (по мотивам «Essential SQLAlchemy»)."""
import logging
import re
from datetime import datetime
from decimal import Decimal
import pandas as pd
from ruff_format import format_string
from sqlalchemy import (
Boolean,
DateTime,
ForeignKey,
Integer,
Numeric,
String,
create_engine,
delete,
func,
insert,
or_,
select,
text,
update,
)
from sqlalchemy.orm import (
DeclarativeBase,
Mapped,
Session,
mapped_column,
relationship,
)
# ---------------------------------------------------------------------------
# Engine
# ---------------------------------------------------------------------------
engine = create_engine("sqlite:///:memory:", echo=False)
# ---------------------------------------------------------------------------
# Базовый класс с удобным repr и display
# ---------------------------------------------------------------------------
class Base(DeclarativeBase):
"""Базовый класс с repr() и display() для схемы таблицы."""
def __repr__(self) -> str:
raw = f"{self.__class__.__name__}"
columns = ",".join(
f"{col.name}={getattr(self, col.name)!r}"
for col in self.__table__.columns
)
return format_string(f"{raw}({columns})")
@classmethod
def display(cls) -> str:
raw = repr(cls.__table__).replace(
f"<{cls.__tablename__}>", f'"{cls.__tablename__}"'
)
raw = re.sub(
r"CallableColumnDefault\(<function datetime\.now at 0x[0-9a-f]+>\)",
"NOW",
raw,
)
try:
return format_string(raw)
except Exception:
logging.exception(raw)
return raw
# ---------------------------------------------------------------------------
# Модели
# ---------------------------------------------------------------------------
class Cookie(Base):
__tablename__ = "cookies"
cookie_id: Mapped[int] = mapped_column(primary_key=True)
cookie_name: Mapped[str | None] = mapped_column(String(50), index=True)
cookie_recipe_url: Mapped[str | None] = mapped_column(String(255))
cookie_sku: Mapped[str | None] = mapped_column(String(55))
quantity: Mapped[int | None] = mapped_column(Integer())
unit_cost: Mapped[Decimal | None] = mapped_column(Numeric(12, 2))
class User(Base):
__tablename__ = "users"
user_id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(12), nullable=False, unique=True)
email_address: Mapped[str] = mapped_column(String(255), nullable=False)
phone: Mapped[str] = mapped_column(String(20), nullable=False)
password: Mapped[str] = mapped_column(String(25), nullable=False)
created_on: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
updated_on: Mapped[datetime] = mapped_column(
DateTime, default=datetime.now, onupdate=datetime.now
)
class Order(Base):
__tablename__ = "orders"
order_id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int | None] = mapped_column(ForeignKey("users.user_id"))
shipped: Mapped[bool] = mapped_column(Boolean, default=False)
user: Mapped["User"] = relationship(back_populates="orders")
line_items: Mapped[list["LineItem"]] = relationship(back_populates="order")
class LineItem(Base):
__tablename__ = "line_items"
line_item_id: Mapped[int] = mapped_column(primary_key=True)
order_id: Mapped[int | None] = mapped_column(ForeignKey("orders.order_id"))
cookie_id: Mapped[int | None] = mapped_column(ForeignKey("cookies.cookie_id"))
quantity: Mapped[int | None] = mapped_column(Integer())
extended_cost: Mapped[Decimal | None] = mapped_column(Numeric(12, 2))
order: Mapped["Order"] = relationship(back_populates="line_items")
cookie: Mapped["Cookie"] = relationship(uselist=False)
# Дополняем User.orders после объявления Order (back_populates с обеих сторон)
User.orders = relationship("Order", back_populates="user", order_by="Order.order_id")
# ---------------------------------------------------------------------------
# Создание схемы
# ---------------------------------------------------------------------------
Base.metadata.create_all(engine)
Cookie.display()
User.display()
Order.display()
LineItem.display()
# ---------------------------------------------------------------------------
# Работа с сессией
# ---------------------------------------------------------------------------
with Session(engine) as session:
# =======================================================================
# INSERT: одиночные объекты через ORM
# =======================================================================
cc_cookie = Cookie(
cookie_name="chocolate chip",
cookie_recipe_url="http://some.aweso.me/cookie/recipe.html",
cookie_sku="CC01",
quantity=12,
unit_cost=Decimal("0.50"),
)
session.add(cc_cookie)
session.commit()
print("cc_cookie.cookie_id =", cc_cookie.cookie_id)
dcc = Cookie(
cookie_name="dark chocolate chip",
cookie_recipe_url="http://some.aweso.me/cookie/recipe/recipe_dark.html",
cookie_sku="CC02",
quantity=1,
unit_cost=Decimal("0.75"),
)
mol = Cookie(
cookie_name="molasses",
cookie_recipe_url="http://some.aweso.me/cookie/recipe_molasses.html",
cookie_sku="MOL01",
quantity=1,
unit_cost=Decimal("0.80"),
)
session.add_all([dcc, mol])
session.flush()
print([("dcc.cookie_id", dcc.cookie_id), ("mol.cookie_id", mol.cookie_id)])
# =======================================================================
# INSERT: bulk-вставка (замена legacy bulk_save_objects)
# =======================================================================
session.execute(
insert(Cookie),
[
{
"cookie_name": "peanut butter",
"cookie_recipe_url": "http://some.aweso.me/cookie/recipe/peanut.html",
"cookie_sku": "PB01",
"quantity": 24,
"unit_cost": Decimal("0.25"),
},
{
"cookie_name": "oatmeal raisin",
"cookie_recipe_url": "http://some.okay.me/cookie/raisin.html",
"cookie_sku": "EWW01",
"quantity": 100,
"unit_cost": Decimal("1.00"),
},
],
)
session.commit()
# Пример с .returning() — если нужны обратно PK/имена
result = session.execute(
insert(Cookie).returning(Cookie.cookie_id, Cookie.cookie_name),
[
{
"cookie_name": "sugar",
"cookie_recipe_url": "http://example.com/sugar",
"cookie_sku": "SGR01",
"quantity": 50,
"unit_cost": Decimal("0.40"),
},
],
)
print("inserted (returning):", result.all())
session.commit()
# =======================================================================
# SELECT: базовые
# =======================================================================
cookies = session.scalars(select(Cookie)).all()
print(cookies)
row = session.execute(select(Cookie.cookie_name, Cookie.quantity)).first()
print(row)
# =======================================================================
# DataFrame из select()
# =======================================================================
stmt = select(Cookie.quantity, Cookie.cookie_name).order_by(Cookie.quantity.desc())
rows = session.execute(stmt).all()
df = pd.DataFrame(rows, columns=["quantity", "cookie_name"])
headers = [""] + df.columns.tolist()
data_rows = [[idx, *row] for idx, row in zip(df.index, df.to_numpy().tolist())]
summary = [headers, None, *data_rows]
print(summary)
# =======================================================================
# LIMIT
# =======================================================================
stmt = select(Cookie).order_by(Cookie.quantity).limit(2)
print([(c.cookie_name,) for c in session.scalars(stmt)])
# =======================================================================
# Агрегаты
# =======================================================================
inv_count = session.scalar(select(func.sum(Cookie.quantity)))
print("inv_count =", inv_count)
rec = session.execute(
select(func.count(Cookie.cookie_name).label("inventory_count"))
).one()
print((list(rec._mapping.keys()), (rec.inventory_count,)))
# =======================================================================
# Фильтры
# =======================================================================
record = session.scalars(
select(Cookie).where(Cookie.cookie_name == "chocolate chip")
).first()
print(record)
record = session.scalars(
select(Cookie).filter_by(cookie_name="chocolate chip")
).first()
print(record)
stmt = select(Cookie).where(Cookie.cookie_name.like("chocolate chip"))
print([c.cookie_name for c in session.scalars(stmt)])
# =======================================================================
# Вычисляемые колонки
# =======================================================================
stmt = select(Cookie.cookie_name, ("SKU-" + Cookie.cookie_sku).label("sku"))
for r in session.execute(stmt):
print(r)
stmt = select(
Cookie.cookie_name,
func.cast(Cookie.quantity * Cookie.unit_cost, Numeric(12, 2)).label("inv_cost"),
)
print([(r.cookie_name, repr(r.inv_cost)) for r in session.execute(stmt)])
stmt = select(Cookie).where(
Cookie.quantity > 23, Cookie.unit_cost < Decimal("0.40")
)
print([(c.cookie_name,) for c in session.scalars(stmt)])
stmt = select(Cookie).where(
or_(
Cookie.quantity.between(10, 50),
Cookie.cookie_name.contains("chip"),
)
)
print([(c.cookie_name,) for c in session.scalars(stmt)])
# =======================================================================
# UPDATE (ORM-объект)
# =======================================================================
cc_cookie = session.scalars(
select(Cookie).filter_by(cookie_name="chocolate chip")
).first()
assert cc_cookie is not None
cc_cookie.quantity = (cc_cookie.quantity or 0) + 120
session.commit()
print("cc_cookie.quantity =", cc_cookie.quantity)
# UPDATE (bulk)
session.execute(
update(Cookie)
.where(Cookie.cookie_name == "chocolate chip")
.values(quantity=Cookie.quantity - 20)
)
session.commit()
print(
"after bulk update:",
session.scalars(
select(Cookie.quantity).where(Cookie.cookie_name == "chocolate chip")
).one(),
)
# =======================================================================
# DELETE (ORM)
# =======================================================================
dcc_cookie = session.scalars(
select(Cookie).filter_by(cookie_name="dark chocolate chip")
).one()
session.delete(dcc_cookie)
session.commit()
assert (
session.scalars(
select(Cookie).filter_by(cookie_name="dark chocolate chip")
).first()
is None
)
# DELETE (bulk)
session.execute(delete(Cookie).where(Cookie.cookie_name == "molasses"))
session.commit()
assert (
session.scalars(select(Cookie).filter_by(cookie_name="molasses")).first()
is None
)
# =======================================================================
# Пользователи и связи
# =======================================================================
cookiemon = User(
username="cookiemon",
email_address="mon@cookie.com",
phone="111-111-1111",
password="password",
)
cakeeater = User(
username="cakeeater",
email_address="cakeeater@cake.com",
phone="222-222-2222",
password="password",
)
pieperson = User(
username="pieperson",
email_address="person@pie.com",
phone="333-333-3333",
password="password",
)
session.add_all([cookiemon, cakeeater, pieperson])
session.commit()
o1 = Order()
o1.user = cookiemon
session.add(o1)
cc = session.scalars(
select(Cookie).filter_by(cookie_name="chocolate chip")
).one()
pb = session.scalars(
select(Cookie).filter_by(cookie_name="peanut butter")
).one()
line1 = LineItem(cookie=cc, quantity=2, extended_cost=Decimal("1.00"))
line2 = LineItem(quantity=12, extended_cost=Decimal("3.00"))
line2.cookie = pb
line2.order = o1
o1.line_items.append(line1)
o1.line_items.append(line2)
session.commit()
o2 = Order()
o2.user = cakeeater
line1 = LineItem(cookie=cc, quantity=24, extended_cost=Decimal("12.00"))
oat = session.scalars(
select(Cookie).filter_by(cookie_name="oatmeal raisin")
).one()
line2 = LineItem(cookie=oat, quantity=6, extended_cost=Decimal("6.00"))
o2.line_items.append(line1)
o2.line_items.append(line2)
session.add(o2)
session.commit()
# =======================================================================
# JOIN
# =======================================================================
stmt = (
select(
Order.order_id,
User.username,
User.phone,
Cookie.cookie_name,
LineItem.quantity,
LineItem.extended_cost,
)
.join(User)
.join(LineItem)
.join(Cookie)
.where(User.username == "cookiemon")
)
print([tuple(r) for r in session.execute(stmt)])
# =======================================================================
# GROUP BY + OUTER JOIN
# =======================================================================
stmt = (
select(User.username, func.count(Order.order_id))
.outerjoin(Order)
.group_by(User.username)
)
print(session.execute(stmt).all())
# =======================================================================
# Функция-обёртка
# =======================================================================
def get_orders_by_customer(
cust_name: str,
shipped: bool | None = None,
details: bool = False,
):
stmt = select(Order.order_id, User.username, User.phone).join(User)
if details:
stmt = (
stmt.add_columns(
Cookie.cookie_name,
LineItem.quantity,
LineItem.extended_cost,
)
.join(LineItem)
.join(Cookie)
)
if shipped is not None:
stmt = stmt.where(Order.shipped == shipped)
stmt = stmt.where(User.username == cust_name)
return session.execute(stmt).all()
print(get_orders_by_customer("cakeeater"))
print(get_orders_by_customer("cakeeater", details=True))
print(get_orders_by_customer("cakeeater", shipped=True))
print(get_orders_by_customer("cakeeater", shipped=False))
print(get_orders_by_customer("cakeeater", shipped=False, details=True))
# =======================================================================
# Текстовый SQL
# =======================================================================
stmt = select(User).where(text("username='cookiemon'"))
for u in session.scalars(stmt):
print(u)
#+END_SRC