Skip to Main Content
  • Questions
  • Extracting data from JSON in relational tables

Breadcrumb

Question and Answer

Chris Saxon

Thanks for the question, Neil.

Asked: October 13, 2016 - 2:21 pm UTC

Last updated: October 28, 2016 - 9:32 am UTC

Version: 10.1.0.2

Viewed 10K+ times! This question is

You Asked

Hi Tom

Given the below JSON example, how do I know which notes and which occupants belong to which jobs (given that there is no unique identifier in the data itself? The relationship is defined by the JSON structure.

Is there a pseudo column or similar that can tell me the job array index that each record is from? I'm aware of FOR ORDINALITY but I think that is effectively a rownum and I assume there is no relationship between this and the actual array position?

WITH json AS (
SELECT '{"jobs":[{"name":"Wash Car","notes":[{"title":"Address","text":"1 High Street"},{"title":"Warning","text":"Scaffolding Required"}],"occupants":[{"name":"Mr Smith","gender":"Male"},{"name":"Mrs Smith","gender":"Female"}]},
{"name":"Wash Car","notes":[{"title":"Address","text":"1 Another Street"}],"occupants":[{"name":"Mr Jones","gender":"Male"},{"name":"Mrs Jones","gender":"Female"}]}]}' doc
FROM dual
)
SELECT row_number,
job_name,
note_title,
note_text,
occupant_name,
occupant_gender
FROM json_table((select doc from json),'$.jobs[*]' NULL ON ERROR
COLUMNS(row_number FOR ORDINALITY,
job_name VARCHAR2(20) PATH '$.name',
NESTED PATH '$.notes[*]'
COLUMNS (note_title VARCHAR2(20) PATH '$.title',
note_text VARCHAR2(20) PATH '$.text'),
NESTED PATH '$.occupants[*]'
COLUMNS (occupant_name VARCHAR2(20) PATH '$.name',
occupant_gender VARCHAR2(20) PATH '$.gender')))

ROW_NUMBER JOB_NAME NOTE_TITLE NOTE_TEXT OCCUPANT_NAME OCCUPANT_GENDER
---------- -------------------- -------------------- -------------------- -------------------- --------------------
1 Wash Car Address 1 High Street
2 Wash Car Warning Scaffolding Required
3 Wash Car Mr Smith Male
4 Wash Car Mrs Smith Female
5 Wash Car Address 1 Another Street
6 Wash Car Mr Jones Male
7 Wash Car Mrs Jones Female

7 rows selected.

There is a one-to-many relationship between job and note and a on one-to-many relationship between job and occupant. I want to extract this data using PL/SQL into a jobs table with a child notes table and a child occupants table on which I'll create the primary key on the jobs records and the foreign key on the notes and occupants records.

The above select isn't overly helpful and to achieve this I'll probably need to loop on all the jobs first record, then get all the notes for each job record and all the occupants for each job record.

Get jobs:

WITH json AS (
SELECT '{"jobs":[{"name":"Wash Car","notes":[{"title":"Address","text":"1 High Street"},{"title":"Warning","text":"Scaffolding Required"}],"occupants":[{"name":"Mr Smith","gender":"Male"},{"name":"Mrs Smith","gender":"Female"}]},
{"name":"Wash Car","notes":[{"title":"Address","text":"1 Another Street"}],"occupants":[{"name":"Mr Jones","gender":"Male"},{"name":"Mrs Jones","gender":"Female"}]}]}' doc
FROM dual
)
SELECT job_name
FROM json_table((select doc from json),'$.jobs[*]' NULL ON ERROR
COLUMNS(job_name VARCHAR2(20) PATH '$.name'))

JOB_NAME
--------------------
Wash Car
Wash Car

But, then when I proceed to get the notes records for each of these jobs records, I won't know what notes belong to which job. Similarly, with the occupants record:

