Skip to main content

Command Palette

Search for a command to run...

Function for Cascade Deletion of any Single Oracle Database Row

Simplify Row Deletion with Automatic Handling of Dependent Foreign Keys without Oracle Table Delete Cascade

Updated
10 min read
Function for Cascade Deletion of any Single Oracle Database Row
J

Specializing in innovative, visionary and strategic application of technology to create value and solve real world problems through a virtual CIO client-service model. Services include vision and strategic planning; creative problem solving and process optimization; application architecting, Oracle database & PL/SQL, Oracle APEX, Forms migration, and web design, build, and support. Experienced certified Oracle Database Administrator, Oracle Cloud Infrastructure, and Linux system administration team as well.

Context

  • Oracle foreign key constraints in database tables ensure referential integrity between two tables. This means that a column value in one table must match at least one value in the other table, or the column value can be null. For example, if the Customer_ID in the orders table is 2435, there must be a Customer_ID 2435 in the Customer table. If you try to delete 2435 from the Customer table, Oracle will show an error because the value exists in a related table, enforcing the match.

  • During the table creation process or by using ALTER TABLE, Oracle offers the "Delete Cascade" option. This option automatically removes all dependent rows if the primary row is deleted.

    • For example:

    •       ALTER TABLE Customer_Orders
            ADD CONSTRAINT Orders_Customer_ID_FK
            FOREIGN KEY (Customer_IDe)
            REFERENCES Customer (Customer_ID)
            ON DELETE CASCADE;
      
  • Delete Cascade can be very useful, BUT the risk of causing unwanted cascading deletions accidentally is potentially high and can be undesirable depending on the use case.

Objective

Delete any table/row combination, including dependent children foreign keys, with a single function, without the need for “Delete Cascade” in the table definition. Returns what will be deleted, or what was deleted depending on whether the function was called as Log Only.

Benefits

  • Single Function: Single database function that can be called for any table/row combination that recursively identifies child dependencies and deletes them, automatically adapting to schema and data changes. If change to delete process required, only one place needs to be changed.

  • Limited Use: Function execution can be limited to instances requiring delete cascade reducing the need to include “Delete Cascade” in the table definition, keeping the table more protected.

  • Logging: Ability to review specific child deletions that will be made before they occur provides the ability for an extra layer of review/reporting. Log information after deletion supports system event tracking.

  • APEX Modal Page:

    • Single Page: Reusable because it can be redirected to from anywhere in APEX. If a change to the delete process is required, only one place needs to be updated.

    • Review: Present deletions that will occur for review prior to actual deletion and require confirmation.

    • Security: Easier to limit access because functionality is only in one place. Could simply be kept in a system utility area.

    • 💡
      Caution: If there are many dependencies an APEX Gateway timeout error could occur if it takes too long to return a result. The delete could be run as a background process in that case once the desire to delete is confirmed. That leads to a larger question of whether a delete with so many dependencies is prudent…

Components How To

Create a single row delete cascade function:

  • Parameters: Accept Schema, Table_Name, Primary_ID, and Log Only Yes/No parameters.

  • Return an HTML-formatted list of tables, constraint column names, and child table primary key values that depend on the row to be deleted and will be removed due to a delete cascade action.

  • Cycle through dependent tables and rows, deleting each one, starting with the lowest dependency, just as the Oracle Delete Cascade action would do.

  • Delete cascade at the table / row combination level for any table, and automatically adjust for schema and table updates, additions, and deletions.

Following is the body of the function including comments and then highlights.

Create Or Replace Function Row_Delete_Cascade (
                                                 p_Schema       In Varchar2
                                                ,p_Table_Name   IN VARCHAR2
                                                ,p_Primary_ID   IN NUMBER
                                                ,p_Log_Only_YN  IN Varchar2
                                                )
  Return Varchar2
AS
  Type t_ID_List    Is Table of Number;
  Type t_Col_List   Is Table of Varchar2(128);

  v_SQL             VARCHAR2(32767 CHAR);
  v_Table_Name      Varchar2(128 );       -- Size should be in bytes
  v_Schema          Varchar2(128 );       -- Size should be in bytes
  v_Primary_Column  Varchar2(128);
  v_Child_IDs       t_ID_List;
  v_SY_Count        Integer;
  PK_Data_Type      VARCHAR2(30 CHAR);
  v_Return_Log      Varchar2(32767 CHAR);
  v_Return_log_Sub  Varchar2(32767 CHAR);
  v_Logged          Boolean;
  v_Log_Only_YN     Varchar2(1 CHAR);
  -- All Table Names with corresponding Foreign Key Column Names
  -- that rely on p_Schema.p_Table_Name
  -- Exclude tables that are the basis for a materialized view.
  --
  Cursor FK_cur IS
      Select  Upper(a.Table_Name)   AS FK_Table
             ,Upper(a.Column_Name)  AS FK_Column
      From    All_Cons_Columns a
      Join    All_Constraints c ON a.Constraint_Name = c.Constraint_Name
      Where       c.Constraint_Type = 'R'
              And c.r_constraint_name IN (
                                          Select  Constraint_Name
                                            FROM  All_Constraints
                                            WHERE     Owner           = v_Schema
                                                  And Table_Name      = v_Table_Name
                                                  AND Constraint_Type = 'P'
                                                  And Table_Name Not In (Select Object_Name
                                                                          From  All_Objects
                                                                          Where     Object_Name = a.Table_Name
                                                                                And Object_Type = 'MATERIALIZED VIEW'
                                                        )

                                          );

