12. Using JSON Data
Oracle Database 12.1.0.2 introduced native support for JSON data. You can use JSON with relational database features, including transactions, indexing, declarative querying, and views. You can project JSON data relationally, making it available for relational processes and tools. Also see node-oracledb’s SODA API, which allows access to JSON documents through a set of NoSQL-style APIs.
For more information about using JSON in Oracle Database see the Database JSON Developer’s Guide.
12.1. Using the Oracle Database 21c JSON Type in node-oracledb
Oracle Database 21c introduced a dedicated JSON data type with a new binary storage format that improves performance and functionality. To take advantage of the new dedicated JSON type in Oracle Database 21c and later versions, use node-oracledb 5.1 or later. For Thick mode, you must additionally use Oracle Client 21c (or later).
In Oracle Database 21c or later, to create a table with a column called
PO_DOCUMENT
for JSON data:
CREATE TABLE j_purchaseorder (po_document JSON);
Inserting JSON Data
To insert JavaScript objects directly by binding as oracledb.DB_TYPE_JSON
:
const data = { "userId": 1, "userName": "Chris", "location": "Australia" };
await connection.execute(
`INSERT INTO j_purchaseorder (po_document) VALUES (:bv)`,
{ bv: {val: data, type: oracledb.DB_TYPE_JSON} }
);
Fetching JSON Data
To query a JSON column, use:
const r = await conn.execute(`SELECT po_document FROM j_purchaseorder`);
console.dir(r.rows, { depth: null });
The output is:
[
{
PO_DOCUMENT: '{"userId":1,"userName":"Chris","location":"Australia"}'
}
]
12.1.1. Using Oracle Client Libraries 19c or Earlier
If node-oracledb Thick mode uses Oracle Client Libraries 19c (or earlier),
querying an Oracle Database 21c (or later), then JSON column returns a
Lob Class BLOB. You can stream the Lob or use
lob.getData()
:
const result = await connection.execute(`SELECT po_document FROM j_purchaseorder`);,
const lob = result.rows[0][0]; // just show first row
const d = await lob.getData();
const j = JSON.parse(d);
console.dir(j, { depth: null });
The output is:
{ userId: 1, userName: 'Chris', location: 'Australia' }
Note oracledb.fetchAsBuffer
will not automatically convert the
Oracle Database 21c JSON type to a Buffer. Using it will give
ORA-40569: Unimplemented JSON feature. Use await lob.getData()
as
shown above.
12.2. Using the Oracle Database 12c JSON Type in node-oracledb
In Oracle Database versions 12c or later (prior to Oracle Database 21c), JSON in relational tables is stored as BLOB, CLOB, or VARCHAR2 data. All of these types can be used with node-oracledb in Thin or Thick mode.
The older syntax to create a table with a JSON column is like:
CREATE TABLE j_purchaseorder (po_document BLOB CHECK (po_document IS JSON));
The check constraint with the clause IS JSON
ensures only JSON data
is stored in that column.
The older syntax can still be used in Oracle Database 21c. However the recommendation is to move to the new JSON type. With the old syntax, the storage can be BLOB, CLOB, or VARCHAR2. Of these, BLOB is preferred to avoid character set conversion overheads.
Inserting JSON Data
When using Oracle Database 12c or later with JSON using BLOB storage, you can insert JSON strings:
const data = { "userId": 1, "userName": "Chris", "location": "Australia" };
const s = JSON.stringify(data); // change JavaScript value to a JSON string
const b = Buffer.from(s, 'utf8');
const result = await connection.execute(
`INSERT INTO j_purchaseorder (po_document) VALUES (:bv)`,
[b] // bind the JSON string
);
Fetching JSON Data
With Oracle Database 12c (or later), you can fetch VARCHAR2 and LOB columns
that contain JSON data in the same way that
JSON type columns are fetched when using Oracle
Database 21c (or later). This can be done by setting
oracledb.future.oldJsonColumnAsObj
to the value true as shown below.
If you are using node-oracledb Thick mode, you must use Oracle Client 19c
(or later) for this setting to work. For example:
oracledb.future.oldJsonColumnAsObj = true;
const r = await conn.execute(`SELECT po_document FROM j_purchaseorder`);
console.dir(r.rows, { depth: null });
12.3. Using BLOB columns with OSON Storage Format in node-oracledb
You can use BLOB columns with Oracle’s optimized binary storage format called OSON if you want the fastest query and update performance. This OSON binary encoding format can be used with BLOB columns in both node-oracledb Thin or Thick modes. For Release 19c, BLOB with format OSON is supported only for Oracle Autonomous Databases. For Thick mode, you must additionally use Oracle Client 21c (or later).
To specify OSON format for BLOB columns, you can use the check
constraint with the clause IS JSON FORMAT OSON
when creating a table. This
check constraint ensures that only binary encoded OSON data is stored in that
column. For example, to create a table with a BLOB column containing OSON
data:
CREATE TABLE my_table (oson_col BLOB CHECK (oson_col IS JSON FORMAT OSON));
Inserting into BLOB columns with OSON Data
To encode the Javascript value into OSON bytes, you can use the
connection.encodeOSON()
method. For example:
const data = {key1: "val1"};
// Generate OSON bytes
const osonBytes = connection.encodeOSON(data);
console.log(osonBytes);
This method returns a Buffer and prints an output such as:
<Buffer ff 4a 5a 01 21 02 01 00 05 00 0d 00 00 fa 00 00 04 6b 65 79 32 a4 01 01 00 00 00 07 33 04 76 61 6c 32>
To insert the OSON bytes into the table, you can use:
// Insert the OSON bytes
const result = await connection.execute(
`INSERT INTO my_table (oson_col) VALUES (:1)`,
[osonBytes]
);
Fetching BLOB Columns with OSON Data
You can fetch BLOB columns which have the IS JSON FORMAT OSON
check
constraint enabled in the same way JSON type columns
are fetched when using Oracle Database 21c (or later). This can be done by
setting oracledb.future.oldJsonColumnAsObj
to the value true as
shown below. If you are using node-oracledb Thick mode, you must use Oracle
Client 21c (or later) for this setting to work. For example:
oracledb.future.oldJsonColumnAsObj = true;
const result = await connection.execute(`SELECT oson_col FROM my_table`);
console.log(result.rows[0][0]);
This prints an output such as:
{key1: "val1"}
If you do not set the oracledb.future.oldJsonColumnAsObj
to true,
then you can fetch BLOB columns that contain OSON data as shown below:
const result = await connection.execute(
`SELECT json_object ('hello' value 'world' returning blob format oson
) FROM dual`
);
const decodeOsonObj = connection.decodeOSON(result.rows[0][0]);
console.log(decodeOsonObj);
The connection.decodeOSON()
decodes the OSON Buffer and returns a
Javascript value. This prints an ouput such as:
{ hello: 'world' }
12.4. IN Bind Type Mapping
When binding a JavaScript object as oracledb.DB_TYPE_JSON
for
oracledb.BIND_IN
or oracledb.BIND_INOUT
in Oracle Database 21c
(or later), JavaScript values are converted to JSON attributes as shown
in the following table. The ‘SQL Equivalent’ syntax can be used in SQL
INSERT and UPDATE statements if specific attribute types are needed but
there is no direct mapping from JavaScript.
JavaScript Type or Value |
JSON Attribute Type or Value |
SQL Equivalent Example |
---|---|---|
null |
null |
NULL |
undefined |
null |
n/a |
true |
true |
n/a |
false |
false |
n/a |
Number |
NUMBER |
|
String |
VARCHAR2 |
|
Date |
TIMESTAMP |
|
Buffer |
RAW |
|
Array |
Array |
|
Object |
Object |
|
n/a |
CLOB |
|
n/a |
BLOB |
|
n/a |
DATE |
|
n/a |
INTERVAL YEAR TO MONTH |
|
n/a |
INTERVAL DAY TO SECOND |
|
n/a |
BINARY_DOUBLE |
|
n/a |
BINARY_FLOAT |
|
An example of creating a CLOB attribute with key mydocument
in a
JSON column using SQL is:
const sql = `INSERT INTO mytab (myjsoncol)
VALUES (JSON_OBJECT(key 'mydocument' value JSON_SCALAR(TO_CLOB(:b)) RETURNING JSON))`;
await connection.execute(sql, ['A short CLOB']);
When mytab
is queried in node-oracledb, the CLOB data will be
returned as a JavaScript String, as shown by the following table. Output
might be like:
{ mydocument: 'A short CLOB' }
12.5. Query and OUT Bind Type Mapping
When getting Oracle Database 21c or later JSON values from the database, the following attribute mapping occurs:
Database JSON Attribute Type or Value |
Javascript Type or Value |
---|---|
null |
null |
false |
false |
true |
true |
NUMBER |
Number |
VARCHAR2 |
String |
RAW |
Buffer |
CLOB |
String |
BLOB |
Buffer |
DATE |
Date |
TIMESTAMP |
Date |
INTERVAL YEAR TO MONTH |
Not supported. Will give an error. |
INTERVAL DAY TO SECOND |
Not supported. Will give an error. |
BINARY_DOUBLE |
Number |
BINARY_FLOAT |
Number |
Arrays |
Array |
Objects |
A plain JavaScript Object |
12.6. SQL/JSON Path Expressions
Oracle Database provides SQL access to JSON data using SQL/JSON path
expressions. A path expression selects zero or more JSON values that
match, or satisfy, it. Path expressions can use wildcards and array
ranges. A simple path expression is $.friends
which is the value of
the JSON field friends
.
For example, the previously created j_purchaseorder
table with JSON
column po_document
can be queried like:
SELECT po.po_document.location FROM j_purchaseorder po
With the JSON '{"userId":1,"userName":"Chris","location":"Australia"}'
stored in the table, a queried value would be Australia
.
The JSON_EXISTS
function tests for the existence of a particular
value within some JSON data. To look for JSON entries that have a
location
field:
const result = await connection.execute(
`SELECT po_document FROM j_purchaseorder WHERE JSON_EXISTS (po_document, '$.location')`
);
const d = result.rows[0][0]; // show only first record in this example
console.dir(d, { depth: null }); // assumes Oracle Database and Client 21c
This query displays:
{ userId: 1, userName: 'Chris', location: 'Australia' }
The SQL/JSON functions JSON_VALUE
and JSON_QUERY
can also be
used.
Note that the default error-handling behavior for these functions is NULL ON ERROR, which means that no value is returned if an error occurs. To ensure that an error is raised, use ERROR ON ERROR.
For more information, see SQL/JSON Path Expressions in the Oracle JSON Developer’s Guide.
12.7. Accessing Relational Data as JSON
In Oracle Database 12.2 or later, the JSON_OBJECT function is a great way to convert relational table data to JSON:
const result = await connection.execute(
`SELECT JSON_OBJECT ('deptId' IS d.department_id, 'name' IS d.department_name) department
FROM departments d
WHERE department_id < :did
ORDER BY d.department_id`,
[50]
);
for (const row of result.rows)
console.log(row[0]);
This produces:
{"deptId":10,"name":"Administration"}
{"deptId":20,"name":"Marketing"}
{"deptId":30,"name":"Purchasing"}
{"deptId":40,"name":"Human Resources"}
12.8. Portable JSON
Writing applications that can handle all the potential JSON storage types and potential client-server version combinations requires code that checks the Oracle versions and the returned column metadata. This allows the code to do appropropriate streaming or type conversion. It will be simpler to restrict the environment and data types supported by the application. Where possible, migrate to the new JSON type to take advantage of its ease of use and performance benefits.
Here is an example of code that works with multiple versions, with the assumption that older DBs use BLOB storage.
Create a table:
if (connection.oracleServerVersion >= 2100000000) {
await connection.execute(`CREATE TABLE mytab (mycol JSON)`);
} else if (connection.oracleServerVersion >= 1201000200) {
await connection.execute(`CREATE TABLE mytab (mycol BLOB CHECK (mycol IS JSON)) LOB (mycol) STORE AS (CACHE)`);
} else {
throw new Error('This application only works with Oracle Database 12.1.0.2 or greater');
}
Insert data:
const inssql = `INSERT INTO mytab (mycol) VALUES (:bv)`;
const data = { "userId": 2, "userName": "Anna", "location": "New Zealand" };
if (oracledb.oracleClientVersion >= 2100000000 && connection.oracleServerVersion >= 2100000000 ) {
await connection.execute(inssql, { bv: { val: data, type: oracledb.DB_TYPE_JSON } });
} else {
const s = JSON.stringify(data);
const b = Buffer.from(s, 'utf8');
await connection.execute(inssql, { bv: { val: b } });
}
Query data:
const qrysql = `SELECT mycol
FROM mytab
WHERE JSON_EXISTS (mycol, '$.location')
OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY`;
result = await connection.execute(qrysql, [], { outFormat: oracledb.OUT_FORMAT_ARRAY });
if (result.metaData[0].fetchType == oracledb.DB_TYPE_JSON) {
j = result.rows[0][0];
} else {
const d = await result.rows[0][0].getData();
j = await JSON.parse(d);
}
console.dir(j, { depth: null });