WITH json AS (
SELECT '{"jobs":[{"name":"Wash Car","notes":[{"title":"Address","text":"1 High Street"},{"title":"Warning","text":"Scaffolding Required"}],"occupants":[{"name":"Mr Smith","gender":"Male"},{"name":"Mrs Smith","gender":"Female"}]},
{"name":"Wash Car","notes":[{"title":"Address","text":"1 Another Street"}],"occupants":[{"name":"Mr Jones","gender":"Male"},{"name":"Mrs Jones","gender":"Female"}]}]}' doc
FROM dual
)
SELECT job_name,
note_title,
note_text
FROM json_table((select doc from json),'$.jobs[*]' NULL ON ERROR
COLUMNS(job_name VARCHAR2(20) PATH '$.name',
NESTED PATH '$.notes[*]'
COLUMNS (note_title VARCHAR2(20) PATH '$.title',
note_text VARCHAR2(20) PATH '$.text')))
/

JOB_NAME NOTE_TITLE NOTE_TEXT
-------------------- -------------------- --------------------
Wash Car Address 1 High Street
Wash Car Warning Scaffolding Required
Wash Car Address 1 Another Street

The only way I can think to achieve this is to select each jobs record one at a time in a loop using the array index i.e. jobs[0], jobs[1] etc and then get the notes for jobs[0] and then the notes for jobs[1] etc.

WITH json AS (
SELECT '{"jobs":[{"name":"Wash Car","notes":[{"title":"Address","text":"1 High Street"},{"title":"Warning","text":"Scaffolding Required"}],"occupants":[{"name":"Mr Smith","gender":"Male"},{"name":"Mrs Smith","gender":"Female"}]},
{"name":"Wash Car","notes":[{"title":"Address","text":"1 Another Street"}],"occupants":[{"name":"Mr Jones","gender":"Male"},{"name":"Mrs Jones","gender":"Female"}]}]}' doc
FROM dual
)
SELECT job_name
FROM json_table((select doc from json),'$.jobs[0]' NULL ON ERROR
COLUMNS(job_name VARCHAR2(20) PATH '$.name'))
/

JOB_NAME
--------------------
Wash Car

Insert the job with a unique primary key identifier then get the notes and insert them into the notes table with a foreign key to the job primary key:

WITH json AS (
SELECT '{"jobs":[{"name":"Wash Car","notes":[{"title":"Address","text":"1 High Street"},{"title":"Warning","text":"Scaffolding Required"}],"occupants":[{"name":"Mr Smith","gender":"Male"},{"name":"Mrs Smith","gender":"Female"}]},
{"name":"Wash Car","notes":[{"title":"Address","text":"1 Another Street"}],"occupants":[{"name":"Mr Jones","gender":"Male"},{"name":"Mrs Jones","gender":"Female"}]}]}' doc
FROM dual
)
SELECT job_name,
note_title,
note_text
FROM json_table((select doc from json),'$.jobs[0]' NULL ON ERROR
COLUMNS(job_name VARCHAR2(20) PATH '$.name',
NESTED PATH '$.notes[*]'
COLUMNS (note_title VARCHAR2(20) PATH '$.title',
note_text VARCHAR2(20) PATH '$.text')))
/

JOB_NAME NOTE_TITLE NOTE_TEXT
-------------------- -------------------- --------------------
Wash Car Address 1 High Street
Wash Car Warning Scaffolding Required

Next get the job in array jobs[1] and then the notes and occupants for job in array jobs[1] etc.

This seems long winded to me. Am I missing a better way?

and Chris said...

I'm guessing you mean 12.1.0.2 for the version, as this is when json_table was released!

Anyway, so solve your problem it'd be nice if you could:

- Extract the jobs first with the names and notes as arrays
- Then convert these arrays to rows with further json_table expressions.

Unfortunately, if you try and nest them like this you get:

