7. Using Bind Variables
SQL and PL/SQL statements may contain bind parameters, which are
colon-prefixed identifiers or numerals. These indicate where separately
specified values are substituted in a statement when it is executed, or
where values are to be returned after execution. For example, :country_id
and :country_name
are the two bind variables in this SQL statement:
const oracledb = require('oracledb');
const result = await connection.execute(
`INSERT INTO countries VALUES (:country_id, :country_name)`,
{country_id: 90, country_name: "Tonga"}
);
IN binds are values passed into the database. OUT binds are used to retrieve data. IN OUT binds are passed in, and may return a different value after the statement executes.
Note
Using bind parameters is recommended in preference to constructing SQL or PL/SQL statements by string concatenation or template literals. This is for performance and security.
Inserted data that is bound is passed to the database separately from the statement text. It can never be executed directly. This means there is no need to escape bound data inserted into the database.
If a statement is executed more than once with different values for the bind parameters, then Oracle can re-use context from the initial execution, generally improving performance. However, if similar statements contain hard coded values instead of bind parameters, Oracle sees that the statement text is different and will be less efficient.
Bind parameters can be used to substitute data values. They cannot be used for direct substitution of column or table names in dynamically constructed statements, see Binding Column and Table Names in Queries.
Bind variables cannot be used in DDL
statements, for example CREATE TABLE
or ALTER
commands.
Sets of values can bound for use in connection.executeMany()
,
see Batch Statement Execution and Bulk Loading.
7.1. IN Bind Parameters
For IN binds, a data value is passed into the database and substituted into the statement during execution of SQL or PL/SQL.
7.1.1. Bind by Name
To bind data values, the bindParams
argument of execute()
should contain bind variable objects with
dir, val,
type properties. Each bind variable
object name must match the statement’s bind parameter name:
const oracledb = require('oracledb');
const result = await connection.execute(
`INSERT INTO countries VALUES (:country_id, :country_name)`,
{
country_id: { dir: oracledb.BIND_IN, val: 90, type: oracledb.NUMBER },
country_name: { dir: oracledb.BIND_IN, val: "Tonga", type: oracledb.STRING }
}
);
console.log("Rows inserted " + result.rowsAffected);
For IN binds:
The direction
dir
isoracledb.BIND_IN
, which is the default whendir
is not specified.The
val
attribute may be a constant or a JavaScript variable.If
type
is omitted, it is derived from the bind data value. If it is set, it can be one of the values in the type table. Typicallytype
is one oforacledb.STRING
,oracledb.NUMBER
,oracledb.DATE
ororacledb.BUFFER
matching the standard Node.js type of the data being passed into the database. Use a bind type oforacledb.BLOB
ororacledb.CLOB
to pass in Lob instances. For binding Oracle Database objects, it can also be the name of an Oracle Database object or collection, or a DbObject Class type.
Since dir
and type
have defaults, these attributes are sometimes
omitted for IN binds. Binds can be like:
const result = await connection.execute(
`INSERT INTO countries VALUES (:country_id, :country_name)`,
{country_id: 90, country_name: "Tonga"}
);
console.log("Rows inserted " + result.rowsAffected);
When a bind parameter name is used more than once in the SQL statement, it should only occur once in the bind object:
const result = await connection.execute(
`SELECT first_name, last_name FROM employees WHERE first_name = :nmbv OR last_name = :nmbv`,
{nmbv: 'Christopher'}
);
7.1.2. Bind by Position
Instead of using named bind parameters, the data can alternatively be in
an array. In this example, values are bound to the SQL bind parameters
:country_id
and :country_name
:
const result = await connection.execute(
`INSERT INTO countries VALUES (:country_id, :country_name)`,
[90, "Tonga"]
);
The position of the array values corresponds to the position of the SQL
bind parameters as they occur in the statement, regardless of their
names. This is still true even if the bind parameters are named like
:0
, :1
, etc. The following snippet will fail because the country
name needs to be the second entry of the array so it becomes the second
value in the INSERT
statement
const result = await connection.execute(
`INSERT INTO countries (country_id, country_name) VALUES (:1, :0)`,
["Tonga", 90] // fail
);
In the context of SQL statements, the input array position ‘n’ indicates the bind parameter at the n’th position in the statement. However, in the context of PL/SQL statements the position ‘n’ in the bind call indicates a binding for the n’th unique parameter name in the statement when scanned left to right.
If a bind parameter name is repeated in the SQL string, then bind by name syntax should be used.
7.1.3. Bind Data Type Notes
When binding a JavaScript Date value in an INSERT
statement, by
default the bind type
is equivalent to TIMESTAMP. In the database,
TIMESTAMP WITH LOCAL TIME ZONE dates are normalized to the database time
zone, or to the time zone specified for TIMESTAMP WITH TIME ZONE columns.
If later queried, they are returned in the session time zone. See
Fetching Date and Timestamps for more information.
7.2. OUT and IN OUT Bind Parameters
OUT binds are used to retrieve data from the database. IN OUT binds are passed into the database, and may return a different value after the statement executes. IN OUT binds can be used for PL/SQL calls, but not for SQL.
For each OUT and IN OUT bind parameter in bindParams, a bind variable object containing dir, val, type, and maxSize properties is used:
The
dir
attribute should beoracledb.BIND_OUT
ororacledb.BIND_INOUT
, depending on whether data is only to be returned from the database or additionally passed into the database.The
val
parameter in needed when binding IN OUT to pass a value into the database. It is not used for OUT binds.The
type
attribute can be one of the constants as discussed in the type table. This determines the mapping between the database type and the JavaScript type.The attribute should be set for OUT binds. If
type
is not specified, thenoracledb.STRING
is assumed.For IN OUT binds,
type
can inferred from the input data value type. However is recommended to explicitly settype
, because the correct value cannot be determined if the input data is null. The output data type will always be the same as the input data type.A
maxSize
attribute should be set for String and Buffer OUT or IN OUT binds. This is the maximum number of bytes the bind parameter will return. If the output value does not fit inmaxSize
bytes, then an error such ORA-06502: PL/SQL: numeric or value error: character string buffer too small or NJS-016: buffer is too small for OUT binds occurs.A default value of 200 bytes is used when
maxSize
is not provided for OUT binds that are returned in Strings or Buffers.A string representing a UROWID may be up to 5267 bytes long in node-oracledb.
For PL/SQL Associative Array binds a maxArraySize property is also required.
Note that before a PL/SQL block returns, all OUT binds should be explicitly set to a value. This includes bind variables that will be ignored. Set simple variables to NULL. Set REF CURSORS to an empty result set. See this GitHub Issue.
7.2.1. Accessing OUT Bind Values
The results parameter of the execute()
callback contains an outBinds property with the
returned OUT and IN OUT bind values.
Given the creation of the PL/SQL procedure TESTPROC
:
CREATE OR REPLACE PROCEDURE testproc (
p_in IN VARCHAR2, p_inout IN OUT VARCHAR2, p_out OUT NUMBER)
AS
BEGIN
p_inout := p_in || p_inout;
p_out := 101;
END;
/
show errors
The procedure TESTPROC
can be called with:
const bindVars = {
i: 'Chris', // default direction is BIND_IN. Data type is inferred from the data
io: { val: 'Jones', dir: oracledb.BIND_INOUT },
o: { dir: oracledb.BIND_OUT, type: oracledb.NUMBER },
};
const result = await connection.execute(
`BEGIN testproc(:i, :io, :o); END;`,
bindVars
);
console.log(result.outBinds);
Since bindParams
is passed as an object, the outBinds
property
is also an object. The Node.js output is:
{ io: 'ChrisJones', o: 101 }
PL/SQL allows named parameters in procedure and function calls. This can
be used in execute()
like:
`BEGIN testproc(p_in => :i, p_inout => :io, p_out => :o); END;`,
An alternative to node-oracledb’s ‘bind by name’ syntax is ‘bind by array’ syntax:
const bindVars = [
'Chris',
{ val: 'Jones', dir: oracledb.BIND_INOUT },
{ type: oracledb.NUMBER, dir: oracledb.BIND_OUT }
];
When bindParams is passed as an array, then
outBinds
is returned as an array, with the same order as the OUT
binds in the statement:
[ 'ChrisJones', 101 ]
Mixing positional and named syntax is not supported. The following will throw an error:
const bindVars = [
'Chris', // valid
{ val: 'Jones', dir: oracledb.BIND_INOUT }, // valid
{ o: { type: oracledb.NUMBER, dir: oracledb.BIND_OUT } } // invalid
];
7.3. DML RETURNING Bind Parameters
“DML RETURNING” (also known as “RETURNING INTO”) statements such as
INSERT INTO tab VALUES (:1) RETURNING ROWID INTO :2
are a way
information can be returned about row changes from
DML
statements. For example you can use DML RETURNING to get the ROWIDs of
newly inserted rows. Another common use case is to return auto
incremented column values.
For statements that affect single rows, you may prefer to use lastRowid.
Bind parameters for DML RETURNING statements can use oracledb.BLOB
,
oracledb.CLOB
, oracledb.STRING
, oracledb.NUMBER
or
oracledb.DATE
for the BIND_OUT type.
To bind named Oracle objects use the class name or
DbObject prototype class for the bind type, as
shown for object binds in Fetching Oracle Database Objects and
Collections.
Oracle Database DATE, TIMESTAMP, TIMESTAMP WITH LOCAL TIME ZONE and
TIMESTAMP WITH TIME ZONE types can be bound as oracledb.DATE
for DML
RETURNING. These types can also be bound as oracledb.STRING
, if
desired. ROWID and UROWID data to be returned can be bound as
oracledb.STRING
. Note that a string representing a UROWID may be up
to 5267 bytes long.
For string and buffer types, an error occurs if maxSize is not large enough to hold a returned value.
Note each DML RETURNING bind OUT parameter is returned as an array
containing zero or more elements. Application code that is designed to
expect only one value could be made more robust if it confirms the
returned array length is not greater than one. This will help identify
invalid data or an incorrect WHERE
clause that causes more results
to be returned.
An example of DML RETURNING binds is:
const result = await connection.execute(
`UPDATE mytab SET name = :name
WHERE id = :id
RETURNING id, ROWID INTO :ids, :rids`,
{
id: 1001,
name: "Krishna",
ids: { type: oracledb.NUMBER, dir: oracledb.BIND_OUT },
rids: { type: oracledb.STRING, dir: oracledb.BIND_OUT }
}
);
console.log(result.outBinds);
If the WHERE
clause matches one record, the output would be like:
{ ids: [ 1001 ], rids: [ 'AAAbvZAAMAAABtNAAA' ] }
When a couple of rows match, the output could be:
{ ids: [ 1001, 1002 ],
rids: [ 'AAAbvZAAMAAABtNAAA', 'AAAbvZAAMAAABtNAAB' ] }
If the WHERE
clause matches no rows, the output would be:
{ ids: [], rids: [] }
The same bind variable placeholder name cannot be used both before and after the RETURNING clause. Consider the example below.
// a variable cannot be used for both input and output in a DML returning
// statement
const result = await connection.execute(
`UPDATE mytab SET name = :name || ' EXTRA TEXT'
WHERE id = :id
RETURNING name INTO :name`,
{
id: 1001,
name: { type: oracledb.STRING, val: "Krishna", dir: oracledb.BIND_INOUT, maxSize: 100 }
}
);
console.log(result.outBinds.name);
Here, the :name
bind variable is used both before and after the RETURNING
clause. In node-oracledb Thick mode, the bind variable will not be updated as
expected with the ‘ EXTRA TEXT’ value and no error is thrown. The Thick mode
prints the following output:
Krishna
With node-oracledb Thin mode, the above example returns the following error:
NJS-149: the bind variable placeholder "NAME" cannot be used both before
and after the RETURNING clause in a DML RETURNING statement
7.4. REF CURSOR Bind Parameters
Oracle REF CURSORS can be bound in node-oracledb by using the type
oracledb.CURSOR
in PL/SQL calls. For an OUT bind, the resulting bind
variable becomes a ResultSet, allowing rows to be
fetched using getRow()
or getRows()
.
The ResultSet can also be converted to a
Readable Stream by using toQueryStream()
.
Oracle Implicit Results are an alternative way to
return query results from PL/SQL.
If using getRow()
or getRows()
the ResultSet must be freed using
close()
when all rows have been fetched, or when the
application does not want to continue getting more rows. If the REF
CURSOR is set to NULL or is not set in the PL/SQL procedure, then the
returned ResultSet is invalid and methods like getRows()
will return
an error when invoked.
Given a PL/SQL procedure defined as:
CREATE OR REPLACE PROCEDURE get_emp_rs (
p_sal IN NUMBER,
p_recordset OUT SYS_REFCURSOR) AS
BEGIN
OPEN p_recordset FOR
SELECT first_name, salary, hire_date
FROM employees
WHERE salary > p_sal;
END;
/
This PL/SQL procedure can be called in node-oracledb using:
const result = await connection.execute(
`"BEGIN get_emp_rs(:sal, :cursor); END;`,
{
sal: 6000,
cursor: { type: oracledb.CURSOR, dir: oracledb.BIND_OUT }
},
{
prefetchRows: 1000, // tune the internal getRow() data fetch performance
fetchArraySize: 1000
}
);
const resultSet = result.outBinds.cursor;
let row;
while ((row = await resultSet.getRow())) {
console.log(row);
}
await resultSet.close(); // always close the ResultSet
All rows can be fetched in one operation by calling getRows()
with
no argument. This is useful when the query is known to return a “small”
number of rows:
const result = await connection.execute(
`"BEGIN get_emp_rs(:sal, :cursor); END;`,
{
sal: 6000,
cursor: { type: oracledb.CURSOR, dir: oracledb.BIND_OUT }
},
{
prefetchRows: 200, // tune the getRows() call
fetchArraySize: 200
}
);
const resultSet = result.outBinds.cursor;
const rows = await resultSet.getRows();
console.log(rows);
await resultSet.close(); // always close the ResultSet
The prefetchRows
and fetchArraySize
can
be used to tune the getRows()
call. The values must be set before, or when,
the ResultSet is obtained.
See refcursor.js for a complete example.
To convert the REF CURSOR ResultSet to a stream, use
toQueryStream()
:
const result = await connection.execute(
`"BEGIN get_emp_rs(:sal, :cursor); END;`,
{
sal: 6000,
cursor: { type: oracledb.CURSOR, dir: oracledb.BIND_OUT }
}
);
const cursor = result.outBinds.cursor;
const queryStream = cursor.toQueryStream();
const consumeStream = new Promise((resolve, reject) => {
queryStream.on('data', function(row) {
console.log(row);
});
queryStream.on('error', reject);
queryStream.on('close', resolve);
});
await consumeStream;
The connection must remain open until the stream is completely read.
Query results must be fetched to completion to avoid resource leaks. The
ResultSet close()
call for streaming query results will be executed
internally when all data has been fetched.
If you want to pass a queried ResultSet into PL/SQL using direction
oracledb.BIND_IN
, then set prefetchRows to
0 for the query returning the ResultSet. This stops the first rows being
silently fetched by node-oracledb and not being available in the later
receiving PL/SQL code. For example:
const result = await connection.execute(
`SELECT * FROM locations`,
[],
{
resultSet: true,
prefetchRows: 0 // stop node-oracledb internally fetching rows from the ResultSet
}
);
// Pass the ResultSet as a REF CURSOR into PL/SQL
await conn.execute(
`BEGIN myproc(:rc); END;`,
{
rc: { val: result.resultSet, type: oracledb.CURSOR, dir: oracledb.BIND_IN }
}
);
Because the default bind direction is BIND_IN
, and the type can be
inferred from result.resultSet
, the PL/SQL procedure call can be
simplified to:
await conn.execute(`BEGIN myproc(:rc); END;`, [result.resultSet]);
7.5. LOB Bind Parameters
Database CLOBs can be bound with type
set to
oracledb.CLOB. Database BLOBs can be bound
as oracledb.BLOB. These binds accept, or
return, node-oracledb Lob instances, which implement the
Node.js Stream interface.
Lobs may represent Oracle Database persistent LOBs (those stored in
tables) or temporary LOBs (such as those created with
connection.createLob()
or returned by some SQL and
PL/SQL operations).
LOBs can be bound with direction oracledb.BIND_IN
,
oracledb.BIND_OUT
or oracledb.BIND_INOUT
, depending on context.
Note that any PL/SQL OUT LOB parameter should be initialized in the PL/SQL block - even just to NULL - before the PL/SQL code completes. Make sure to do this in all PL/SQL code paths including in error handlers. This prevents node-oracledb throwing the error DPI-007: invalid OCI handle or descriptor.
In many cases it will be easier to work with JavaScript Strings and
Buffers instead of Lobs. These types can be bound
directly for SQL IN binds to insert into, or update, LOB columns. They
can also be bound to PL/SQL LOB parameters. Set the bind type
to
oracledb.STRING for CLOBs,
oracledb.DB_TYPE_NVARCHAR for NCLOBs,
and oracledb.BUFFER for BLOBs. The
default size used for these binds in the OUT direction is 200, so set
maxSize
appropriately.
See Working with CLOB, NCLOB and BLOB Data for examples and more information on binding and working with LOBs.
7.5.1. Size Limits for Binding LOBs to Strings and Buffers
When CLOBs are bound as oracledb.STRING
, BCLOBs bound as
oracledb.DB_TYPE_NVARCHAR
, or BLOBs are bound as
oracledb.BUFFER
, then their size is limited to 1GB. Commonly the
practical limitation is the memory available to Node.js and the V8
engine. For data larger than several megabytes, it is recommended to
bind as oracledb.CLOB
or oracledb.BLOB
and use Lob
streaming. If you try to create large Strings or
Buffers in Node.js you will see errors like JavaScript heap out of
memory, or other space related messages.
Internally, temporary LOBs are used when binding Strings and Buffers larger than 32 KB for PL/SQL calls. Freeing of the temporary LOB is handled automatically. For SQL calls no temporary LOBs are used.
7.6. Binding Multiple Values to a SQL WHERE IN
Clause
Binding a single JavaScript value into a SQL WHERE IN
clause is
easy:
sql = `SELECT first_name, last_name FROM employees WHERE first_name IN (:bv)`;
binds = ['Christopher'];
await connection.execute(sql, binds, function(...));
But a common use case for a SQL WHERE IN
clause is for multiple
values, for example when a web user selects multiple check-box options
and the query should match all chosen values.
To use a fixed, small number of values in an WHERE IN
bind clause,
the SQL query should have individual bind parameters, for example:
const sql = `SELECT first_name, last_name FROM employees WHERE first_name IN (:bv1, :bv2, :bv3, :bv4)`;
const binds = ['Alyssa', 'Christopher', 'Hazel', 'Samuel'];
const result = await connection.execute(sql, binds);
If you sometimes execute the query with a smaller number of items, then null can be bound for each ‘missing’ value:
const binds = ['Alyssa', 'Christopher', 'Hazel', null];
When the exact same statement text is re-executed many times regardless of the number of user supplied values, this provides performance and scaling benefits from not having multiple, unique SQL statements being run.
If the statement is not going to be re-executed, or the number of values is only going to be known at runtime, then a SQL statement can be built up:
const binds = ['Christopher', 'Hazel', 'Samuel'];
let sql = `SELECT first_name, last_name FROM employees WHERE first_name IN (`;
for (const i = 0; i < binds.length; i++)
sql += (i > 0) ? ", :" + i : ":" + i;
sql += ")";
This will construct a SQL statement:
SELECT first_name, last_name FROM employees WHERE first_name IN (:0, :1, :2)
You could use a tagged literal template to do this conveniently. Binds are still used for security. But, depending how often this query is executed, and how changeable the number of bind values is, you can end up with lots of ‘unique’ query strings being executed. You might not get the statement caching benefits that re-executing a fixed SQL statement would have.
A general solution for a larger number of values is to construct a SQL statement like:
SELECT ... WHERE col IN ( <something that returns a list of values> )
The best way to do the <something that returns a list of values>
will depend on how the data is initially represented and the number of
items. You might look at using CONNECT BY or at using a global temporary
table.
One method is to use an Oracle collection with the TABLE()
clause.
For example, if the following type was created:
SQL> CREATE OR REPLACE TYPE name_array AS TABLE OF VARCHAR2(20);
2 /
then the application could do:
const sql = `SELECT first_name, last_name
FROM employees
WHERE first_name IN (SELECT * FROM TABLE(:bv))`;
const inlist = ['Christopher', 'Hazel', 'Samuel'];
const binds = { bv: { type: "NAME_ARRAY", val: inlist } };
const result = await connection.execute(sql, binds, options);
You may decide to overload the use of the database
SYS.ODCIVARCHAR2LIST
or SYS.ODCINUMBERLIST
types so you don’t
need to create a type like name_array
:
const binds = { bv: { type: 'SYS.ODCIVARCHAR2LIST', val: inlist } };
Since this TABLE()
solution uses an object type, there is a
performance impact because of the extra round-trips
required to get the type information. Unless you have a large number of
binds you may prefer one of the previous solutions.
Some general references are On Cursors, SQL, and Analytics and in this StackOverflow answer.
7.7. Binding in a LIKE
or REGEXP_LIKE
Clause
To do pattern matching with a LIKE
clause, bind a string containing
the pattern match wildcards, for example:
const pattern = "%uth%";
result = await connection.execute(
`SELECT CITY FROM LOCATIONS WHERE CITY LIKE :bv`,
{ bv: pattern }
);
console.log(result.rows[0]);
Output is like:
[ [ 'South Brunswick' ], [ 'South San Francisco' ], [ 'Southlake' ] ]
The same is true for regular expression functions such as
REGEXP_LIKE
and REGEXP_SUBSTR
. For example:
const pattern = ',[^,]+,';
result = await connection.execute(
`SELECT REGEXP_SUBSTR('500 Oracle Parkway, Redwood Shores, CA', :bv) FROM DUAL`,
{ bv: pattern }
);
console.log(result.rows);
Output is like:
[ [ ', Redwood Shores,' ] ]
7.8. Binding Column and Table Names in Queries
It is not possible to bind table names in queries. Instead use a hard-coded Allow List of names to build the final SQL statement, for example:
const validTables = ['LOCATIONS', 'DEPARTMENTS'];
const tableName = getTableNameFromEndUser();
if (!validTables.includes(tableName)) {
throw new Error('Invalid table name');
}
const query = `SELECT * FROM ` + tableName;
The same technique can be used to construct the list of selected column names. Make sure to use an Allow List of names to avoid SQL Injection security risks.
Each final SQL statement will obviously be distinct, and will use a slot in the statement cache by default.
It is possible to bind column names used in an ORDER BY:
const sql = `SELECT first_name, last_name
FROM employees
ORDER BY
CASE :ob
WHEN 'FIRST_NAME' THEN first_name
ELSE last_name
END`;
const columnName = getColumnNameFromEndUser(); // your function
const binds = [columnName];
const result = await connection.execute(sql, binds);
In this example, when columnName
is ‘FIRST_NAME’ then the result set
will be ordered by first name, otherwise the order will be by last name.
You should analyze the statement usage patterns and optimizer query plan before deciding whether to using binds like this, or to use multiple hard-coded SQL statements, each with a different ORDER BY.