Essential SQLAlchemy: Chapter 6
This commit is contained in:
@@ -3243,3 +3243,510 @@ sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) CHECK constraint failed:
|
|||||||
|
|
||||||
#+RESULTS:
|
#+RESULTS:
|
||||||
| Artist | Album | Customer | Employee | Genre | Invoice | InvoiceLine | Track | MediaType | Playlist | PlaylistTrack |
|
| Artist | Album | Customer | Employee | Genre | Invoice | InvoiceLine | Track | MediaType | Playlist | PlaylistTrack |
|
||||||
|
|
||||||
|
* Chapter 6: Defining Schema with SQLAlchemy ORM
|
||||||
|
** SQLAlchemy v1
|
||||||
|
:PROPERTIES:
|
||||||
|
:header-args:python: :session *ch6* :tangle ch6-v1.py
|
||||||
|
:header-args: :cache yes
|
||||||
|
:END:
|
||||||
|
#+BEGIN_SRC python :shebang "#!/usr/bin/env python3" :results value
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from ruff_format import format_string
|
||||||
|
from sqlalchemy import Table, Column, Integer, Numeric, String
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
class MyBase:
|
||||||
|
@classmethod
|
||||||
|
def display(cls: Base) -> 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)
|
||||||
|
|
||||||
|
|
||||||
|
class Cookie(Base, MyBase):
|
||||||
|
__tablename__ = "cookies"
|
||||||
|
|
||||||
|
cookie_id = Column(Integer(), primary_key=True)
|
||||||
|
cookie_name = Column(String(50), index=True)
|
||||||
|
cookie_recipe_url = Column(String(255))
|
||||||
|
cookie_sku = Column(String(55))
|
||||||
|
quantity = Column(Integer())
|
||||||
|
unit_cost = Column(Numeric(12, 2))
|
||||||
|
|
||||||
|
|
||||||
|
Cookie.display()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS[5b56178ce5c3f4569e8a6b504d2403a6cd89b6d2]:
|
||||||
|
#+begin_example
|
||||||
|
Table(
|
||||||
|
"cookies",
|
||||||
|
MetaData(),
|
||||||
|
Column(
|
||||||
|
"cookie_id",
|
||||||
|
Integer(),
|
||||||
|
table="cookies",
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
Column("cookie_name", String(length=50), table="cookies"),
|
||||||
|
Column("cookie_recipe_url", String(length=255), table="cookies"),
|
||||||
|
Column("cookie_sku", String(length=55), table="cookies"),
|
||||||
|
Column("quantity", Integer(), table="cookies"),
|
||||||
|
Column("unit_cost", Numeric(precision=12, scale=2), table="cookies"),
|
||||||
|
schema=None,
|
||||||
|
)
|
||||||
|
#+end_example
|
||||||
|
|
||||||
|
|
||||||
|
#+BEGIN_SRC python :results value
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import DateTime
|
||||||
|
|
||||||
|
class User(Base, MyBase):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
user_id = Column(Integer(), primary_key=True)
|
||||||
|
username = Column(String(12), nullable=False, unique=True)
|
||||||
|
email_address = Column(String(255), nullable=False)
|
||||||
|
phone = Column(String(20), nullable=False)
|
||||||
|
password = Column(String(25), nullable=False)
|
||||||
|
created_on = Column(DateTime(), default=datetime.now)
|
||||||
|
updated_on = Column(DateTime(), default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
User.display()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS[e9cc339461880657ed2388d4bc3210231a6ab18f]:
|
||||||
|
#+begin_example
|
||||||
|
Table(
|
||||||
|
"users",
|
||||||
|
MetaData(),
|
||||||
|
Column(
|
||||||
|
"user_id", Integer(), table="users", primary_key=True, nullable=False
|
||||||
|
),
|
||||||
|
Column("username", String(length=12), table="users", nullable=False),
|
||||||
|
Column("email_address", String(length=255), table="users", nullable=False),
|
||||||
|
Column("phone", String(length=20), table="users", nullable=False),
|
||||||
|
Column("password", String(length=25), table="users", nullable=False),
|
||||||
|
Column("created_on", DateTime(), table="users", default=NOW),
|
||||||
|
Column("updated_on", DateTime(), table="users", onupdate=NOW, default=NOW),
|
||||||
|
schema=None,
|
||||||
|
)
|
||||||
|
#+end_example
|
||||||
|
|
||||||
|
*** Keys, Constraints, and Indexes
|
||||||
|
#+BEGIN_SRC python :results none :tangle no
|
||||||
|
class SomeDataClass(Base):
|
||||||
|
__tablename__ = "somedataclass"
|
||||||
|
__table_args__ = (
|
||||||
|
ForeignKeyConstraint("id", "other_table.id"),
|
||||||
|
CheckConstraint("unit_cost >= 0.00", name="unit_cost_positive"),
|
||||||
|
)
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
*** Relationships
|
||||||
|
#+BEGIN_SRC python :results value
|
||||||
|
from sqlalchemy import ForeignKey, Boolean
|
||||||
|
from sqlalchemy.orm import relationship, backref
|
||||||
|
|
||||||
|
|
||||||
|
class Order(Base, MyBase):
|
||||||
|
__tablename__ = "orders"
|
||||||
|
|
||||||
|
order_id = Column(Integer(), primary_key=True)
|
||||||
|
user_id = Column(Integer(), ForeignKey("users.user_id"))
|
||||||
|
shipped = Column(Boolean(), default=False)
|
||||||
|
|
||||||
|
user = relationship("User", backref=backref("orders", order_by=order_id))
|
||||||
|
|
||||||
|
|
||||||
|
Order.display()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS[081b1bbdd2e372705d12cca3f6c0adc1f5c73652]:
|
||||||
|
#+begin_example
|
||||||
|
Table(
|
||||||
|
"orders",
|
||||||
|
MetaData(),
|
||||||
|
Column(
|
||||||
|
"order_id", Integer(), table="orders", primary_key=True, nullable=False
|
||||||
|
),
|
||||||
|
Column("user_id", Integer(), ForeignKey("users.user_id"), table="orders"),
|
||||||
|
Column(
|
||||||
|
"shipped",
|
||||||
|
Boolean(),
|
||||||
|
table="orders",
|
||||||
|
default=ScalarElementColumnDefault(False),
|
||||||
|
),
|
||||||
|
schema=None,
|
||||||
|
)
|
||||||
|
#+end_example
|
||||||
|
|
||||||
|
One-to-One
|
||||||
|
#+BEGIN_SRC python :results value
|
||||||
|
class LineItem(Base, MyBase):
|
||||||
|
__tablename__ = "line_items"
|
||||||
|
|
||||||
|
line_item_id = Column(Integer(), primary_key=True)
|
||||||
|
order_id = Column(Integer(), ForeignKey("orders.order_id"))
|
||||||
|
cookie_id = Column(Integer(), ForeignKey("cookies.cookie_id"))
|
||||||
|
quantity = Column(Integer())
|
||||||
|
extended_cost = Column(Numeric(12, 2))
|
||||||
|
order = relationship("Order", backref=backref("line_items", order_by=line_item_id))
|
||||||
|
cookie = relationship("Cookie", uselist=False)
|
||||||
|
|
||||||
|
LineItem.display()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS[0a7fb3d313c7117a29787471715c6b37d8820c1b]:
|
||||||
|
#+begin_example
|
||||||
|
Table(
|
||||||
|
"line_items",
|
||||||
|
MetaData(),
|
||||||
|
Column(
|
||||||
|
"line_item_id",
|
||||||
|
Integer(),
|
||||||
|
table="line_items",
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
"order_id", Integer(), ForeignKey("orders.order_id"), table="line_items"
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
"cookie_id",
|
||||||
|
Integer(),
|
||||||
|
ForeignKey("cookies.cookie_id"),
|
||||||
|
table="line_items",
|
||||||
|
),
|
||||||
|
Column("quantity", Integer(), table="line_items"),
|
||||||
|
Column("extended_cost", Numeric(precision=12, scale=2), table="line_items"),
|
||||||
|
schema=None,
|
||||||
|
)
|
||||||
|
#+end_example
|
||||||
|
|
||||||
|
#+BEGIN_SRC python :results value
|
||||||
|
Order.display()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS[75aad6fe29bd5403e2810c4b62e4667147e65e8f]:
|
||||||
|
#+begin_example
|
||||||
|
Table(
|
||||||
|
"orders",
|
||||||
|
MetaData(),
|
||||||
|
Column(
|
||||||
|
"order_id", Integer(), table="orders", primary_key=True, nullable=False
|
||||||
|
),
|
||||||
|
Column("user_id", Integer(), ForeignKey("users.user_id"), table="orders"),
|
||||||
|
Column(
|
||||||
|
"shipped",
|
||||||
|
Boolean(),
|
||||||
|
table="orders",
|
||||||
|
default=ScalarElementColumnDefault(False),
|
||||||
|
),
|
||||||
|
schema=None,
|
||||||
|
)
|
||||||
|
#+end_example
|
||||||
|
|
||||||
|
#+BEGIN_SRC python :results none
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
engine = create_engine("sqlite:///:memory:")
|
||||||
|
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** SQLAlchemy v2
|
||||||
|
:PROPERTIES:
|
||||||
|
:header-args:python: :session *ch6-v2* :tangle ch6-v2.py :cache yes
|
||||||
|
:END:
|
||||||
|
#+BEGIN_SRC python :shebang "#!/usr/bin/env python3" :results value
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from ruff_format import format_string
|
||||||
|
from sqlalchemy import (
|
||||||
|
Boolean,
|
||||||
|
DateTime,
|
||||||
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
|
Numeric,
|
||||||
|
String,
|
||||||
|
create_engine,
|
||||||
|
select,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import (
|
||||||
|
DeclarativeBase,
|
||||||
|
Mapped,
|
||||||
|
mapped_column,
|
||||||
|
relationship,
|
||||||
|
Session,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
"""Базовый класс для всех декларативных моделей."""
|
||||||
|
|
||||||
|
|
||||||
|
class MyBase:
|
||||||
|
"""Миксин для красивого отображения таблицы."""
|
||||||
|
|
||||||
|
@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, MyBase):
|
||||||
|
__tablename__ = "cookies"
|
||||||
|
|
||||||
|
cookie_id: Mapped[int] = mapped_column(Integer, 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))
|
||||||
|
|
||||||
|
|
||||||
|
Cookie.display()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS[f185c05d802c49b3e988a951968bc5555eb88c57]:
|
||||||
|
#+begin_example
|
||||||
|
Table(
|
||||||
|
"cookies",
|
||||||
|
MetaData(),
|
||||||
|
Column(
|
||||||
|
"cookie_id",
|
||||||
|
Integer(),
|
||||||
|
table="cookies",
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
Column("cookie_name", String(length=50), table="cookies"),
|
||||||
|
Column("cookie_recipe_url", String(length=255), table="cookies"),
|
||||||
|
Column("cookie_sku", String(length=55), table="cookies"),
|
||||||
|
Column("quantity", Integer(), table="cookies"),
|
||||||
|
Column("unit_cost", Numeric(precision=12, scale=2), table="cookies"),
|
||||||
|
schema=None,
|
||||||
|
)
|
||||||
|
#+end_example
|
||||||
|
|
||||||
|
|
||||||
|
#+BEGIN_SRC python :results value
|
||||||
|
class User(Base, MyBase):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
user_id: Mapped[int] = mapped_column(Integer, 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
|
||||||
|
)
|
||||||
|
|
||||||
|
orders: Mapped[list["Order"]] = relationship(
|
||||||
|
back_populates="user", order_by="Order.order_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
User.display()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS[d8d31f6b8ede872fe62231ee9e4f9380c2455209]:
|
||||||
|
#+begin_example
|
||||||
|
Table(
|
||||||
|
"users",
|
||||||
|
MetaData(),
|
||||||
|
Column(
|
||||||
|
"user_id", Integer(), table="users", primary_key=True, nullable=False
|
||||||
|
),
|
||||||
|
Column("username", String(length=12), table="users", nullable=False),
|
||||||
|
Column("email_address", String(length=255), table="users", nullable=False),
|
||||||
|
Column("phone", String(length=20), table="users", nullable=False),
|
||||||
|
Column("password", String(length=25), table="users", nullable=False),
|
||||||
|
Column(
|
||||||
|
"created_on", DateTime(), table="users", nullable=False, default=NOW
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
"updated_on",
|
||||||
|
DateTime(),
|
||||||
|
table="users",
|
||||||
|
nullable=False,
|
||||||
|
onupdate=NOW,
|
||||||
|
default=NOW,
|
||||||
|
),
|
||||||
|
schema=None,
|
||||||
|
)
|
||||||
|
#+end_example
|
||||||
|
|
||||||
|
*** Keys, Constraints, and Indexes
|
||||||
|
#+BEGIN_SRC python :results none :tangle no
|
||||||
|
from sqlalchemy import CheckConstraint, ForeignKeyConstraint
|
||||||
|
|
||||||
|
|
||||||
|
class SomeDataClass(Base):
|
||||||
|
__tablename__ = "somedataclass"
|
||||||
|
__table_args__ = (
|
||||||
|
ForeignKeyConstraint(["id"], ["other_table.id"]),
|
||||||
|
CheckConstraint("unit_cost >= 0.00", name="unit_cost_positive"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
unit_cost: Mapped[Decimal | None] = mapped_column(Numeric(12, 2))
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
*** Relationships
|
||||||
|
#+BEGIN_SRC python :results value
|
||||||
|
class Order(Base, MyBase):
|
||||||
|
__tablename__ = "orders"
|
||||||
|
|
||||||
|
order_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
user_id: Mapped[int | None] = mapped_column(ForeignKey("users.user_id"))
|
||||||
|
shipped: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
|
user: Mapped["User | None"] = relationship(back_populates="orders")
|
||||||
|
line_items: Mapped[list["LineItem"]] = relationship(
|
||||||
|
back_populates="order", order_by="LineItem.line_item_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Order.display()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS[6ece26db13e2cf39ae98338d93d50f5139ed05a0]:
|
||||||
|
#+begin_example
|
||||||
|
Table(
|
||||||
|
"orders",
|
||||||
|
MetaData(),
|
||||||
|
Column(
|
||||||
|
"order_id", Integer(), table="orders", primary_key=True, nullable=False
|
||||||
|
),
|
||||||
|
Column("user_id", Integer(), ForeignKey("users.user_id"), table="orders"),
|
||||||
|
Column(
|
||||||
|
"shipped",
|
||||||
|
Boolean(),
|
||||||
|
table="orders",
|
||||||
|
default=ScalarElementColumnDefault(False),
|
||||||
|
),
|
||||||
|
schema=None,
|
||||||
|
)
|
||||||
|
#+end_example
|
||||||
|
|
||||||
|
One-to-One
|
||||||
|
#+BEGIN_SRC python :results value
|
||||||
|
class LineItem(Base, MyBase):
|
||||||
|
__tablename__ = "line_items"
|
||||||
|
|
||||||
|
line_item_id: Mapped[int] = mapped_column(Integer, 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 | None"] = relationship(back_populates="line_items")
|
||||||
|
cookie: Mapped["Cookie | None"] = relationship(uselist=False)
|
||||||
|
|
||||||
|
|
||||||
|
LineItem.display()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS[823e900242cca3c4999b00a1efc879ea28aa5060]:
|
||||||
|
#+begin_example
|
||||||
|
Table(
|
||||||
|
"line_items",
|
||||||
|
MetaData(),
|
||||||
|
Column(
|
||||||
|
"line_item_id",
|
||||||
|
Integer(),
|
||||||
|
table="line_items",
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
"order_id", Integer(), ForeignKey("orders.order_id"), table="line_items"
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
"cookie_id",
|
||||||
|
Integer(),
|
||||||
|
ForeignKey("cookies.cookie_id"),
|
||||||
|
table="line_items",
|
||||||
|
),
|
||||||
|
Column("quantity", Integer(), table="line_items"),
|
||||||
|
Column("extended_cost", Numeric(precision=12, scale=2), table="line_items"),
|
||||||
|
schema=None,
|
||||||
|
)
|
||||||
|
#+end_example
|
||||||
|
|
||||||
|
#+BEGIN_SRC python :results value
|
||||||
|
Order.display()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS[ef90d3beef41c5187fbc5ee9f04a074da1fb310a]:
|
||||||
|
#+begin_example
|
||||||
|
Table(
|
||||||
|
"orders",
|
||||||
|
MetaData(),
|
||||||
|
Column(
|
||||||
|
"order_id", Integer(), table="orders", primary_key=True, nullable=False
|
||||||
|
),
|
||||||
|
Column("user_id", Integer(), ForeignKey("users.user_id"), table="orders"),
|
||||||
|
Column(
|
||||||
|
"shipped",
|
||||||
|
Boolean(),
|
||||||
|
table="orders",
|
||||||
|
default=ScalarElementColumnDefault(False),
|
||||||
|
),
|
||||||
|
schema=None,
|
||||||
|
)
|
||||||
|
#+end_example
|
||||||
|
|
||||||
|
|
||||||
|
#+BEGIN_SRC python :results none
|
||||||
|
engine = create_engine("sqlite:///:memory:")
|
||||||
|
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
session.add(
|
||||||
|
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.commit()
|
||||||
|
|
||||||
|
stmt = select(Cookie).where(Cookie.cookie_name == "chocolate chip")
|
||||||
|
for cookie in session.scalars(stmt):
|
||||||
|
print(cookie.cookie_name, cookie.unit_cost)
|
||||||
|
#+END_SRC
|
||||||
|
|||||||
Reference in New Issue
Block a user