with json as
  (select
    '{"jobs":[{"name":"Wash Car","notes":[{"title":"Address","text":"1 High Street"},{"title":"Warning","text":"Scaffolding Required"}],"occupants":[{"name":"Mr Smith","gender":"Male"},{"name":"Mrs Smith","gender":"Female"}]},
{"name":"Wash Car","notes":[{"title":"Address","text":"1 Another Street"}],"occupants":[{"name":"Mr Jones","gender":"Male"},{"name":"Mrs Jones","gender":"Female"}]}]}'
    doc
  from dual
  ), jobs as (
select row_number, job_name, notes, names
from json_table (
  ( select doc from json ) ,
   '$.jobs[*]' null on error 
   columns ( row_number for ordinality, 
    job_name varchar2 ( 20 ) path '$.name', 
    notes varchar2(1000) format json path '$.notes',
    names varchar2(1000) format json path '$.occupants' 
  ) 
)
)
  select row_number, job_name, 
         note_id, note_title, note_text, 
         name_id, name, gender
  from   jobs,
         json_table(notes, '$[*]' 
          columns
           note_id    for ordinality, 
           note_title varchar2(30) path '$.title',
           note_text  varchar2(30) path '$.text'
         ),
         json_table(names, '$[*]' 
          columns
           name_id    for ordinality, 
           name varchar2(30) path '$.name',
           gender  varchar2(30) path '$.gender'
         );

ORA-40556: unsupported chaining of JSON_TABLE


:(

But there is a way around this!

Use the no_merge hint. This stops Oracle rewriting it one big json expression:

with json as
  (select
    '{"jobs":[{"name":"Wash Car","notes":[{"title":"Address","text":"1 High Street"},{"title":"Warning","text":"Scaffolding Required"}],"occupants":[{"name":"Mr Smith","gender":"Male"},{"name":"Mrs Smith","gender":"Female"}]},
{"name":"Wash Car","notes":[{"title":"Address","text":"1 Another Street"}],"occupants":[{"name":"Mr Jones","gender":"Male"},{"name":"Mrs Jones","gender":"Female"}]}]}'
    doc
  from dual
  ), jobs as (
select /*+ no_merge */row_number, job_name, notes, names
from json_table (
  ( select doc from json ) ,
   '$.jobs[*]' null on error 
   columns ( row_number for ordinality, 
    job_name varchar2 ( 20 ) path '$.name', 
    notes varchar2(1000) format json path '$.notes',
    names varchar2(1000) format json path '$.occupants' 
  ) 
)
)
  select row_number, job_name, 
         note_id, note_title, note_text, 
         name_id, name, gender
  from   jobs,
         json_table(notes, '$[*]' 
          columns
           note_id    for ordinality, 
           note_title varchar2(30) path '$.title',
           note_text  varchar2(30) path '$.text'
         ),
         json_table(names, '$[*]' 
          columns
           name_id    for ordinality, 
           name varchar2(30) path '$.name',
           gender  varchar2(30) path '$.gender'
         );

ROW_NUMBER  JOB_NAME  NOTE_ID  NOTE_TITLE  NOTE_TEXT             NAME_ID  NAME       GENDER  
1           Wash Car  1        Address     1 High Street         1        Mr Smith   Male    
1           Wash Car  1        Address     1 High Street         2        Mrs Smith  Female  
1           Wash Car  2        Warning     Scaffolding Required  1        Mr Smith   Male    
1           Wash Car  2        Warning     Scaffolding Required  2        Mrs Smith  Female  
2           Wash Car  1        Address     1 Another Street      1        Mr Jones   Male    
2           Wash Car  1        Address     1 Another Street      2        Mrs Jones  Female 


Do this and you can insert everything in a single statement with insert all!

You'll need to compute a row_number for each entity so you only insert the rows once. Then insert each row when the relevant rownum = 1:

create table jobs (
  job_id int,
  jb     varchar2(20)
);

create table notes (
  job_id int,
  title  varchar2(20),
  text   varchar2(30)
);

create table occupants (
  job_id int,
  name   varchar2(20),
  gender varchar2(30)
);
        
insert all 
  when rnj = 1 then into jobs values (row_number, job_name)
  when rnnm = 1 then into occupants values (row_number, name, gender)
  when rnnt = 1 then into notes values (row_number, note_title, note_text)
