Skip to content

Commit 4513d62

Browse files
authored
Documentation/714 part 2 add smaller feature details (#715)
* Create non-orm multiple example & clean up orm one * Sync examples to have similar structure * Add description of caching in SQLAlchemy.
1 parent 2cdab1b commit 4513d62

11 files changed

Lines changed: 237 additions & 26 deletions

File tree

doc/changes/unreleased.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@
1717
* #703: Added ORM examples
1818
* #706: Added more single and multiple entry ORM examples
1919
* #712: Added to features information on: autoincremented columns, foreign keys, & automatic indexes
20+
* #714: Added to features information on: caching + more non-ORM examples

doc/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,5 @@
8787
# All HTTP redirections from the source URI to
8888
# the canonical URI will be treated as "working".
8989
r"https://github\.com/.*": r"https://github\.com/login*",
90-
r"https://exasol\.my\.site\.com/s/article/.*": r"https://exasol\.my\.site\.com/s/article/.*?language=en_US",
90+
r"https://exasol\.my\.site\.com/s/.*": r"https://exasol\.my\.site\.com/s/.*?language=en_US",
9191
}

doc/user_guide/examples/non_orm/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ Basic Operations
88

99
create_tables
1010
single_entry
11+
multiple_entries
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.. _example_non_orm_multiple_entries:
2+
3+
Working with Multiple Entries
4+
=============================
5+
6+
This script inserts multiple entries into our two tables in one session & relies
7+
on in-session auto-incrementing behavior. As mentioned in the :ref:`known_limitations`,
8+
SQLAlchemy is slower than other drivers at performing multiple entry inserts.
9+
10+
.. literalinclude:: ../../../../examples/features/non_orm/_2_working_with_multiple_entries.py
11+
:language: python3
12+
:caption: examples/features/non_orm/_2_working_with_multiple_entries.py

doc/user_guide/features/index.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,38 @@ unnecessary but intentionally unsupported to prevent interference with the engin
3939
optimization algorithms. For more in-depth information, explore the Exasol documentation
4040
on `indexes <https://docs.exasol.com/db/latest/performance/indexes.htm>`__.
4141

42+
Caching
43+
-------
44+
45+
Since version 1.4, SQLAlchemy features a "SQL compilation caching" facility designed to
46+
significantly reduce Python interpreter overhead during query construction. This system
47+
works by generating a unique cache key that represents the structural state of a Core or
48+
an ORM SQL construct, including its columns, tables, and JOIN conditions. Once a
49+
specific query structure is compiled into a string for the first time, SQLAlchemy stores
50+
the result in an internal cache; subsequent executions with the same structure skip the
51+
expensive string compilation process and reuse the existing SQL. This is particularly
52+
beneficial for ORM-heavy applications, as it streamlines the logic for lazy loaders and
53+
relationship lookups.
54+
55+
For more information, see these pages from SQLAlchemy:
56+
57+
* `Performance <https://docs.sqlalchemy.org/en/21/faq/performance.html>`__
58+
* `SQL Compilation Caching <https://docs.sqlalchemy.org/en/21/core/connections.html#sql-compilation-caching>`__
59+
60+
Since version 1.4, SQLAlchemy has also streamlined its "Reflection API" by implementing
61+
an internal metadata cache within the Inspector object. This system is designed to
62+
minimize the performance cost of querying database system catalogs—such as those in
63+
Exasol—by ensuring that schema details like columns, foreign keys, and constraints are
64+
only fetched once per inspector instance. When a method like ``get_foreign_keys()`` is
65+
called, the result is stored in an internal dictionary; subsequent requests for the same
66+
table metadata skip the network round-trip and return the cached data immediately.
67+
This is particularly advantageous for applications that perform heavy reflection
68+
operations.
69+
70+
For more details, see:
71+
72+
* `Reflecting Database Objects <https://docs.sqlalchemy.org/en/21/core/reflection.html>`__
73+
4274
Foreign Keys
4375
------------
4476

doc/user_guide/index.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ General Tips
5252

5353
Known Limitations
5454
-----------------
55+
.. note::
56+
* Running into an error with a specific SQLAlchemy example? Our Exasol dialect may
57+
already be handling that for you! Check out our :ref:`features` for a full breakdown
58+
of what is automated.
59+
* In some cases, we might be able to provide new features or solutions for these
60+
limitations. Please feel free to reach out by creating a
61+
`GitHub issue <https://github.com/exasol/sqlalchemy-exasol/issues>`__.
62+
* For technical difficulties, please submit a request
63+
via the `Exasol Support Portal <https://exasol.my.site.com/s/create-new-case>`__.
64+
5565
* Insert
5666

