Chapter 27 · Application Integration: Drivers, Pools, Session State, Transactions, and Resilience

Bind Variables, Array/Bulk Operations, Type Mapping, LOB Streaming, and Injection Defense

Use binds and batch APIs to reduce parse and network work, map Oracle NUMBER/time/JSON/VECTOR/LOB types consciously, stream large LOBs, and prove why identifiers require allowlisting instead of bind placeholders.

Advanced130–150 minutesBind/batch/injection/type/LOB labCurrent driver-specific APIs pinnedIdentifiers are not bind valuesLast reviewed: August 2026

Learning outcomes

ServiceHub fills the shared pool with thousands of SQL texts because IDs are interpolated into SQL. Another endpoint correctly binds values but tries to bind a table name. A bulk loader sends one INSERT per network round trip, while an attachment endpoint reads an unbounded CLOB into memory. Efficient integration separates SQL grammar from typed values and uses batch/streaming APIs deliberately.

01

Use bind variables/prepared statements for values and observe cursor reuse.

02

Use python-oracledb executemany and node-oracledb executeMany with explicit batch-error transaction semantics.

03

Map NUMBER, DATE/TIMESTAMP/TSTZ, JSON, VECTOR, and LOB values without silent precision/time-zone assumptions.

04

Stream LOBs instead of materializing arbitrary payloads in application memory.

05

Show that values can be bound but arbitrary identifiers require an allowlist plus identifier validation/quoting.

Generation-time baseline, drivers, licensing, services, and lab boundary

Mandatory database examples target Oracle AI Database Free 26ai, reviewed against RU 23.26.3, SQL Developer 26.2, SQLcl 26.2.1.222.1617, JDBC/UCP 23.26.3.0, ODP.NET 23.26.3, python-oracledb 4.0.2, node-oracledb 7.0.1, and Oracle Instant Client 23.26.3 where Thick mode is discussed. Free is limited to 2 foreground CPU cores, 2 GB database RAM, 12 GB user data, and one installation per logical environment, with no Release Update patching or Oracle Support SRs. The course baseline is CDB/instance FREE, application PDB/service FREEPDB1 on port 1521, owner SERVICEHUB_OWNER, and runtime user SERVICEHUB_APP. JDBC Thin is the preferred Java path; JDBC OCI/Type 2 is deprecated in 26ai. python-oracledb and node-oracledb default to Thin mode and use Oracle Client libraries only if Thick mode is explicitly initialized. Application Continuity and Transaction Guard are not licensed in Free. On EE/EE-ES, Application Continuity requires Active Data Guard, RAC One Node, or RAC. DRCP and RESET_STATE are separate mechanisms. TCPS examples require a configured TLS listener/server certificate and trusted client configuration. No real password, wallet, private key, or provider secret is embedded, no mandatory lab changes COMPATIBLE, and no remote GitHub file is modified.

1. Disposable typed table

sql · setup
BEGIN EXECUTE IMMEDIATE 'DROP TABLE sh27_bind_lab PURGE';EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;/CREATE TABLE sh27_bind_lab (  item_id NUMBER PRIMARY KEY,  tenant_code VARCHAR2(20) NOT NULL,  item_name VARCHAR2(100) NOT NULL,  amount NUMBER(12,2) NOT NULL,  created_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,  payload_json JSON,  attachment CLOB,  embedding VECTOR(4,FLOAT32));INSERT INTO sh27_bind_lab(  item_id,tenant_code,item_name,amount,payload_json,embedding) VALUES(  1,'TENANT_A','Compressor guide',25.50,  JSON('{"status":"ACTIVE"}'),  VECTOR('[0.9,0.8,0.1,0.0]',4,FLOAT32));INSERT INTO sh27_bind_lab(  item_id,tenant_code,item_name,amount,payload_json,embedding) VALUES(  2,'TENANT_B','Private electrical guide',75.00,  JSON('{"status":"ACTIVE"}'),  VECTOR('[0.1,0.1,0.9,0.2]',4,FLOAT32));COMMIT;

2. Deliberately unsafe string interpolation

python · do not construct predicates from user text
attacker_input = "TENANT_A' OR '1'='1"sql = (    "select item_id, tenant_code, item_name "    "from sh27_bind_lab "    "where tenant_code = '" + attacker_input + "'")# Resulting SQL changes the predicate and can expose all rows.

The attack changes SQL grammar. Least privilege limits damage, but the correct application construction is still parameter binding.

3. Bind the value

python · python-oracledb value bind
tenant_code = "TENANT_A' OR '1'='1"cur.execute(    """    select item_id, tenant_code, item_name    from sh27_bind_lab    where tenant_code = :tenant_code    """,    tenant_code=tenant_code)print(cur.fetchall())# Expected: no row for this literal tenant value.

The quote/OR tokens remain characters in a value. Binds are not authorization—VPD/grants still protect rows/objects.

4. Observe reusable SQL text

sql · cursor evidence
SELECT  sql_id, executions, parse_calls, version_count,  substr(sql_text,1,140) AS sql_textFROM v$sqlWHERE sql_text LIKE  'select item_id, tenant_code, item_name%sh27_bind_lab%'ORDER BY last_active_time DESC;

Repeated executions of one bind SQL text can reuse parsed cursors/statement caches instead of producing one SQL text per ID. Exact parse counts depend on session and statement-cache behavior—measure an interval rather than requiring a magic parse ratio.

5. Batch DML reduces network round trips