with json as
  (select
'{"jobs":[{"name":"Wash Car","notes":[{"title":"Address","text":"1 High Street"},{"title":"Warning","text":"Scaffolding Required"}],"occupants":[{"name":"Mr Smith","gender":"Male"},{"name":"Mrs Smith","gender":"Female"}]},
{"name":"Wash Car","notes":[{"title":"Address","text":"1 Another Street"}],"occupants":[{"name":"Mr Jones","gender":"Male"},{"name":"Mrs Jones","gender":"Female"}]}]}'
    doc
  from dual
  ), jobs as (
select /*+ no_merge */row_number, job_name, notes, names
from json_table (
  ( select doc from json ) ,
   '$.jobs[*]' null on error 
   columns ( row_number for ordinality, 
    job_name varchar2 ( 20 ) path '$.name', 
    notes varchar2(1000) format json path '$.notes'
    ,
    names varchar2(1000) format json path '$.occupants' 
  ) 
)
)
  select row_number, job_name, 
         note_id, note_title, note_text, 
         name_id, name, gender,
         row_number() over (partition by row_number order by row_number) rnj,
         row_number() over (partition by row_number, note_id order by row_number, name_id) rnnt,
         row_number() over (partition by row_number, name_id order by row_number, note_id) rnnm
  from   jobs,
         json_table(notes, '$[*]' 
          columns
           note_id    for ordinality, 
           note_title varchar2(30) path '$.title',
           note_text  varchar2(30) path '$.text'
         ),
         json_table(names, '$[*]' 
          columns
           name_id    for ordinality, 
           name varchar2(30) path '$.name',
           gender  varchar2(30) path '$.gender'
         );
         
select * from jobs;

JOB_ID  JB        
1       Wash Car  
2       Wash Car 

select * from occupants;

JOB_ID  NAME       GENDER  
1       Mr Smith   Male    
1       Mrs Smith  Female  
2       Mr Jones   Male    
2       Mrs Jones  Female  

select * from notes;

JOB_ID  TITLE    TEXT                  
1       Address  1 High Street         
1       Warning  Scaffolding Required  
2       Address  1 Another Street  

