Prüfung von Referenzen auf Interactive Grid-Spalten leicht gemacht

01.
Juni
2024
Veröffentlicht von: Michael Schmid

Der Advisor von APEX (unter „Utilities“) ist ein wertvolles Werkzeug, um die Qualität von APEX-Applikationen zu prüfen. So kann z. B. geprüft werden, ob alle in der Applikation hinterlegten Abfragen oder PL/SQL-Code-Blöcke syntaktisch und semantisch korrekt sind und vieles mehr. Dadurch lassen sich viele Fehler frühzeitig erkennen und beheben, die sich ansonsten erst zur Laufzeit zeigen würden. Leider wurde der Advisor jedoch von Oracle über viele Jahre hinweg kaum weiterentwickelt, so dass sich Referenzen auf die Spalten von Interactive Grids (verfügbar seit Version 5.1) nicht vernünftig mit dem Advisor prüfen lassen. Dies wurde erst mit der APEX-Version 23.2 nachgeholt – da jedoch viele Entwickler wohl noch längere Zeit mit älteren Versionen arbeiten werden, stellt dieser Beitrag ein einfaches PL/SQL-Skript vor, mit dem sich ungültige Referenzen auf Interactive Grid-Spalten einfach identifizieren lassen.

Die Problematik

Seit Interactive Grids mit der Version 5.1 eingeführt wurden, erfreut sich dieses Feature berechtigterweise einer sehr großen Beliebtheit – es gibt sehr wahrscheinlich kaum eine produktive APEX-Anwendung, die auf die Verwendung von Interactive Grids verzichtet. Dabei lassen sich die Verhaltensweisen von Interactive Grids mit eigenem DML- und Locking-Code, durch Validierungen und auch Dynamic Actions in vielfältiger Weise mit eigenem PL/SQL-Code nachschärfen.

Dabei wird praktisch immer auf die Spaltenwerte des Interactive Grids mit Hilfe der Bindevariablen-Notation (Doppelpunkt) Bezug genommen. Ein typischer eigener DML-Code für ein Interactive Grids könnte so aussehen:

case :APEX$ROW_STATUS
  when 'C' then
    insert into emp ( empno, ename, deptno )
    values ( :EMPNO, :ENAME, :DEPTNO )
    returning empno into :EMPNO;
  when 'U' then
    update emp
    set ename  = :ENAME,
        deptno = :DEPTNO
    where empno  = :EMPNO;
  when 'D' then
    delete emp
    where empno = :EMPNO;
end case;


Es gibt zwar die Möglichkeit, im PL/SQL-Code-Editor im Page Designer eine Validierung des Codes durchzuführen. Diese überprüft jedoch nur, ob der Code syntaktisch und semantisch korrekt ist. Ob sich jedoch die Namen der verwendeten Bindevariablen sinnvoll auf Applikation- oder Page-Items bzw. auf Interactive Grids-Spalten beziehen, wird nicht überprüft.

Falls man sich somit bei der Code-Eingabe bei einem Item- bzw. Spaltennamen vertippt, so bleibt dieser Fehler zunächst unbemerkt. Es kommt aber noch schlimmer: Zur Ausführungszeit kommt es ebenfalls zu keinem harten Laufzeit-Fehler – es wird vielmehr nur in der Browser-Konsole und im Debug-Log ein Eintrag erzeugt, dass die ID des falsch getippten Items nicht gefunden werden konnte. Der Code selbst wird aber ausgeführt, und für die „falsche“ Bindevariable wird von APEX der Wert NULL gebunden! Dies kann lange unbemerkt bleiben und zu Datenkorruption führen. Darüber hinaus sind solche Fehler schwer zu analysieren.

Umso lohnender wäre deshalb die Ausführung des Advisors, um solche Fehler frühzeitig zu identifizieren. Leider erweist sich der Advisor jedoch bis APEX-Version 23.1 dafür als wenig hilfreich, da jede Referenz auf eine Interactive Grid-Spalte als ungültig gemeldet wird.
Dazu ein einfaches Beispiel:

APEX Advisor 


Obwohl alle verwendeten Spalten-Referenzen im PL/SQL-Code existieren, meldet der Advisor fälschlicherweise alle Referenzen als ungültig. Die diesbezüglichen Prüfungen des Advisors sind also schlicht unbrauchbar. Bei einer größeren Applikation, die viele Seiten mit Interactive Grids mit eigenem Code verwendet, führen diese vielen Falschmeldungen des Advisor außerdem dazu, dass der gesamte Bericht unübersichtlich und schwerer auswertbar ist.