Begin
  v_Table_Name      := Upper(Trim(p_Table_Name));
  v_Schema          := Upper(Trim(p_Schema));
  v_Log_Only_YN     := Upper(Trim(p_Log_Only_YN));

  -- Fetch the column_Name of p_Table_Name's Primary Key
  Select  Column_Name
    Into  v_Primary_Column
    FROM  All_Cons_Columns
    WHERE
              Owner     = v_Schema
          And Table_Name = v_Table_Name
          And Constraint_Name In (
                                  Select Constraint_Name
                                    From All_Constraints
                                    WHERE     Owner           = v_Schema
                                          And Table_Name      = v_Table_Name
                                          AND Constraint_Type = 'P'
                                  )
          ;

  If v_Primary_Column Is Not Null Then
    -- p_Table_Name has a primary key assigned.

    -- Determine data type of p_Table_Name's
    -- primary key column.
    Select  Data_Type
      Into  PK_Data_Type
      FROM  All_Tab_Columns
      WHERE     Owner       = v_Schema
            And Table_Name  = v_Table_Name
            AND Column_Name = v_Primary_Column;
    v_Return_Log    := '<b>Primary Delete: ' || v_Schema || '.' || v_Table_Name
                        || '.' || v_Primary_Column || ' : ' || p_Primary_ID
                        || ' Type: ' || PK_Data_Type || '</b><br>';
    FOR FK_Rec In FK_Cur LOOP
      -- For each child, grandchild, etc. dependent table

      Declare
        v_Count       Integer;
        Child_PK_Cols t_Col_List;
        Child_PK_Type VARCHAR2(30);

      Begin
        -- Get child table's PK column Name.
        Select  Column_Name Bulk Collect
          Into  Child_PK_Cols
          From  All_Cons_Columns
          Where     Owner       = v_Schema
                And Table_Name  = Upper(FK_Rec.FK_Table)
                And Constraint_Name In (
                                    Select  Constraint_Name
                                      From  All_Constraints
                                      Where     Owner           = v_Schema
                                            And Table_Name      = Upper(FK_Rec.FK_Table)
                                            And Constraint_Type = 'P'
                                    )
        ;
        -- Determine child primary key column data type.
        Select  Data_Type
          Into  Child_PK_Type
          From  All_Tab_Columns
          Where     Table_Name  = UPPER(fk_rec.fk_table)
                And Owner       = v_Schema
                AND Column_Name = Child_PK_Cols(1);

        -- Fetch child primary key values that are dependents
        -- of p_Table_Name primary key value.
        -- Handle cases where the child primary key column is type Raw
        -- for example if SYS_GUID().  If alphanumeric this will error.
        --
        If Child_PK_Type Like 'RAW%' Then
          v_SQL := 'SELECT TO_NUMBER(RAWTOHEX(' || Child_PK_Cols(1) || ')) FROM ' || (v_Schema || '.' || FK_Rec.FK_Table) ||
                   ' WHERE ' || FK_Rec.FK_Column || ' = :id';
        Else
          v_SQL := 'SELECT ' || Child_PK_Cols(1) || ' FROM ' || (v_Schema || '.' || FK_Rec.FK_Table) ||
                   ' WHERE ' || FK_Rec.FK_Column || ' = :id';
        End If;
        v_Logged      := FALSE;

        EXECUTE IMMEDIATE v_SQL Bulk Collect Into v_Child_IDs Using p_Primary_ID;
        v_SY_Count  := v_Child_IDs.Count;
        -- Recursively delete each child row
        FOR i IN 1 .. v_Child_IDs.COUNT
          Loop
            v_Return_Log_Sub  := SY_Del_Primary_Key_Row_Children(  p_Schema     => p_Schema
                                                                  ,p_Table_Name => fk_rec.fk_table
                                                                  ,p_Primary_ID => v_Child_IDs(i)
                                                                  ,p_Log_Only_YN=> p_Log_Only_YN
                                                                );
            If Not v_Logged Then
              v_Return_Log  := v_Return_Log
                              || '&nbsp;&nbsp;Child: ' || v_Schema || '.' || FK_Rec.FK_Table || '.' || FK_Rec.FK_Column || '<br>';
              v_Logged      := TRUE;
            End If;
            v_Return_Log := v_Return_Log
                            || '&nbsp;&nbsp;&nbsp;&nbsp;Delete ID: ' || v_Child_IDs(i) || '<br>';
          End Loop;

        -- Delete child rows referencing current row
        If Child_PK_Type Like 'RAW%' Then

          v_SQL := 'DELETE FROM ' || (v_Schema || '.' || FK_Rec.FK_Table) ||
                   ' WHERE ' || FK_Rec.FK_Column || ' = :id';
        Else
          v_SQL := 'DELETE FROM ' || (v_Schema || '.' || FK_Rec.FK_Table) ||
                   ' WHERE ' || FK_Rec.FK_Column || ' = :id';
        End If;
        If v_Log_Only_YN = 'N' Then
          EXECUTE IMMEDIATE v_sql USING p_Primary_ID;
        End If;
      END;
    END LOOP;

    -- Delete p_Table_Name row with primary key value of p_Primary_ID in schema p_Schema.
    If PK_Data_Type Like 'RAW%' Then
      v_sql := 'DELETE FROM ' || (v_Schema || '.' || v_Table_Name) || ' WHERE ' || v_Primary_Column || ' = UTL_RAW.CAST_TO_RAW(:id)';
    Else
      v_sql := 'DELETE FROM ' || (v_Schema || '.' || v_Table_Name) || ' WHERE ' || v_Primary_Column || ' = :id';
    End If;
    If v_Log_Only_YN = 'N' Then
      EXECUTE IMMEDIATE v_sql USING p_Primary_ID;
    End If;

  End If;
  Return v_Return_Log;

EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE;
END;
/

Notes:

  • The function returns a list of the primary and child table information that is useful for reviewing what will be deleted before actually deletion (p_Log_Only_YN = ‘Y’), and for reviewing and tracking what actually was deleted (p_Log_Only_YN = ‘N’). Of course, review after deletion requires good historical logging to translate the primary ID values of now deleted rows that no longer exist…

  • The return list only consists of table information for child dependencies that have rows to be deleted. If a complete list of dependencies is desired, even if there are no matching rows move the following section:

                  If Not v_Logged Then
                    v_Return_Log  := v_Return_Log
                                    || '&nbsp;&nbsp;Child: ' || v_Schema || '.' || FK_Rec.FK_Table || '.' || FK_Rec.FK_Column || '<br>';
                    v_Logged      := TRUE;
                  End If;
    

    Above

              FOR i IN 1 .. v_Child_IDs.COUNT
    

    And remove references to v_Logged so it will always add the current dependent child table to the return log value.

  • The function can be made more universal by adding a parameter to only delete the primary table/row combination, resulting in an error if foreign key dependencies exist.

  • If your organization has standards for delete procedures, adjust the v_SQL delete statements to use those standard procedures instead of direct SQL "Delete for" statements. This approach maintains existing delete procedures that might include other actions, prevents foreign key errors since this function orders actions based on dependencies, and does not interfere with this function.

  • Identification of column Number vs. RAW data type is important so they are handle properly as can be seen in the Select and Delete statement construction.

  • The cursor specifically excludes materialized views from being considered. This might seem unnecessary because they are a “view”, but materialized views create underlying tables that could be confused for tables that need to have rows deleted.

  • Consider creating a table white and/or blacklist that is checked prior to deletion to only allow certain tables to have rows deleted in this manner, and/or prevent any rows from being deleted from a table in this manner, respectively.

  • Be sure to add your own style of error management, system event tracking, and parameter value validation.

Single Row Delete Cascade APEX Modal Dialog

Create a single APEX Modal Dialog page that users can access from anywhere to review the potential impact of deleting a row. They can then choose to cancel or proceed with the deletion.

The page takes function parameters from a redirect like schema, table name, primary ID, and a descriptive heading (blurred out in image), such as "Sam Smith ID 2435," to describe the Customer row to be deleted. Passing the heading makes it easy to appropriately adapt the heading for the type of data being deleted, i.e. orders, employees, inventory, etc.

Before Page header, the page runs a process that calls the row delete function in Log Only mode. It saves the return value of rows to be deleted in a Rich Text Page Item for viewing. This log data allows users to review the information and decide whether to cancel or confirm the deletion using buttons at the bottom of the page.

The Delete Button calls the delete row function again, but this time with p_Log_Only_YN = 'N'.

An example APEX page is:

A downside is that the list displays table names and primary ID values, which require users to have more knowledge and the ability to search using primary IDs if they want to verify one or more rows. To make this more user-friendly, use a lookup table to translate table names into user-friendly names. Translating the primary ID values would need adding a "v_SQL" select statement/execute immediate that queries row data and adds it to the log.

Of course, it doesn’t hurt to confirm the confirmation of the delete…

Conclusion

The row delete cascade function offers an alternative to the Oracle table “Delete Cascade” when deciding which table or row to delete and when Delete Cascade is suitable. This function requires a deliberate decision to use delete cascade, reducing risk from accidental deletions, like removing a product from the product table and unintentionally erasing inventory, orders, commissions, and more (and a career?). The function is highly flexible because it works for any table or row combination without needing specific code for each case, and it automatically adapts to changes, additions, and deletions in the schema and table rows.

Hopefully you found this helpful. If you did, please click the like button.