Rating

  (4 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

Neil Brookes, October 18, 2016 - 7:37 am UTC

Hi Chris

Yes you are right, I meant to select 12.1.0.2.0.

Thanks for your solution giving me a way to select the array objects with the parents. I especially like your "insert all".

My concern is that at present my actual functionality involves more levels of nesting than I eluded to in my example:

ma_jobs           one-to-many ma_job_notes
ma_jobs           one-to-many ma_job_sections
ma_job_section    one-to-many ma_job_attributes
ma_job_attributes one-to-many ma_job_attribute_options
ma_job_attributes one-to-many ma_job_attr_validation_rules
ma_job_attributes one-to-many ma_job_attribute_dependencies
ma_job_attributes one-to-many ma_job_attribute_values


When extracted using a SQL solution such as yours, this is going to result in a very large number of records due to the duplication required of the parent records to return the child records (especially with the relationship between ma_job_attributes as a parent to 4 child tables.

Would a PL/SQL solution be more suitable such that I target records from the JSON table by table (i.e. extract the ma_jobs then the ma_job_notes and then the ma_job_sections etc).

My understanding is that in JSON array order is preserved therefore my idea was to have nested loops - the outer loop getting all of the ma_jobs, then an inner loop selecting all of the ma_job_notes and ma_job_sections and another inner loop selecting all of the ma_job_attributes etc.

I guess to do this, I'll have to access each array using [1], [2], [3] ... [n].

e.g.
ma_jobs[1], ma_jobs[1].ma_job_notes[1]
            ma_jobs[1].ma_job_notes[2]
            ma_jobs[1].ma_job_notes[3]
            ma_jobs[1].ma_job_notes[n]
ma_jobs[2], ma_jobs[2].ma_job_notes[1]
            ma_jobs[2].ma_job_notes[2]
            ma_jobs[2].ma_job_notes[n]
ma_jobs[n], ma_jobs[n].ma_job_notes[1]
            ma_jobs[n].ma_job_notes[2]
            ma_jobs[n].ma_job_notes[3]
            ma_jobs[n].ma_job_notes[n]


I will be using a sequence number to populate primary and foreign keys.

Am I making this more complicated than it needs to be?

Thanks

Neil
Chris Saxon
October 21, 2016 - 10:11 am UTC

Nested loops will work. But the solution won't scale well if you have many sub attributes.

You could keep extending the single SQL statement. But this is likely to be a nightmare to maintain.

So how about trying something like this:

- Create a record type storing all the sub-document details for each job and an array of this type
- Select the JSON into this array, so you have one record/job
- Also have an array of numbers to store job ids

With these data structures, you can:

- Bulk insert the job details into the table
- Return the ids for these into the job id array
- The index for these job_ids will match the index of the original record with the other attributes
- Bulk insert into the child tables. You can pass the job ids from the their array
- Reuse this method as needed for attributes with many children, or use insert all technique

Hopefully this will keep a good balance between performance and maintainability.

Here's a starting example:

create sequence s start with 20;
create table docs (
  doc varchar2(1000) check (doc is json)
);

insert into docs values ('{
  "jobs": [
    {
      "name": "Wash Car",
      "notes": [
        {
          "title": "Address",
          "text": "1 High Street"
        },
        {
          "title": "Warning",
          "text": "Scaffolding Required"
        }
      ],
      "occupants": [
        {
          "name": "Mr Smith",
          "gender": "Male"
        },
        {
          "name": "Mrs Smith",
          "gender": "Female"
        }
      ]
    },
    {
      "name": "Wash Car"
    }
  ]
}');


create table jobs (
  job_id int,
  jb     varchar2(20)
);

create table notes (
  job_id int,
  title  varchar2(20),
  text   varchar2(30)
);

create table occupants (
  job_id int,
  name   varchar2(20),
  gender varchar2(30)
);

declare
  type rec is record (
    job_num int,
    job_name varchar2(100),
    notes varchar2(1000),
    names varchar2(1000)
  );
  type rec_arr is table of rec index by binary_integer;
  
  job_ids dbms_sql.number_table;
  recs rec_arr;
begin

  with jobs as (
select row_number, job_name, notes, names
from json_table (
  ( select doc from docs ) ,
   '$.jobs[*]' null on error 
   columns ( row_number for ordinality, 
    job_name varchar2 ( 20 ) path '$.name', 
    notes varchar2(1000) format json path '$.notes',
    names varchar2(1000) format json path '$.occupants' 
  ) 
)
)
  select row_number, job_name, notes, names
  bulk   collect into recs
  from   jobs;
  
  forall i in 1 .. recs.count
    insert into jobs values (s.nextval, recs(i).job_name)
    returning job_id bulk collect into job_ids;
 
  forall i in 1 .. recs.count 
    insert into occupants 
      with json as (
  select recs(i).names j from dual
)
  select job_ids(i) , j.*
  from   json, json_table (j, '$[*]' null on error
    columns (
      name varchar2(100) path '$.name',
      gender varchar2(100) path '$.gender'
    )
  ) j;
  
end;
/
select * from occupants;

JOB_ID  NAME       GENDER  
20      Mr Smith   Male    
20      Mrs Smith  Female  

select * from jobs;

JOB_ID  JB        
20      Wash Car  
21      Wash Car  


Increase nesting

Neil Brookes, October 26, 2016 - 3:13 pm UTC

Hi Chris

Your solution is great. However, I can see a couple of issues for me. As briefly mentioned before, in reality I'm going down to many more levels of nesting.

I've made an example in the JSON below including occupant attributes to show another level below occupants. My real world development actually goes down another level again with four arrays of objects at this bottom level.

I can see this causing an issue with the solution you've proposed as the datatype length of the column definition in the JSON_TABLE COLUMNS clause will be insufficient to hold $.occupants JSON with it's nested arrays of objects. I believe the maximum length I'll be able to define will be 4000 but each $.occupants JSON will exceed this.

Also, I'm not sure how I'd extend your solution to allow me to extract these nested arrays in the child tables such as occupant attributes.

Please see below - I've put some comments in to highlight my problems:

DROP SEQUENCE jobs_nb_seq;
DROP SEQUENCE notes_nb_seq;
DROP SEQUENCE occupants_nb_seq;
DROP SEQUENCE occupant_attributes_nb_seq;

CREATE sequence jobs_nb_seq                START WITH 100;
CREATE sequence notes_nb_seq               START WITH 200;
CREATE sequence occupants_nb_seq           START WITH 300;
CREATE sequence occupant_attributes_nb_seq START WITH 400;

DROP TABLE jobs_nb;
DROP TABLE notes_nb;
DROP TABLE occupants_nb;
DROP TABLE occupant_attributes_nb;
DROP TABLE docs_nb;

CREATE TABLE docs_nb( 
  doc VARCHAR2(4000) CHECK(doc IS JSON));
  
CREATE TABLE jobs_nb ( 
  id   INT            NOT NULL, 
  name VARCHAR2(20)   NOT NULL);
  
CREATE TABLE notes_nb ( 
  id     INT          NOT NULL,
  job_id INT          NOT NULL, 
  title  VARCHAR2(20) NOT NULL, 
  text   VARCHAR2(30) NOT NULL);

CREATE TABLE occupants_nb (
  id     INT          NOT NULL,
  job_id INT          NOT NULL,
  name   VARCHAR2(20) NOT NULL,
  gender VARCHAR2(30) NOT NULL);  
  
CREATE TABLE occupant_attributes_nb (
  id          INT          NOT NULL,
  occupant_id INT          NOT NULL,
  code        VARCHAR2(4)  NOT NULL,
  name        VARCHAR2(20) NOT NULL,
  value       VARCHAR2(30) NOT NULL);   
  
INSERT INTO docs_nb 
VALUES (
'{
  "jobs": [
    {
      "name": "Wash Car",
      "notes": [
        {
          "title": "Address",
          "text": "1 High Street"
        },
        {
          "title": "Warning",
          "text": "Scaffolding Required"
        }
      ],
      "occupants": [
        {
          "name": "Mr Smith",
          "gender": "Male"
        },
        {
          "name": "Mrs Smith",
          "gender": "Female"
        },
        {
          "name": "Ms Smith",
          "gender": "Female"
        }
      ]
    },
    {
      "name": "Wash Car 2",
      "notes": [
        {
          "title": "Address",
          "text": "2 High Street"
        },
        {
          "title": "Warning",
          "text": "Scaffolding Required 2"
        },
        {
          "title": "Another note",
          "text": "This is another note 2"
        }
      ],
      "occupants": [
        {
          "name": "Mr Smith2",
          "gender": "Male2",
          "attributes": [
            {
              "code": "EC",
              "name": "Eye Colour",
              "value": "Brown"
            },
            {
              "code": "HT",
              "name": "Height",
              "value": "5"
            },
            {
              "code": "HC",
              "name": "Hair Colour",
              "value": "Blonde"
            }
          ]
        },
        {
          "name": "Mrs Smith2",
          "gender": "Female2"
        }
      ]
    }
  ]
}');
 
DECLARE

  TYPE t_jobs_rec IS RECORD (
    name      jobs_nb.name%TYPE,
    notes     CLOB,
    occupants CLOB);

  TYPE t_jobs_tab IS TABLE OF t_jobs_rec INDEX BY BINARY_INTEGER;
  
  l_jobs_tab         t_jobs_tab;
  l_job_ids_tab      dbms_sql.number_table;
  l_occupant_ids_tab dbms_sql.number_table;

BEGIN

  -- Bulk Collect each job into PL/SQL jobs tabl
  WITH jobs AS (
    SELECT name,
           notes,
           occupants
    FROM   JSON_TABLE((SELECT doc FROM docs_nb), '$.jobs[*]' NULL ON ERROR 
             COLUMNS(name      VARCHAR2(20)   PATH '$.name', 
                     notes     VARCHAR2(1000) FORMAT JSON PATH '$.notes',
                     occupants VARCHAR2(1000) FORMAT JSON PATH '$.occupants'))
  )
  SELECT name,
         notes,
         occupants 
  BULK COLLECT INTO l_jobs_tab 
  FROM jobs;

  -- Insert each job in PL/SQL jobs table into database jobs_nb table
  -- l_job_ids_tab used to store sequence number generated jobs_nb.id primary key
  FORALL i IN 1 .. l_jobs_tab.COUNT
    INSERT INTO jobs_nb (
      id,
      name)
    VALUES (
      jobs_nb_seq.NEXTVAL,
      l_jobs_tab(i).name) RETURNING id BULK COLLECT INTO l_job_ids_tab;
      
  -- Insert each occupant in PL/SQL jobs table into database occupants_nb table
  -- l_job_ids_tab used to retreive sequence number generated jobs_nb.id for occupants_nb.job_id foreign key
  FORALL i IN 1 .. l_jobs_tab.COUNT
    INSERT INTO occupants_nb (
      id,
      job_id,
      name,
      gender)
    WITH json AS (
      SELECT l_jobs_tab(i).occupants j 
      FROM   dual
    )
    SELECT occupants_nb_seq.NEXTVAL,
           l_job_ids_tab(i),
           j.name,
           j.gender
    FROM   json,
           JSON_TABLE(j,'$[*]' NULL ON ERROR
             COLUMNS(name   VARCHAR2(100) PATH '$.name',
                     gender VARCHAR2(100) PATH '$.gender')) j;
                     
  -- Insert each occupant attribute in PL/SQL jobs table into database occupant_attributes_nb table
  -- TO DO
  -- How would I populate the l_occupant_ids_tab for this insert         
    
  -- Insert each note in PL/SQL jobs table into database notes_nb table
  -- l_job_ids_tab used to retreive sequence number generated jobs_nb.id for notes_nb.job_id foreign key
  FORALL i IN 1 .. l_jobs_tab.COUNT
    INSERT INTO notes_nb(
      id,
      job_id,
      title,
      text)
    WITH json AS ( 
      SELECT l_jobs_tab(i).notes j 
      FROM   dual
    )
    SELECT notes_nb_seq.NEXTVAL,
           l_job_ids_tab(i),
           j.title,
           j.text
    FROM   json,
           JSON_TABLE(j,'$[*]' NULL ON ERROR
             COLUMNS(title VARCHAR2(100) PATH '$.title', 
                     text  VARCHAR2(100) PATH '$.text')) j;
                     
END;
/

SELECT * FROM jobs_nb
/

SELECT * FROM notes_nb
/

SELECT * FROM occupants_nb
/

SELECT * FROM occupant_attributes_nb
/


Kind Regards

Neil
Chris Saxon
October 27, 2016 - 1:38 pm UTC

Hmmmm. Tricky. We don't support returning on an "insert .. select" unfortunately!

I think your best bet is to use something which enables you to get parts of a JSON document. 12.2 comes with new PL/SQL types to do this!

In the meantime, check out some of the existing JSON manipulators, such as PL/JSON:

https://github.com/pljson/pljson

or APEX_JSON in (APEX 5):

https://docs.oracle.com/cd/E59726_01/doc.50/e39149/apex_json.htm#AEAPI29635

Neil Brookes, October 27, 2016 - 3:52 pm UTC

Hi Chris

Yes, those 12.2 PL/SQL JSON Object types look like exactly what I need.

I'm using APEX JSON for creating JSON from relational data as I couldn't find any native equivalent.

I'll have a look into the functionality of APEX JSON for consuming JSON.

Thanks
Connor McDonald
October 28, 2016 - 2:04 am UTC

glad we could help

Neil Brookes, October 28, 2016 - 8:08 am UTC

Hi Chris

I'm presuming to achieve something like this using p_index - getting one array index at a time would require me to use dynamic SQL?

DECLARE

  CURSOR c_jobs(p_index IN PLS_INTEGER) IS
    SELECT name
    FROM   JSON_TABLE((SELECT doc FROM docs_nb), '$.jobs['||p_index||']' ERROR ON ERROR 
              COLUMNS(name VARCHAR2(20)PATH '$.name'));
              
  l_jobs c_jobs%ROWTYPE; 
  l_jobs_index PLS_INTEGER := 0;

BEGIN

  LOOP
 
    OPEN c_jobs(0);
    FETCH c_jobs INTO l_jobs;
    IF c_jobs%FOUND THEN
      CLOSE c_jobs;
    ELSE
      CLOSE c_jobs;
      EXIT;
    END IF;

    INSERT INTO jobs_nb (
      id,
      name)
    VALUES (
      jobs_nb_seq.NEXTVAL,
      l_jobs.name);
      
    l_jobs_index := l_jobs_index + 1;  
  
  END LOOP;


END;
/

Chris Saxon
October 28, 2016 - 9:32 am UTC

Yes. The path expression has to be a literal.

More to Explore

Analytics

Analytic SQL got you confused? Check out Connor McDonald's complete video course.