5767
- SQLAlchemy is slower than other drivers at performing multiple entry inserts.

examples/features/non_orm/_0_create_tables.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
from __future__ import annotations
2+
13
from sqlalchemy import (
24
Column,
35
ForeignKey,
46
Integer,
57
MetaData,
68
String,
79
Table,
10+
text,
811
)
912

1013
from examples.config import (
@@ -13,13 +16,15 @@
1316
SQL_ALCHEMY,
1417
)
1518

19+
# For more information on table definitions, check out:
20+
# https://docs.sqlalchemy.org/en/21/tutorial/metadata.html
21+
1622
# 1. Ensure that the schema exists
1723
SQL_ALCHEMY.create_schema(engine=ENGINE, schema=DEFAULT_SCHEMA_NAME)
1824

1925
# 2. Use the schema to define the metadata_obj, which is used in the `Base` class
2026
metadata_obj = MetaData(schema=DEFAULT_SCHEMA_NAME)
2127

22-
2328
metadata = MetaData()
2429

2530
# 3. Define tables
@@ -42,3 +47,17 @@
4247
# 4. Create all tables
4348
with ENGINE.begin() as conn:
4449
metadata.create_all(conn)
50+
51+
# 5. Verify that the tables have been created
52+
query = f"""
53+
SELECT TABLE_NAME
54+
FROM SYS.EXA_ALL_TABLES
55+
WHERE TABLE_SCHEMA = '{DEFAULT_SCHEMA_NAME}'
56+
ORDER BY TABLE_NAME
57+
"""
58+
59+
with ENGINE.connect() as con:
60+
results = con.execute(text(query)).fetchall()
61+
62+
if __name__ == "__main__":
63+
print(f"Tables in schema={DEFAULT_SCHEMA_NAME}: {results}")

examples/features/non_orm/_1_working_with_single_entry.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,22 +52,22 @@ def select_all_entries():
5252

5353
select_all_entries()
5454

55-
# 3. Update (Atomic execution)
55+
# 3. Update the entry
5656
with ENGINE.begin() as conn:
5757
# a. Update User
5858
conn.execute(
5959
update(user_table)
6060
.where(user_table.c.first_name == "Jax")
6161
.values(first_name="Paris")
6262
)
63-
# b. Update Email (using a subquery or join-like syntax depending on logic)
63+
# b. Update EmailAddress
6464
conn.execute(
6565
update(email_address_table)
6666
.where(email_address_table.c.email_address == "[email protected]")
6767
.values(email_address="[email protected]")
6868
)
6969

70-
# 4. Delete
70+
# 4. Delete the entry
7171
with ENGINE.begin() as conn:
7272
# a. Get the IDs of the users you want to delete
7373
user_ids_stmt = select(user_table.c.id).where(user_table.c.first_name == "Paris")
@@ -81,5 +81,5 @@ def select_all_entries():
8181
)
8282
)
8383

