'Iterate over table of objects in Oracle

How should I iterate over a table of objects like this?

CREATE TYPE SOME_OBJECT AS OBJECT (
  ATTR1 VARCHAR2(20)
, ATTR2 VARCHAR2(30)
);
/

CREATE TYPE C_SOME_OBJECT AS TABLE OF SOME_OBJECT;
/

And... I want to use this as a parameter for a procedure. Have to initialize it in procedure?

PROCEDURE SOME_PROCEDURE(OBJECT IN C_SOME_OBJECT)
IS
BEGIN
  --Some code here iterating IN parameter
END;


Solution 1:[1]

-- NOTE #1
-- You cant use Object as name for your input variable because it is a type
-- (Types are reserved words)
PROCEDURE SOME_PROCEDURE(p_SOME_TABLE IN C_SOME_OBJECT)
IS
BEGIN
  FOR i IN p_SOME_TABLE.FIRST .. p_SOME_TABLE.LAST
  LOOP
     -- NOTE #2 Manage it like this
     -- Current iteration: p_SOME_TABLE(i)
     -- Access Example   : p_SOME_TABLE(i).ATTR1

     -- NOTE #3 Or you can assign current iteration to a variable
     -- then use that variable thought it is not by reference if you
     -- do it like this.
  END LOOP;
END;

UPDATE:

Also be careful not to confuse RECORDS with OBJECTS.

An OBJECT is an SQL type but RECORD on the other hand is a PL/SQL type, you could think of it more of a C/C++ like struct.

UPDATE_2:

Note that this wont raise any exception if the table is empty thus you need to handle if there are no data, for example you could use .COUNT like this:

IF p_SOME_TABLE.COUNT = 0 THEN
   RAISE my_exception;
END IF;

Solution 2:[2]

You can use a WHILE loop with FIRST, NEXT, LAST to handle sparse collections (where elements have been deleted) and you also need to check for uninitialised elements in the collection:

CREATE PROCEDURE SOME_PROCEDURE(
  I_OBJECT IN C_SOME_OBJECT
)
IS
  i PLS_INTEGER;
BEGIN
  -- Check that the collection is not NULL
  IF I_OBJECT IS NULL THEN
    RETURN;
  END IF;

  i := I_OBJECT.FIRST;
  WHILE i IS NOT NULL LOOP
    IF I_OBJECT(i) IS NOT NULL THEN
      DBMS_OUTPUT.PUT_LINE( i || ' = (' || I_OBJECT(i).attr1 || ', ' || I_OBJECT(i).attr2 || ')' );
    ELSE
      DBMS_OUTPUT.PUT_LINE( i || ' IS NULL' );
    END IF;
    i := I_OBJECT.NEXT(i);
  END LOOP;
END;
/

Then you can call it:

DECLARE
  o C_SOME_OBJECT := C_SOME_OBJECT(
                       SOME_OBJECT( '1.1', '1.2' ),
                       SOME_OBJECT( '2.1', '2.2' ),
                       NULL,
                       SOME_OBJECT( '4.1', '4.2' )
                     );
BEGIN
  o.DELETE(2);

  SOME_PROCEDURE( o );
END;
/

Outputs:

1 = (1.1, 1.2)
3 IS NULL
4 = (4.1, 4.2)

Issues with FOR ... LOOP:

Using FOR i IN 1 .. o.COUNT LOOP:

DECLARE
  o C_SOME_OBJECT := C_SOME_OBJECT(
                       SOME_OBJECT( '1.1', '1.2' ),
                       SOME_OBJECT( '2.1', '2.2' ),
                       NULL,
                       SOME_OBJECT( '4.1', '4.2' )
                     );
BEGIN
  o.DELETE(2);

  FOR i IN 1 .. o.COUNT LOOP
    IF o(i) IS NOT NULL THEN
      DBMS_OUTPUT.PUT_LINE( i || ' = (' || o(i).attr1 || ', ' || o(i).attr2 || ')' );
    ELSE
      DBMS_OUTPUT.PUT_LINE( i || ' IS NULL' );
    END IF;
  END LOOP;
END;
/

Does not run and raises the exception:

ORA-01403: no data found
ORA-06512: at line 12

Because it reaches the second item in the array and tries to read it but it has been deleted and no data is found.

If the exception is caught:

DECLARE
  o C_SOME_OBJECT := C_SOME_OBJECT(
                       SOME_OBJECT( '1.1', '1.2' ),
                       SOME_OBJECT( '2.1', '2.2' ),
                       NULL,
                       SOME_OBJECT( '4.1', '4.2' )
                     );
BEGIN
  o.DELETE(2);

  FOR i IN 1 .. o.COUNT LOOP
    BEGIN
      IF o(i) IS NOT NULL THEN
        DBMS_OUTPUT.PUT_LINE( i || ' = (' || o(i).attr1 || ', ' || o(i).attr2 || ')' );
      ELSE
        DBMS_OUTPUT.PUT_LINE( i || ' IS NULL' );
      END IF;
    EXCEPTION
      WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE( i || ' NOT FOUND' );
    END;
  END LOOP;
END;
/

Then the output is:

1 = (1.1, 1.2)
2 NOT FOUND
3 IS NULL

This outputs element 2, which does not exist, and does not output element 4, which does exist.

Using FOR i IN o.FIRST .. o.LAST LOOP:

DECLARE
  o C_SOME_OBJECT := C_SOME_OBJECT(
                       SOME_OBJECT( '1.1', '1.2' ),
                       SOME_OBJECT( '2.1', '2.2' ),
                       NULL,
                       SOME_OBJECT( '4.1', '4.2' )
                     );
BEGIN
  o.DELETE(2);

  FOR i IN o.FIRST .. o.LAST LOOP
    BEGIN
      IF o(i) IS NOT NULL THEN
        DBMS_OUTPUT.PUT_LINE( i || ' = (' || o(i).attr1 || ', ' || o(i).attr2 || ')' );
      ELSE
        DBMS_OUTPUT.PUT_LINE( i || ' IS NULL' );
      END IF;
    EXCEPTION
      WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE( i || ' NOT FOUND' );
    END;
  END LOOP;
END;
/

Outputs:

1 = (1.1, 1.2)
2 NOT FOUND
3 IS NULL
4 = (4.1, 4.2)

This outputs 4 elements (including the deleted 2nd element) when o.COUNT is only 3; meaning that the code is doing extra, unnecessary processing.

db<>fiddle here

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2