diff --git a/Essential-SQLAlchemy-2nd.org b/Essential-SQLAlchemy-2nd.org index d9506b2..05d9e88 100644 --- a/Essential-SQLAlchemy-2nd.org +++ b/Essential-SQLAlchemy-2nd.org @@ -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\(\)", + "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