SQLAlchemy Essential: Chapter 3
This commit is contained in:
@@ -1523,3 +1523,746 @@
|
|||||||
#+RESULTS:
|
#+RESULTS:
|
||||||
| 1 | cookiemon | 111-111-1111 | chocolate chip | 2 | Decimal | (1.00) |
|
| 1 | cookiemon | 111-111-1111 | chocolate chip | 2 | Decimal | (1.00) |
|
||||||
| 1 | cookiemon | 111-111-1111 | ligth chocolate chip | 12 | Decimal | (3.00) |
|
| 1 | cookiemon | 111-111-1111 | ligth chocolate chip | 12 | Decimal | (3.00) |
|
||||||
|
|
||||||
|
* Chapter 3: Exceptions and Transactions
|
||||||
|
|
||||||
|
** SQLAlchemy v1
|
||||||
|
:PROPERTIES:
|
||||||
|
:header-args: :cache no
|
||||||
|
:header-args:python: :tangle ch3-v1.py
|
||||||
|
:header-args: :session *ch3*
|
||||||
|
:END:
|
||||||
|
|
||||||
|
#+BEGIN_SRC python :shebang "#!/usr/bin/env python3" :results none
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stderr = sys.stdout
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
Boolean,
|
||||||
|
CheckConstraint,
|
||||||
|
Column,
|
||||||
|
DateTime,
|
||||||
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
|
MetaData,
|
||||||
|
Numeric,
|
||||||
|
String,
|
||||||
|
Table,
|
||||||
|
create_engine,
|
||||||
|
PrimaryKeyConstraint,
|
||||||
|
UniqueConstraint,
|
||||||
|
)
|
||||||
|
from sqlparse import format as sqlformat
|
||||||
|
|
||||||
|
|
||||||
|
def sqlf(
|
||||||
|
sql: str | object,
|
||||||
|
reindent=True,
|
||||||
|
keyword_case="upper",
|
||||||
|
indent_width=2,
|
||||||
|
strip_comments=False,
|
||||||
|
identifier_case="lower",
|
||||||
|
):
|
||||||
|
if not isinstance(sql, str):
|
||||||
|
sql = str(sql)
|
||||||
|
return sqlformat(
|
||||||
|
sql,
|
||||||
|
encoding="utf8",
|
||||||
|
reindent=reindent,
|
||||||
|
keyword_case=keyword_case,
|
||||||
|
indent_width=indent_width,
|
||||||
|
strip_comments=strip_comments,
|
||||||
|
identifier_case=identifier_case,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
metadata = MetaData()
|
||||||
|
|
||||||
|
cookies = Table(
|
||||||
|
"cookies",
|
||||||
|
metadata,
|
||||||
|
Column("cookie_id", Integer(), primary_key=True),
|
||||||
|
Column("cookie_name", String(50), index=True),
|
||||||
|
Column("cookie_recipe_url", String(255), unique=True),
|
||||||
|
Column("cookie_sku", String(55), unique=True),
|
||||||
|
Column("quantity", Integer()),
|
||||||
|
Column("unit_cost", Numeric(12, 2)),
|
||||||
|
CheckConstraint("unit_cost >= 0.00", name="unit_cost_positive"),
|
||||||
|
CheckConstraint("quantity >= 0", name="quantity_positive"),
|
||||||
|
)
|
||||||
|
|
||||||
|
users = Table(
|
||||||
|
"users",
|
||||||
|
metadata,
|
||||||
|
Column("user_id", Integer()),
|
||||||
|
Column("username", String(15), nullable=False),
|
||||||
|
Column("email_address", String(255), nullable=False),
|
||||||
|
Column("phone", String(20), nullable=False),
|
||||||
|
Column("password", String(25), nullable=False),
|
||||||
|
Column("created_on", DateTime(), default=datetime.now),
|
||||||
|
Column("updated_on", DateTime(), default=datetime.now, onupdate=datetime.now),
|
||||||
|
PrimaryKeyConstraint("user_id", name="user_pk"),
|
||||||
|
UniqueConstraint("username", name="uix_username"),
|
||||||
|
)
|
||||||
|
|
||||||
|
orders = Table(
|
||||||
|
"orders",
|
||||||
|
metadata,
|
||||||
|
Column("order_id", Integer(), primary_key=True),
|
||||||
|
Column("user_id", ForeignKey("users.user_id")),
|
||||||
|
Column("shipped", Boolean(), default=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
line_items = Table(
|
||||||
|
"line_items",
|
||||||
|
metadata,
|
||||||
|
Column("line_items_id", Integer(), primary_key=True),
|
||||||
|
Column("order_id", ForeignKey("orders.order_id")),
|
||||||
|
Column("cookie_id", ForeignKey("cookies.cookie_id")),
|
||||||
|
Column("quantity", Integer()),
|
||||||
|
Column("extended_cost", Numeric(12, 2)),
|
||||||
|
)
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///:memory:", echo=True)
|
||||||
|
connection = engine.connect()
|
||||||
|
metadata.create_all(engine)
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
*** AttributeError
|
||||||
|
#+BEGIN_SRC python :results output
|
||||||
|
import logging
|
||||||
|
from sqlalchemy import select, insert
|
||||||
|
|
||||||
|
ins = insert(users).values(
|
||||||
|
username="cookiemon",
|
||||||
|
email_address="mon@cookie.com",
|
||||||
|
phone="111-111-1111",
|
||||||
|
password="password",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = connection.execute(ins)
|
||||||
|
|
||||||
|
s = select(users.c.username)
|
||||||
|
results = connection.execute(s)
|
||||||
|
|
||||||
|
try:
|
||||||
|
[(result.username, result.password) for result in results]
|
||||||
|
except AttributeError as exc:
|
||||||
|
logging.exception(results)
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS:
|
||||||
|
#+begin_example
|
||||||
|
2026-09-18 12:06:22,953 INFO sqlalchemy.engine.Engine BEGIN (implicit)
|
||||||
|
INFO:sqlalchemy.engine.Engine:BEGIN (implicit)
|
||||||
|
2026-09-18 12:06:22,954 INFO sqlalchemy.engine.Engine INSERT INTO users (username, email_address, phone, password, created_on, updated_on) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
INFO:sqlalchemy.engine.Engine:INSERT INTO users (username, email_address, phone, password, created_on, updated_on) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
2026-09-18 12:06:22,954 INFO sqlalchemy.engine.Engine [generated in 0.00036s] ('cookiemon', 'mon@cookie.com', '111-111-1111', 'password', '2026-09-18 12:06:22.953895', '2026-09-18 12:06:22.953904')
|
||||||
|
INFO:sqlalchemy.engine.Engine:[generated in 0.00036s] ('cookiemon', 'mon@cookie.com', '111-111-1111', 'password', '2026-09-18 12:06:22.953895', '2026-09-18 12:06:22.953904')
|
||||||
|
2026-09-18 12:06:22,954 INFO sqlalchemy.engine.Engine SELECT users.username
|
||||||
|
FROM users
|
||||||
|
INFO:sqlalchemy.engine.Engine:SELECT users.username
|
||||||
|
FROM users
|
||||||
|
2026-09-18 12:06:22,954 INFO sqlalchemy.engine.Engine [generated in 0.00014s] ()
|
||||||
|
INFO:sqlalchemy.engine.Engine:[generated in 0.00014s] ()
|
||||||
|
ERROR:root:<sqlalchemy.engine.cursor.CursorResult object at 0x7fa8f6747950>
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/result.py", line 211, in _key_not_found
|
||||||
|
self._key_fallback(key, None)
|
||||||
|
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/cursor.py", line 851, in _key_fallback
|
||||||
|
raise exc.NoSuchColumnError(
|
||||||
|
...<2 lines>...
|
||||||
|
) from err
|
||||||
|
sqlalchemy.exc.NoSuchColumnError: Could not locate column in row for column 'password'
|
||||||
|
|
||||||
|
The above exception was the direct cause of the following exception:
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/tmp/babel-sg4Ps9/python-VQah8h", line 17, in <module>
|
||||||
|
[(result.username, result.password) for result in results]
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
File "lib/sqlalchemy/cyextension/resultproxy.pyx", line 66, in sqlalchemy.cyextension.resultproxy.BaseRow.__getattr__
|
||||||
|
File "lib/sqlalchemy/cyextension/resultproxy.pyx", line 63, in sqlalchemy.cyextension.resultproxy.BaseRow._get_by_key_impl
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/result.py", line 213, in _key_not_found
|
||||||
|
raise AttributeError(ke.args[0]) from ke
|
||||||
|
AttributeError: Could not locate column in row for column 'password'
|
||||||
|
#+end_example
|
||||||
|
*** IntegrityError
|
||||||
|
#+BEGIN_SRC python :results output
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
s = select(users.c.username)
|
||||||
|
print(connection.execute(s).fetchall())
|
||||||
|
|
||||||
|
ins = insert(users).values(
|
||||||
|
username="cookiemon",
|
||||||
|
email_address="damon@cookie.com",
|
||||||
|
phone="111-111-1111",
|
||||||
|
password="password",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = connection.execute(ins)
|
||||||
|
except IntegrityError as exc:
|
||||||
|
logging.exception(str(ins))
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS[cdbcb9c1f18b5c62c2127f1ac9ffd14b2a38039e]:
|
||||||
|
#+begin_example
|
||||||
|
2026-09-18 12:06:22,969 INFO sqlalchemy.engine.Engine SELECT users.username
|
||||||
|
FROM users
|
||||||
|
INFO:sqlalchemy.engine.Engine:SELECT users.username
|
||||||
|
FROM users
|
||||||
|
2026-09-18 12:06:22,969 INFO sqlalchemy.engine.Engine [cached since 0.01439s ago] ()
|
||||||
|
INFO:sqlalchemy.engine.Engine:[cached since 0.01439s ago] ()
|
||||||
|
[('cookiemon',)]
|
||||||
|
2026-09-18 12:06:22,969 INFO sqlalchemy.engine.Engine INSERT INTO users (username, email_address, phone, password, created_on, updated_on) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
INFO:sqlalchemy.engine.Engine:INSERT INTO users (username, email_address, phone, password, created_on, updated_on) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
2026-09-18 12:06:22,969 INFO sqlalchemy.engine.Engine [cached since 0.0159s ago] ('cookiemon', 'damon@cookie.com', '111-111-1111', 'password', '2026-09-18 12:06:22.969623', '2026-09-18 12:06:22.969627')
|
||||||
|
INFO:sqlalchemy.engine.Engine:[cached since 0.0159s ago] ('cookiemon', 'damon@cookie.com', '111-111-1111', 'password', '2026-09-18 12:06:22.969623', '2026-09-18 12:06:22.969627')
|
||||||
|
ERROR:root:INSERT INTO users (username, email_address, phone, password, created_on, updated_on) VALUES (:username, :email_address, :phone, :password, :created_on, :updated_on)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
|
||||||
|
self.dialect.do_execute(
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~^
|
||||||
|
cursor, str_statement, effective_parameters, context
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
)
|
||||||
|
^
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/default.py", line 952, in do_execute
|
||||||
|
cursor.execute(statement, parameters)
|
||||||
|
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
sqlite3.IntegrityError: UNIQUE constraint failed: users.username
|
||||||
|
|
||||||
|
The above exception was the direct cause of the following exception:
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/tmp/babel-sg4Ps9/python-Dqwfvm", line 13, in <module>
|
||||||
|
result = connection.execute(ins)
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1421, in execute
|
||||||
|
return meth(
|
||||||
|
self,
|
||||||
|
distilled_parameters,
|
||||||
|
execution_options or NO_OPTIONS,
|
||||||
|
)
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/sql/elements.py", line 526, in _execute_on_connection
|
||||||
|
return connection._execute_clauseelement(
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
|
||||||
|
self, distilled_params, execution_options
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
)
|
||||||
|
^
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1643, in _execute_clauseelement
|
||||||
|
ret = self._execute_context(
|
||||||
|
dialect,
|
||||||
|
...<8 lines>...
|
||||||
|
cache_hit=cache_hit,
|
||||||
|
)
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context
|
||||||
|
return self._exec_single_context(
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~^
|
||||||
|
dialect, context, statement, parameters
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
)
|
||||||
|
^
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context
|
||||||
|
self._handle_dbapi_exception(
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
|
||||||
|
e, str_statement, effective_parameters, cursor, context
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
)
|
||||||
|
^
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 2365, in _handle_dbapi_exception
|
||||||
|
raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
|
||||||
|
self.dialect.do_execute(
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~^
|
||||||
|
cursor, str_statement, effective_parameters, context
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
)
|
||||||
|
^
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/default.py", line 952, in do_execute
|
||||||
|
cursor.execute(statement, parameters)
|
||||||
|
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: users.username
|
||||||
|
[SQL: INSERT INTO users (username, email_address, phone, password, created_on, updated_on) VALUES (?, ?, ?, ?, ?, ?)]
|
||||||
|
[parameters: ('cookiemon', 'damon@cookie.com', '111-111-1111', 'password', '2026-09-18 12:06:22.969623', '2026-09-18 12:06:22.969627')]
|
||||||
|
(Background on this error at: https://sqlalche.me/e/20/gkpj)
|
||||||
|
#+end_example
|
||||||
|
*** Transactions
|
||||||
|
#+BEGIN_SRC python :results none
|
||||||
|
ins = cookies.insert()
|
||||||
|
inventory_list = [
|
||||||
|
{
|
||||||
|
"cookie_name": "chocolate chip",
|
||||||
|
"cookie_recipe_url": "http://some.aweso.me/cookie/recipe.html",
|
||||||
|
"cookie_sku": "CC01",
|
||||||
|
"quantity": "12",
|
||||||
|
"unit_cost": "0.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie_name": "dark chocolate chip",
|
||||||
|
"cookie_recipe_url": "http://some.aweso.me/cookie/recipe_dark.html",
|
||||||
|
"cookie_sku": "CC02",
|
||||||
|
"quantity": "1",
|
||||||
|
"unit_cost": "0.75",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
result = connection.execute(ins, inventory_list)
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+BEGIN_SRC python :results none
|
||||||
|
ins = insert(orders).values(user_id=1, order_id="1")
|
||||||
|
result = connection.execute(ins)
|
||||||
|
ins = insert(line_items)
|
||||||
|
order_items = [
|
||||||
|
{
|
||||||
|
"order_id": 1,
|
||||||
|
"cookie_id": 1,
|
||||||
|
"quantity": 9,
|
||||||
|
"extended_cost": 4.50,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = connection.execute(ins, order_items)
|
||||||
|
|
||||||
|
ins = insert(orders).values(user_id=1, order_id="2")
|
||||||
|
result = connection.execute(ins)
|
||||||
|
ins = insert(line_items)
|
||||||
|
order_items = [
|
||||||
|
{
|
||||||
|
"order_id": 2,
|
||||||
|
"cookie_id": 1,
|
||||||
|
"quantity": 4,
|
||||||
|
"extended_cost": 1.50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order_id": 2,
|
||||||
|
"cookie_id": 2,
|
||||||
|
"quantity": 1,
|
||||||
|
"extended_cost": 4.50,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
result = connection.execute(ins, order_items)
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+BEGIN_SRC python :results none
|
||||||
|
from sqlalchemy import update
|
||||||
|
|
||||||
|
|
||||||
|
def ship_it(order_id: int) -> None:
|
||||||
|
s = select(line_items.c.cookie_id, line_items.c.quantity)
|
||||||
|
s = s.where(line_items.c.order_id == order_id)
|
||||||
|
cookies_to_ship = connection.execute(s)
|
||||||
|
for cookie in cookies_to_ship:
|
||||||
|
u = (
|
||||||
|
update(cookies)
|
||||||
|
.where(cookies.c.cookie_id == cookie.cookie_id)
|
||||||
|
.values(quantity=cookies.c.quantity - cookie.quantity)
|
||||||
|
)
|
||||||
|
result = connection.execute(u)
|
||||||
|
u = update(orders).where(orders.c.order_id == order_id).values(shipped=True)
|
||||||
|
connection.execute(u)
|
||||||
|
print(f"Shipped order ID: {order_id}")
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+BEGIN_SRC python :results output
|
||||||
|
ship_it(1)
|
||||||
|
s = select(cookies.c.cookie_name, cookies.c.quantity)
|
||||||
|
print(connection.execute(s).fetchall())
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS:
|
||||||
|
#+begin_example
|
||||||
|
2026-09-18 12:06:23,066 INFO sqlalchemy.engine.Engine SELECT line_items.cookie_id, line_items.quantity
|
||||||
|
FROM line_items
|
||||||
|
WHERE line_items.order_id = ?
|
||||||
|
INFO:sqlalchemy.engine.Engine:SELECT line_items.cookie_id, line_items.quantity
|
||||||
|
FROM line_items
|
||||||
|
WHERE line_items.order_id = ?
|
||||||
|
2026-09-18 12:06:23,066 INFO sqlalchemy.engine.Engine [generated in 0.00024s] (1,)
|
||||||
|
INFO:sqlalchemy.engine.Engine:[generated in 0.00024s] (1,)
|
||||||
|
2026-09-18 12:06:23,067 INFO sqlalchemy.engine.Engine UPDATE cookies SET quantity=(cookies.quantity - ?) WHERE cookies.cookie_id = ?
|
||||||
|
INFO:sqlalchemy.engine.Engine:UPDATE cookies SET quantity=(cookies.quantity - ?) WHERE cookies.cookie_id = ?
|
||||||
|
2026-09-18 12:06:23,067 INFO sqlalchemy.engine.Engine [generated in 0.00009s] (9, 1)
|
||||||
|
INFO:sqlalchemy.engine.Engine:[generated in 0.00009s] (9, 1)
|
||||||
|
2026-09-18 12:06:23,067 INFO sqlalchemy.engine.Engine UPDATE orders SET shipped=? WHERE orders.order_id = ?
|
||||||
|
INFO:sqlalchemy.engine.Engine:UPDATE orders SET shipped=? WHERE orders.order_id = ?
|
||||||
|
2026-09-18 12:06:23,067 INFO sqlalchemy.engine.Engine [generated in 0.00008s] (1, 1)
|
||||||
|
INFO:sqlalchemy.engine.Engine:[generated in 0.00008s] (1, 1)
|
||||||
|
Shipped order ID: 1
|
||||||
|
2026-09-18 12:06:23,067 INFO sqlalchemy.engine.Engine SELECT cookies.cookie_name, cookies.quantity
|
||||||
|
FROM cookies
|
||||||
|
INFO:sqlalchemy.engine.Engine:SELECT cookies.cookie_name, cookies.quantity
|
||||||
|
FROM cookies
|
||||||
|
2026-09-18 12:06:23,067 INFO sqlalchemy.engine.Engine [generated in 0.00006s] ()
|
||||||
|
INFO:sqlalchemy.engine.Engine:[generated in 0.00006s] ()
|
||||||
|
[('chocolate chip', 3), ('dark chocolate chip', 1)]
|
||||||
|
#+end_example
|
||||||
|
|
||||||
|
#+BEGIN_SRC python :results output
|
||||||
|
try:
|
||||||
|
ship_it(2)
|
||||||
|
except IntegrityError:
|
||||||
|
logging.exception("")
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS:
|
||||||
|
#+begin_example
|
||||||
|
2026-09-18 12:07:29,522 INFO sqlalchemy.engine.Engine SELECT line_items.cookie_id, line_items.quantity
|
||||||
|
FROM line_items
|
||||||
|
WHERE line_items.order_id = ?
|
||||||
|
INFO:sqlalchemy.engine.Engine:SELECT line_items.cookie_id, line_items.quantity
|
||||||
|
FROM line_items
|
||||||
|
WHERE line_items.order_id = ?
|
||||||
|
2026-09-18 12:07:29,522 INFO sqlalchemy.engine.Engine [cached since 66.46s ago] (2,)
|
||||||
|
INFO:sqlalchemy.engine.Engine:[cached since 66.46s ago] (2,)
|
||||||
|
2026-09-18 12:07:29,523 INFO sqlalchemy.engine.Engine UPDATE cookies SET quantity=(cookies.quantity - ?) WHERE cookies.cookie_id = ?
|
||||||
|
INFO:sqlalchemy.engine.Engine:UPDATE cookies SET quantity=(cookies.quantity - ?) WHERE cookies.cookie_id = ?
|
||||||
|
2026-09-18 12:07:29,523 INFO sqlalchemy.engine.Engine [cached since 66.46s ago] (4, 1)
|
||||||
|
INFO:sqlalchemy.engine.Engine:[cached since 66.46s ago] (4, 1)
|
||||||
|
ERROR:root:
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
|
||||||
|
self.dialect.do_execute(
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~^
|
||||||
|
cursor, str_statement, effective_parameters, context
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
)
|
||||||
|
^
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/default.py", line 952, in do_execute
|
||||||
|
cursor.execute(statement, parameters)
|
||||||
|
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
sqlite3.IntegrityError: CHECK constraint failed: quantity_positive
|
||||||
|
|
||||||
|
The above exception was the direct cause of the following exception:
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/tmp/babel-sg4Ps9/python-swDwMp", line 2, in <module>
|
||||||
|
ship_it(2)
|
||||||
|
~~~~~~~^^^
|
||||||
|
File "<string>", line 14, in ship_it
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1421, in execute
|
||||||
|
return meth(
|
||||||
|
self,
|
||||||
|
distilled_parameters,
|
||||||
|
execution_options or NO_OPTIONS,
|
||||||
|
)
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/sql/elements.py", line 526, in _execute_on_connection
|
||||||
|
return connection._execute_clauseelement(
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
|
||||||
|
self, distilled_params, execution_options
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
)
|
||||||
|
^
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1643, in _execute_clauseelement
|
||||||
|
ret = self._execute_context(
|
||||||
|
dialect,
|
||||||
|
...<8 lines>...
|
||||||
|
cache_hit=cache_hit,
|
||||||
|
)
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context
|
||||||
|
return self._exec_single_context(
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~^
|
||||||
|
dialect, context, statement, parameters
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
)
|
||||||
|
^
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context
|
||||||
|
self._handle_dbapi_exception(
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
|
||||||
|
e, str_statement, effective_parameters, cursor, context
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
)
|
||||||
|
^
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 2365, in _handle_dbapi_exception
|
||||||
|
raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
|
||||||
|
self.dialect.do_execute(
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~^
|
||||||
|
cursor, str_statement, effective_parameters, context
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
)
|
||||||
|
^
|
||||||
|
File "/home/octopus/study/Python/sqlalchemy-study/.venv/lib/python3.14/site-packages/sqlalchemy/engine/default.py", line 952, in do_execute
|
||||||
|
cursor.execute(statement, parameters)
|
||||||
|
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) CHECK constraint failed: quantity_positive
|
||||||
|
[SQL: UPDATE cookies SET quantity=(cookies.quantity - ?) WHERE cookies.cookie_id = ?]
|
||||||
|
[parameters: (4, 1)]
|
||||||
|
(Background on this error at: https://sqlalche.me/e/20/gkpj)
|
||||||
|
#+end_example
|
||||||
|
|
||||||
|
#+BEGIN_SRC python :results none :cache yes
|
||||||
|
def ship_it(order_id: int) -> None:
|
||||||
|
s = select(line_items.c.cookie_id, line_items.c.quantity)
|
||||||
|
s = s.where(line_items.c.order_id == order_id)
|
||||||
|
transaction = connection.begin()
|
||||||
|
cookies_to_ship = connection.execute(s).fetchall()
|
||||||
|
|
||||||
|
try:
|
||||||
|
for cookie in cookies_to_ship:
|
||||||
|
u = update(cookies).where(cookies.c.cookie_id == cookie.cookie_id).values(quantity = cookies.c.quantity -cookie.quantity)
|
||||||
|
result = connection.execute(u)
|
||||||
|
u = update(orders).where(orders.c.order_id == order_id).values(shipped=True)
|
||||||
|
result = connection.execute(u)
|
||||||
|
print(f"Shipped order ID: {order_id}")
|
||||||
|
transaction.commit()
|
||||||
|
except IntegrityError as error:
|
||||||
|
transaction.rollback()
|
||||||
|
logging.exception()
|
||||||
|
#+END_SRC
|
||||||
|
** SQLAlchemy v2
|
||||||
|
:PROPERTIES:
|
||||||
|
:header-args: :cache yes
|
||||||
|
:header-args:python: :tangle ch3-v2.py
|
||||||
|
:header-args: :session *ch3-v2*
|
||||||
|
:END:
|
||||||
|
#+BEGIN_SRC python :results output :shebang "#!/usr/bin/env python3"
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stderr = sys.stdout
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
Boolean,
|
||||||
|
CheckConstraint,
|
||||||
|
Column,
|
||||||
|
DateTime,
|
||||||
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
|
MetaData,
|
||||||
|
Numeric,
|
||||||
|
PrimaryKeyConstraint,
|
||||||
|
String,
|
||||||
|
Table,
|
||||||
|
UniqueConstraint,
|
||||||
|
create_engine,
|
||||||
|
insert,
|
||||||
|
select,
|
||||||
|
update,
|
||||||
|
)
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlparse import format as sqlformat
|
||||||
|
|
||||||
|
|
||||||
|
def sqlf(
|
||||||
|
sql: str | object,
|
||||||
|
reindent=True,
|
||||||
|
keyword_case="upper",
|
||||||
|
indent_width=2,
|
||||||
|
strip_comments=False,
|
||||||
|
identifier_case="lower",
|
||||||
|
):
|
||||||
|
if not isinstance(sql, str):
|
||||||
|
sql = str(sql)
|
||||||
|
return sqlformat(
|
||||||
|
sql,
|
||||||
|
encoding="utf8",
|
||||||
|
reindent=reindent,
|
||||||
|
keyword_case=keyword_case,
|
||||||
|
indent_width=indent_width,
|
||||||
|
strip_comments=strip_comments,
|
||||||
|
identifier_case=identifier_case,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
metadata = MetaData()
|
||||||
|
|
||||||
|
cookies = Table(
|
||||||
|
"cookies",
|
||||||
|
metadata,
|
||||||
|
Column("cookie_id", Integer(), primary_key=True),
|
||||||
|
Column("cookie_name", String(50), index=True),
|
||||||
|
Column("cookie_recipe_url", String(255), unique=True),
|
||||||
|
Column("cookie_sku", String(55), unique=True),
|
||||||
|
Column("quantity", Integer()),
|
||||||
|
Column("unit_cost", Numeric(12, 2)),
|
||||||
|
CheckConstraint("unit_cost >= 0.00", name="unit_cost_positive"),
|
||||||
|
CheckConstraint("quantity >= 0", name="quantity_positive"),
|
||||||
|
)
|
||||||
|
|
||||||
|
users = Table(
|
||||||
|
"users",
|
||||||
|
metadata,
|
||||||
|
Column("user_id", Integer()),
|
||||||
|
Column("username", String(15), nullable=False),
|
||||||
|
Column("email_address", String(255), nullable=False),
|
||||||
|
Column("phone", String(20), nullable=False),
|
||||||
|
Column("password", String(25), nullable=False),
|
||||||
|
Column("created_on", DateTime(), default=datetime.now),
|
||||||
|
Column("updated_on", DateTime(), default=datetime.now, onupdate=datetime.now),
|
||||||
|
PrimaryKeyConstraint("user_id", name="user_pk"),
|
||||||
|
UniqueConstraint("username", name="uix_username"),
|
||||||
|
)
|
||||||
|
|
||||||
|
orders = Table(
|
||||||
|
"orders",
|
||||||
|
metadata,
|
||||||
|
Column("order_id", Integer(), primary_key=True),
|
||||||
|
Column("user_id", ForeignKey("users.user_id")),
|
||||||
|
Column("shipped", Boolean(), default=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
line_items = Table(
|
||||||
|
"line_items",
|
||||||
|
metadata,
|
||||||
|
Column("line_items_id", Integer(), primary_key=True),
|
||||||
|
Column("order_id", ForeignKey("orders.order_id")),
|
||||||
|
Column("cookie_id", ForeignKey("cookies.cookie_id")),
|
||||||
|
Column("quantity", Integer()),
|
||||||
|
Column("extended_cost", Numeric(12, 2)),
|
||||||
|
)
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///:memory:", echo=True)
|
||||||
|
|
||||||
|
with engine.begin() as connection:
|
||||||
|
metadata.create_all(connection)
|
||||||
|
|
||||||
|
# --- insert одного пользователя ---
|
||||||
|
ins = insert(users).values(
|
||||||
|
username="cookiemon",
|
||||||
|
email_address="mon@cookie.com",
|
||||||
|
phone="111-111-1111",
|
||||||
|
password="password",
|
||||||
|
)
|
||||||
|
connection.execute(ins)
|
||||||
|
|
||||||
|
# --- select возвращает Row, у которого есть .username ---
|
||||||
|
s = select(users.c.username, users.c.password)
|
||||||
|
results = connection.execute(s)
|
||||||
|
try:
|
||||||
|
[(row.username, row.password) for row in results]
|
||||||
|
except AttributeError:
|
||||||
|
logging.exception(results)
|
||||||
|
|
||||||
|
# --- проверка вставки ---
|
||||||
|
s = select(users.c.username)
|
||||||
|
print(connection.execute(s).fetchall())
|
||||||
|
|
||||||
|
# --- нарушение уникальности username ---
|
||||||
|
ins = insert(users).values(
|
||||||
|
username="cookiemon",
|
||||||
|
email_address="damon@cookie.com",
|
||||||
|
phone="111-111-1111",
|
||||||
|
password="password",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with connection.begin_nested(): # SAVEPOINT, чтобы не убить внешнюю транзакцию
|
||||||
|
connection.execute(ins)
|
||||||
|
except IntegrityError:
|
||||||
|
logging.exception(str(ins))
|
||||||
|
|
||||||
|
# --- массовая вставка cookies ---
|
||||||
|
inventory_list = [
|
||||||
|
{
|
||||||
|
"cookie_name": "chocolate chip",
|
||||||
|
"cookie_recipe_url": "http://some.aweso.me/cookie/recipe.html",
|
||||||
|
"cookie_sku": "CC01",
|
||||||
|
"quantity": "12",
|
||||||
|
"unit_cost": "0.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie_name": "dark chocolate chip",
|
||||||
|
"cookie_recipe_url": "http://some.aweso.me/cookie/recipe_dark.html",
|
||||||
|
"cookie_sku": "CC02",
|
||||||
|
"quantity": "1",
|
||||||
|
"unit_cost": "0.75",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
connection.execute(insert(cookies), inventory_list)
|
||||||
|
|
||||||
|
# --- заказы ---
|
||||||
|
connection.execute(insert(orders).values(user_id=1, order_id="1"))
|
||||||
|
connection.execute(
|
||||||
|
insert(line_items),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"order_id": 1,
|
||||||
|
"cookie_id": 1,
|
||||||
|
"quantity": 9,
|
||||||
|
"extended_cost": 4.50,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
connection.execute(insert(orders).values(user_id=1, order_id="2"))
|
||||||
|
connection.execute(
|
||||||
|
insert(line_items),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"order_id": 2,
|
||||||
|
"cookie_id": 1,
|
||||||
|
"quantity": 4,
|
||||||
|
"extended_cost": 1.50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order_id": 2,
|
||||||
|
"cookie_id": 2,
|
||||||
|
"quantity": 1,
|
||||||
|
"extended_cost": 4.50,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ship_it(order_id: int) -> None:
|
||||||
|
"""Версия без явной транзакции — каждый execute коммитится сам."""
|
||||||
|
s = select(line_items.c.cookie_id, line_items.c.quantity).where(
|
||||||
|
line_items.c.order_id == order_id
|
||||||
|
)
|
||||||
|
cookies_to_ship = connection.execute(s).fetchall()
|
||||||
|
for cookie in cookies_to_ship:
|
||||||
|
u = (
|
||||||
|
update(cookies)
|
||||||
|
.where(cookies.c.cookie_id == cookie.cookie_id)
|
||||||
|
.values(quantity=cookies.c.quantity - cookie.quantity)
|
||||||
|
)
|
||||||
|
connection.execute(u)
|
||||||
|
u = update(orders).where(orders.c.order_id == order_id).values(shipped=True)
|
||||||
|
connection.execute(u)
|
||||||
|
print(f"Shipped order ID: {order_id}")
|
||||||
|
|
||||||
|
|
||||||
|
def ship_it_atomic(order_id: int) -> None:
|
||||||
|
"""Версия с явной транзакцией и rollback при ошибке."""
|
||||||
|
s = select(line_items.c.cookie_id, line_items.c.quantity).where(
|
||||||
|
line_items.c.order_id == order_id
|
||||||
|
)
|
||||||
|
|
||||||
|
with engine.begin() as conn:
|
||||||
|
cookies_to_ship = conn.execute(s).fetchall()
|
||||||
|
try:
|
||||||
|
for cookie in cookies_to_ship:
|
||||||
|
u = (
|
||||||
|
update(cookies)
|
||||||
|
.where(cookies.c.cookie_id == cookie.cookie_id)
|
||||||
|
.values(quantity=cookies.c.quantity - cookie.quantity)
|
||||||
|
)
|
||||||
|
conn.execute(u)
|
||||||
|
u = update(orders).where(orders.c.order_id == order_id).values(shipped=True)
|
||||||
|
conn.execute(u)
|
||||||
|
print(f"Shipped order ID: {order_id}")
|
||||||
|
except IntegrityError:
|
||||||
|
# with engine.begin() сам сделает rollback
|
||||||
|
logging.exception("Failed to ship order %s", order_id)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# --- используем транзакционную версию ---
|
||||||
|
ship_it_atomic(1)
|
||||||
|
s = select(cookies.c.cookie_name, cookies.c.quantity)
|
||||||
|
with engine.connect() as conn:
|
||||||
|
print(conn.execute(s).fetchall())
|
||||||
|
|
||||||
|
try:
|
||||||
|
ship_it_atomic(2)
|
||||||
|
except IntegrityError:
|
||||||
|
logging.exception("")
|
||||||
|
#+END_SRC
|
||||||
|
|||||||
Reference in New Issue
Block a user