mirror of
https://github.com/oracle/skills.git
synced 2026-09-19 03:46:55 +08:00
9.1 KiB
9.1 KiB
Python + Oracle Database
Overview
python-oracledb is the official Oracle Python driver (successor to cx_Oracle). It supports two modes:
- Thin mode (default): pure Python, no Oracle Client libraries required. Supports most features.
- Thick mode: requires Oracle Client (Instant Client or full client). Required for advanced features like Advanced Queuing, Sharding, and some proxy authentication scenarios.
pip install oracledb
Connecting
Basic Connection (Thin Mode)
import oracledb
# Easy Connect string
conn = oracledb.connect(
user="hr",
password="password",
dsn="localhost:1521/freepdb1"
)
# TNS alias (requires tnsnames.ora in TNS_ADMIN path)
conn = oracledb.connect(
user="hr",
password="password",
dsn="mydb_high"
)
# Close explicitly or use context manager
with oracledb.connect(user="hr", password="password", dsn="localhost:1521/freepdb1") as conn:
with conn.cursor() as cur:
cur.execute("SELECT sysdate FROM dual")
print(cur.fetchone())
Wallet / mTLS (Autonomous Database)
import oracledb
conn = oracledb.connect(
user="admin",
password="password",
dsn="myatp_high", # TNS alias from tnsnames.ora in wallet
config_dir="/path/to/wallet",
wallet_location="/path/to/wallet",
wallet_password="wallet_password" # if the wallet is password-protected
)
Enabling Thick Mode
import oracledb
# Call before any connection — sets the mode for the entire process
oracledb.init_oracle_client(lib_dir="/opt/oracle/instantclient_21_9")
conn = oracledb.connect(user="hr", password="password", dsn="localhost:1521/freepdb1")
Executing SQL
Bind Variables
Always use bind variables — never format user input into SQL strings.
with conn.cursor() as cur:
# Named binds (recommended)
cur.execute(
"SELECT last_name, salary FROM employees WHERE department_id = :dept_id AND salary > :min_sal",
dept_id=60,
min_sal=5000
)
# Or pass as dict
cur.execute(
"SELECT last_name FROM employees WHERE employee_id = :id",
{"id": 100}
)
rows = cur.fetchall()
for row in rows:
print(row)
DML with Binds
with conn.cursor() as cur:
cur.execute(
"UPDATE employees SET salary = :sal WHERE employee_id = :id",
sal=9000,
id=100
)
conn.commit()
Batch Execution (executemany)
data = [
{"id": 201, "name": "Alice", "dept": 10},
{"id": 202, "name": "Bob", "dept": 20},
{"id": 203, "name": "Carol", "dept": 10},
]
with conn.cursor() as cur:
cur.executemany(
"INSERT INTO employees (employee_id, last_name, department_id) VALUES (:id, :name, :dept)",
data
)
conn.commit()
Fetching Results
with conn.cursor() as cur:
cur.execute("SELECT employee_id, last_name, salary FROM employees WHERE rownum <= 100")
# fetchone — single row
row = cur.fetchone()
# fetchmany — batch
rows = cur.fetchmany(numRows=25)
# fetchall — all remaining (careful with large result sets)
rows = cur.fetchall()
# Iterate directly (memory-efficient for large sets)
cur.execute("SELECT * FROM employees")
for row in cur:
print(row)
Column Names
with conn.cursor() as cur:
cur.execute("SELECT employee_id, last_name, salary FROM employees WHERE rownum <= 5")
columns = [col[0] for col in cur.description]
rows = cur.fetchall()
for row in rows:
print(dict(zip(columns, row)))
Fetch as Dictionaries (rowfactory)
def make_dict_factory(cursor):
cols = [col[0].lower() for col in cursor.description]
def create_row(*args):
return dict(zip(cols, args))
return create_row
with conn.cursor() as cur:
cur.execute("SELECT employee_id, last_name FROM employees WHERE rownum <= 5")
cur.rowfactory = make_dict_factory(cur)
for row in cur:
print(row) # {'employee_id': 100, 'last_name': 'King'}
Connection Pooling
Use a connection pool for web applications and multi-threaded code. Never share a single connection across threads.
import oracledb
# Create pool at application startup
pool = oracledb.create_pool(
user="hr",
password="password",
dsn="localhost:1521/freepdb1",
min=2, # minimum open connections
max=10, # maximum connections
increment=1 # connections opened when more are needed
)
# Acquire a connection from the pool
with pool.acquire() as conn:
with conn.cursor() as cur:
cur.execute("SELECT COUNT(*) FROM employees")
print(cur.fetchone())
# Pool is returned automatically; close pool at app shutdown
pool.close()
PL/SQL Calls
import oracledb
with conn.cursor() as cur:
# Call a stored procedure
cur.callproc("hr.update_salary", [100, 9500])
conn.commit()
# Call a function
result = cur.callfunc("hr.get_employee_count", oracledb.DB_TYPE_NUMBER, [10])
print(result)
# Anonymous PL/SQL block with OUT parameters
out_val = cur.var(oracledb.DB_TYPE_VARCHAR)
cur.execute(
"""
BEGIN
:out := 'Hello from PL/SQL';
END;
""",
out=out_val
)
print(out_val.getvalue())
REF CURSOR
with conn.cursor() as cur:
ref_cursor = cur.var(oracledb.DB_TYPE_CURSOR)
cur.execute(
"""
BEGIN
OPEN :rc FOR SELECT employee_id, last_name FROM employees WHERE department_id = :dept;
END;
""",
rc=ref_cursor,
dept=60
)
for row in ref_cursor.getvalue():
print(row)
LOB Handling
import oracledb
# Read a CLOB
with conn.cursor() as cur:
cur.execute("SELECT resume FROM employee_docs WHERE employee_id = 100")
row = cur.fetchone()
if row:
clob = row[0]
text = clob.read() # reads entire CLOB as string
print(text[:200])
# Write a CLOB
with conn.cursor() as cur:
large_text = "..." * 10000
cur.execute(
"UPDATE employee_docs SET resume = :clob WHERE employee_id = :id",
clob=large_text,
id=100
)
conn.commit()
# Read a BLOB
with conn.cursor() as cur:
cur.execute("SELECT photo FROM employee_photos WHERE employee_id = 100")
row = cur.fetchone()
if row:
blob_data = row[0].read()
with open("photo.jpg", "wb") as f:
f.write(blob_data)
Async Support (python-oracledb 2.x)
import asyncio
import oracledb
async def main():
conn = await oracledb.connect_async(
user="hr",
password="password",
dsn="localhost:1521/freepdb1"
)
async with conn.cursor() as cur:
await cur.execute("SELECT COUNT(*) FROM employees")
row = await cur.fetchone()
print(row)
await conn.close()
asyncio.run(main())
# Async pool
async def pooled():
pool = oracledb.create_pool_async(
user="hr", password="password", dsn="localhost:1521/freepdb1",
min=2, max=10, increment=1
)
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("SELECT 1 FROM dual")
print(await cur.fetchone())
await pool.close()
asyncio.run(pooled())
Best Practices
- Always use bind variables — never concatenate user input into SQL.
- Use a connection pool in web/multi-threaded apps; do not share connections across threads.
- Use context managers (
with) for connections and cursors to ensure cleanup. - Use
executemanyfor bulk inserts/updates instead of loopingexecute. - Set
arraysizeon the cursor for large fetches:cur.arraysize = 1000reduces round-trips. - Commit explicitly —
python-oracledbdoes not auto-commit. - Prefer thin mode unless you specifically need thick-mode-only features.
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
f"SELECT ... WHERE id = {user_input}" |
SQL injection | Use bind variables |
| Sharing one connection across threads | Corruption, errors | Use a pool; one connection per thread |
Not calling conn.commit() |
DML silently rolled back on disconnect | Always commit or use autocommit=True for simple scripts |
fetchall() on a million-row query |
OOM | Use fetchmany() or iterate the cursor |
Using cx_Oracle in new projects |
Deprecated | Migrate to python-oracledb |
Not setting arraysize for large fetches |
Excessive round-trips | cur.arraysize = 1000 before fetch |
Oracle Version Notes (19c vs 26ai)
- Thin mode is supported for Oracle Database 12.1 and later.
python-oracledb2.x adds async support and JSON improvements for 21c+.- Oracle 23ai JSON Relational Duality Views are accessible via normal SELECT queries.