python · python-oracledb executemany with batch errors
rows = [    (10, "TENANT_A", "Pump guide", 10.00),    (11, "TENANT_A", "Valve guide", 20.00),    (10, "TENANT_A", "Duplicate PK", 30.00)]cur.executemany(    """    insert into sh27_bind_lab(      item_id,tenant_code,item_name,amount    ) values(:1,:2,:3,:4)    """,    rows,    batcherrors=True)for err in cur.getbatcherrors():    print(err.offset, err.message)conn.rollback()  # this lab chooses all-or-nothing

With batch errors, valid rows can be processed while bad rows are reported. The application must explicitly decide whether partial success is allowed; this lab rolls back the batch.

6. Node executeMany follows the same principle

javascript · node-oracledb executeMany
const sql = `  insert into sh27_bind_lab(    item_id, tenant_code, item_name, amount  ) values (:id, :tenant, :name, :amount)`;const binds = [  {id: 20, tenant: "TENANT_A", name: "A", amount: 1.25},  {id: 21, tenant: "TENANT_A", name: "B", amount: 2.50}];const result = await connection.executeMany(sql, binds, {  autoCommit: false,  bindDefs: {    id:     {type: oracledb.NUMBER},    tenant: {type: oracledb.STRING, maxSize: 20},    name:   {type: oracledb.STRING, maxSize: 100},    amount: {type: oracledb.NUMBER}  },  batchErrors: true,  dmlRowCounts: true});console.log(result.dmlRowCounts, result.batchErrors);await connection.rollback();

7. Type mapping is correctness

Oracle type Risk Rule
NUMBER Binary floating/JavaScript Number may lose exact large integer/decimal values Use Decimal/BigInt/string/type handlers when exact semantics require it.
DATE Has date+time but no timezone Never infer UTC merely from DATE.
TIMESTAMP No timezone Define application timezone conversion explicitly.
TIMESTAMP WITH TIME ZONE Offset/region representation can differ by runtime Test round-trip as an instant plus required zone semantics.
JSON Native JSON versus text/object conversion differs by driver/version Use current JSON APIs and test numbers/null/date conventions.
VECTOR Dimension/format/model mismatch Pin embedding provenance/dimension as in Chapter 25.
CLOB/BLOB Unbounded process-memory use Stream/chunk with driver LOB APIs.

8. LOB streaming keeps memory bounded

javascript · node-oracledb CLOB stream
const result = await connection.execute(  `select attachment     from sh27_bind_lab    where item_id = :id`,  { id: 1 });const lob = result.rows[0][0];lob.setEncoding("utf8");lob.on("data", chunk => {  // Process/write each chunk.});lob.on("end", () => console.log("done"));lob.on("error", console.error);// Do not release the connection before the LOB stream completes.

Python/JDBC/ODP.NET expose equivalent LOB reader/stream APIs. A locator/stream can depend on the originating session, so pool lifecycle is part of LOB correctness.

9. Bind placeholders cannot be identifiers

sql · wrong syntax
SELECT *FROM :table_name;-- Typical result:-- ORA-00903: invalid table name

Object and column names must be known at parse time; bind values arrive later. The repair is not arbitrary string concatenation.

10. Validate and allowlist identifiers

python · python-oracledb 4.0.2 helpers
requested = user_inputif requested not in {"SH27_BIND_LAB"}:    raise ValueError("object is not allowed")if not oracledb.is_simple_sql_name(requested):    raise ValueError("invalid identifier")safe_name = oracledb.enquote_name(requested)sql = f"select count(*) from {safe_name}"
javascript · node-oracledb 7.0.1 helpers
const allowed = new Set(["SH27_BIND_LAB"]);if (!allowed.has(userInput) || !oracledb.isSimpleSqlName(userInput)) {  throw new Error("object is not allowed");}const safeName = oracledb.enquoteName(userInput);const sql = `select count(*) from ${safeName}`;

Syntactic validation is not a business authorization rule; the allowlist decides what the application may access.

11. Server-side dynamic SQL follows the same rule

sql · allowlist plus DBMS_ASSERT
DECLARE  l_table VARCHAR2(128) := :requested_table;BEGIN  IF l_table NOT IN ('SH27_BIND_LAB') THEN    RAISE_APPLICATION_ERROR(-20001,'table not allowed');  END IF;  EXECUTE IMMEDIATE    'SELECT COUNT(*) FROM ' ||    DBMS_ASSERT.SIMPLE_SQL_NAME(l_table)  INTO :row_count;END;/

12. Cleanup and production judgment

sql · cleanup
DROP TABLE sh27_bind_lab PURGE;

Bind every user-controlled value, batch with explicit atomicity/partial-success rules, predefine types/sizes where useful, stream large LOBs, and treat dynamic identifiers as controlled code—not user data. Test edge NUMBER/timezone/JSON/VECTOR mappings across every supported language/runtime.

No pack, restart, or new COMPATIBLE change is required beyond earlier type prerequisites such as VECTOR. Batch size is not “larger is always faster”; measure redo/undo/network/memory. Lesson 4 applies the same explicitness to commit/retry behavior.

Check your understanding

  1. What does a value bind protect?
  2. Why can :table_name not identify a table?
  3. What does batchErrors change?
  4. Why can JavaScript Number be unsafe for Oracle NUMBER?
  5. What must remain valid while streaming a LOB?
Review the answers

It prevents that input value from changing SQL grammar and promotes reusable SQL shape.

Object names are resolved during parse, before bind values are supplied.

Individual row failures can be reported while other batch rows are processed; the caller must choose commit/rollback semantics.

It cannot exactly represent every Oracle integer/decimal value.

The originating connection/session until the locator/stream operation is complete.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.