Essential SQLAlchemy: Chapter 7
This commit is contained in:
@@ -4286,3 +4286,581 @@ Table(
|
||||
session.add(o2)
|
||||
session.commit()
|
||||
#+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
|
||||
|
||||
Reference in New Issue
Block a user