Die Lösung mit Hilfe eines Skripts

Ein großer Vorteil von APEX besteht darin, dass all die Metadaten, die unsere Applikationen ausmachen, mit Hilfe der APEX-Dictionary-Views durch SQL und PL/SQL ausgewertet werden können. Dies ermöglicht die Entwicklung eines PL/SQL-Skripts, das fehlerhafte Bindevariablen-Referenzen im Zusammenhang mit Interactive Grids identifizieren und melden kann – am Ende dieses Beitrags finden Sie das komplette Skript zur Ausführung im SQL*Plus oder SQL Developer.
Das Skript arbeitet folgendermaßen:

1. Am Anfang wird über den Wert der Konstante C_APP_ID festgelegt, welche Applikation geprüft werden soll.

2. Falls gewünscht ist, dass alle geprüften Referenzen ausgegeben werden (und nicht nur fehlerhafte), so kann die Konstante C_VERBOSE auf TRUE gesetzt werden.

3.  Der Cursor GET_CODE holt aus APEX-Dictionary-Views folgende Codefragmente, die sich auf Interactive Grids beziehen:

  • APEX_APPLICATION_PAGE_VAL: der Code von Validierungen auf Interactive Grids,
  • APEX_APPLICATION_PAGE_PROC: der Code von Interactive Grid-Prozessen,
  • PEX_APPLICATION_PAGE_DA und APEX_APPLICATION_PAGE_DA_ACTS: der Code von Dynamic Actions, die auf Interactive Grids arbeit

Falls Code aus anderen Views zusätzlich geprüft werden soll, so kann die Abfrage des Cursors leicht um einen weiteren UNION ALL-Abschnitt erweitert werden.

4. Mit Hilfe der in APEX eingebauten Funktion WWV_FLOW_UTILITIES.GET_BINDS wird für jedes Code-Fragment ermittelt, welche Bindevariablen darin enthalten sind. Um die Vereinigungsmenge aus allen gefundenen Bindevariablen bilden zu können (mit MULTISET UNION), müssen die Funktionsergebnisse vorher noch mit Hilfe der Hilfsfunktion TO_TABLE in Nested Tables umgewandelt werden.

5. Für jede verwendete Bindevariable wird nun geprüft,

  • ob es sich um eine (gültige) Referenz auf ein Page- oder Applikation-Item handelt (Funktion EXISTS_ITEM) oder
  • ob es sich um eine (gültige) Referenz auf eine Spalte des jeweiligen Interactive Grids handelt (Funktion EXISTS_COLUMN) oder
  • ob es sich um eine Referenz auf ein eingebautes Item (wie z.B. APP_USER) handelt (Funktion IS_BUILTIN).

6. Falls keine der drei Möglichkeiten für eine gültige und sinnvolle Referenz vorliegt, wird per DBMS_OUTPUT.PUT_LINE eine entsprechende Meldung ausgegeben.

Die Ausführungszeit des Skripts beträgt im Regelfall auch bei größeren Applikationen nur wenige Sekunden.

Fazit

Mit Hilfe des Skripts lassen sich Bindevariable-Referenzen von Interactive Grids – in Ergänzung zu den Prüfungen im Advisor – einfach überprüfen und Fehler leicht beheben. Das Skript lässt sich auch problemlos erweitern, um andere, zusätzliche Prüfungen und Analysen durchzuführen. Technisch ohne Weiteres machbar wäre z. B. die Integration des Skript-Codes in einer Pipelined Table-Funktion, die die Ergebnisse als Nested Table zurückgibt, und die Darstellung davon in einem APEX-Bericht.
 

Hier das komplette Skript für Copy/Paste