84-
# c. Now delete the user
84+
# c. Delete the user
8585
conn.execute(delete(user_table).where(user_table.c.id.in_(user_ids)))
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
from sqlalchemy import (
2+
and_,
3+
delete,
4+
insert,
5+
or_,
6+
select,
7+
update,
8+
)
9+
10+
from examples.config import ENGINE
11+
12+
# Import the Table objects (not classes) from your metadata
13+
from examples.features.non_orm._0_create_tables import (
14+
email_address_table,
15+
user_table,
16+
)
17+
18+
# 0. Clean tables
19+
with ENGINE.begin() as conn:
20+
conn.execute(delete(email_address_table))
21+
conn.execute(delete(user_table))
22+
23+
# 1. Insert multiple entries
24+
data = [
25+
{
26+
"first_name": "Lux",
27+
"last_name": "Noceda",
28+
"email_addresses": ["[email protected]", "[email protected]"],
29+
},
30+
{
31+
"first_name": "Eda",
32+
"last_name": "Clawthorne",
33+
"email_addresses": ["[email protected]"],
34+
},
35+
{
36+
"first_name": "Raine",
37+
"last_name": "Whispers",
38+
"email_addresses": ["[email protected]"],
39+
},
40+
{
41+
"first_name": "Amity",
42+
"last_name": "Blight",
43+
"email_addresses": ["[email protected]"],
44+
},
45+
{
46+
"first_name": "Willow",
47+
"last_name": "Park",
48+
"email_addresses": ["[email protected]"],
49+
},
50+
]
51+
52+
with ENGINE.begin() as conn:
53+
# a. Insert Users
54+
user_payload = [
55+
{"first_name": d["first_name"], "last_name": d["last_name"]} for d in data
56+
]
57+
conn.execute(insert(user_table), user_payload)
58+
59+
# b. Retrieve user ids
60+
stmt = select(user_table.c.id, user_table.c.first_name, user_table.c.last_name)
61+
user_map = {(row.first_name, row.last_name): row.id for row in conn.execute(stmt)}
62+
63+
# c. Insert EmailAddresses
64+
email_payload = []
65+
for entry in data:
66+
u_id = user_map.get((entry["first_name"], entry["last_name"]))
67+
for email in entry["email_addresses"]:
68+
email_payload.append({"user_id": u_id, "email_address": email})
69+
conn.execute(insert(email_address_table), email_payload)
70+
71+
72+
# 2. Display results
73+
def select_all_entries():
74+
with ENGINE.connect() as conn:
75+
# Join user_table and email_address_table explicitly
76+
stmt = select(
77+
user_table.c.id,
78+
user_table.c.first_name,
79+
user_table.c.last_name,
80+
email_address_table.c.email_address,
81+
).select_from(user_table.join(email_address_table, isouter=True))
82+
results = conn.execute(stmt).fetchall()
83+
84+
# Grouping manually since Core doesn't have ORM's unique().all() object deduplication
85+
grouped = {}
86+
for row in results:
87+
key = (row.id, row.first_name, row.last_name)
88+
if key not in grouped:
89+
grouped[key] = []
90+
if row.email_address:
91+
grouped[key].append(row.email_address)
92+
93+
for (u_id, f_name, l_name), emails in grouped.items():
94+
print(f"{u_id} {f_name} {l_name} {emails}")
95+
96+
97+
select_all_entries()
98+
99+
# 3. Update multiple entries
100+
with ENGINE.begin() as conn:
101+
# a. Update the last_name of Users
102+
new_last_name = "Clawthorne-Whispers"
103+
conn.execute(
104+
update(user_table)
105+
.where(user_table.c.last_name.in_(["Whispers", "Clawthorne"]))
106+
.values(last_name=new_last_name)
107+
)
108+
109+
# b. Update the EmailAddress
110+
eda_id_subq = (
111+
select(user_table.c.id)
112+
.where(user_table.c.first_name == "Eda")
113+
.scalar_subquery()
114+
)
115+
conn.execute(
116+
update(email_address_table)
117+
.where(email_address_table.c.user_id == eda_id_subq)
118+
.values(email_address="[email protected]")
119+
)
120+
121+
# 4. Delete multiple entries
122+
with ENGINE.begin() as conn:
123+
# a. Define the selection criteria for the users you want to target
124+
target_criteria = or_(
125+
and_(user_table.c.first_name == "Amity", user_table.c.last_name == "Blight"),
126+
and_(user_table.c.first_name == "Willow", user_table.c.last_name == "Park"),
127+
and_(user_table.c.first_name == "Lux", user_table.c.last_name == "Noceda"),
128+
)
129+
stmt = select(user_table.c.id).where(target_criteria)
130+
users_ids = conn.execute(stmt).scalars().all()
131+
132+
# b. Delete EmailAddresses associated with these Users, as they graduated
133+
for uid in users_ids:
134+
email_delete_stmt = delete(email_address_table).where(
135+
email_address_table.c.user_id == uid
136+
)
137+
conn.execute(email_delete_stmt)
138+
139+
select_all_entries()

examples/features/orm/_0_create_tables.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
SQL_ALCHEMY,
2121
)
2222

23-
# For more information on ORM table definition, check out:
23+
# For more information on ORM table definitions, check out:
2424
# https://docs.sqlalchemy.org/en/20/orm/quickstart.html#orm-quick-start
2525

2626
# 1. Ensure that the schema exists

0 commit comments

Comments
 (0)