set serveroutput on
declare
  c_app_id constant number := XXXXX /* Replace with your app id */;
  --
  c_verbose constant boolean := false;
  --
  l_binds apex_t_varchar2;
  l_bind  varchar2(4000 byte);
  l_refs  pls_integer := 0;
  --
  cursor get_code is
    select application_id, page_id,
           region_id, 'VALIDATION' comp_type,
           validation_name comp_name,
           validation_expression1 code1,
           validation_expression2 code2,
           condition_expression1  code3,
           null code4
    from   apex_application_page_val
    where  application_id = c_app_id
    and    region_id is not null
    union all
    select application_id, page_id,
           region_id, 'PROCESS' comp_type,
           process_name comp_name,
           cast(process_source as varchar2(4000 byte)) code1,
           attribute_04 as code2,
           condition_expression1 code3,
           null code4
    from   apex_application_page_proc
    where  application_id = c_app_id
    and    region_id is not null
    union all
    select da.application_id, da.page_id,
           da.when_region_id, 'DYNAMIC ACTION' comp_type,
           da.dynamic_action_name comp_name,
           act.attribute_01 code1,
           act.attribute_03 code2,
           act.attribute_04 code3,
           act.attribute_06 code4
    from   apex_application_page_da da
           join apex_application_page_da_acts act
             on(act.dynamic_action_id = da.dynamic_action_id)
    where  da.application_id = c_app_id
    and    da.when_region_id is not null
    order by application_id, page_id;
  --
  function to_table(p_array dbms_sql.varchar2_table) return apex_t_varchar2
  as
    l_tab apex_t_varchar2 := apex_t_varchar2();
  begin
    for i in 1 .. p_array.count loop
      if p_array(i) is not null
      then
        l_tab.extend(1);
        l_tab(l_tab.last) := p_array(i);
      end if;
    end loop;
    return l_tab;
  end;
  --
  function exists_item(p_application_id number, p_item varchar2)
    return boolean
  as
    l_count pls_integer;
  begin
    select count(*)
    into   l_count
    from   (select 1 from apex_application_page_items
            where  application_id = p_application_id
            and    item_name = p_item
            union all
            select 1 from apex_application_items
            where  application_id = p_application_id
            and    item_name = p_item)
    where  rownum = 1;
    --
    return(l_count > 0);
  end;
  --
  function exists_column(p_application_id number, p_region_id number,
                         p_column varchar2)
    return boolean
  as
    l_count pls_integer;
  begin
    select count(*)
    into   l_count
    from   (select 1 from apex_appl_page_ig_columns
            where  application_id = p_application_id
            and    region_id = p_region_id
            and    name = p_column
            union all
            select 1 from apex_application_page_rpt_cols
            where  application_id = p_application_id
            and    region_id = p_region_id
            and    column_alias = p_column)
    where  rownum = 1;
    --
    return(l_count > 0);
  end;
  --
  function is_builtin(p_bind varchar2) return boolean as
  begin
    return l_bind like 'APEX$ROW%' or l_bind like 'APP\_%' escape '\' or
           l_bind like 'JET\_%' escape '\' or l_bind like 'THEME\_%' escape '\' or
           l_bind like 'WORKSPACE\_%' escape '\' or
           l_bind in('REQUEST', 'DEBUG', 'IMAGE_PREFIX', 'APEX_FILES');
  end;
begin
  for l_code in get_code
  loop
    l_binds := to_table(wwv_flow_utilities.get_binds(l_code.code1)) multiset union distinct
               to_table(wwv_flow_utilities.get_binds(l_code.code2)) multiset union distinct
               to_table(wwv_flow_utilities.get_binds(l_code.code3)) multiset union distinct
               to_table(wwv_flow_utilities.get_binds(l_code.code4));
    for i in 1 .. l_binds.count
    loop
      l_bind := trim(leading ':' from l_binds(i));
      --
      l_refs := l_refs + 1;
      --
      if not exists_item(l_code.application_id, l_bind) and
         not exists_column(l_code.application_id, l_code.region_id, l_bind) and
         not is_builtin(l_bind)
      then
        dbms_output.put_line(apex_string.format(
          'PROBLEM: Page Id: %s, Region Id: %s, Type: %s, Object Name: %s, Bind Variable: %s',
          l_code.page_id, l_code.region_id, l_code.comp_type, l_code.comp_name, l_bind));
      elsif c_verbose
      then
        dbms_output.put_line(apex_string.format(
          'OK: Page Id: %s, Region Id: %s, Type: %s, Object Name: %s, Bind Variable: %s',
          l_code.page_id, l_code.region_id, l_code.comp_type, l_code.comp_name, l_bind));
      end if;
    end loop;
  end loop;
  dbms_output.put_line('Total references checked: ' || l_refs);
end;
/

 

Jede Menge Know-how für Sie!

In unserer Know-How Datenbank finden Sie mehr als 300 ausführliche Beiträge zu den Oracle-Themen wie DBA, SQL, PL/SQL, APEX und vielem mehr.
Hier erhalten Sie Antworten auf Ihre Fragen.