Cascading LOV in Tabular Form

November 2, 2010 8 Kommentare

Da Oracle leider noch keine out-of-the-box Lösung zu diesem Thema bereitstellt, möchte ich in diesem Beitrag ein Beispiel aufzeigen, wie man eine cascading LOV in einer Tabular Form realisieren kann.
Es gibt sicherlich noch andere Ansätze und Lösungen, aber diese erscheint mir derzeit die einfachste funktionierende zu sein.

Diese kleine Beispiel-Anwendung basiert auf dem EMP-Schema und der EMP-Tabelle. Per Definition sollen bei der Auswahl eines Departments nur die Personen als Manager angezeigt werden, die dem entspr. Department zugeordnet sind.
D.h. dass die Spalte MGR als cascading LOV realisiert werden soll. Durch Änderung des Departments soll sich die MGR-Liste enstp. des zuvor ausgewählten Departments automatisch aktualisieren.
Das Beispiel dazu finden Sie hier: http://apex.oracle.com/pls/apex/f?p=39514

Die nachfolgende Beschreibung basiert darauf, dass bereits eine Tabular Form existiert und in etwa so aussieht, wie in folgender Abbildung dargestellt. Die LOVs auf den Spalten DEPTNO und MGR sind simple LOVs, die alle Departments bzw. alle Personen selektieren.

Aufbau Tabular Form EMP

Welche Schritte sind nun zusätzlich notwendig, um eine cascading LOV in einer Tabular Form zu realisieren?
1. Hilfs-Item erzeugen
2. On-Demand Prozess erzeugen
3. Javascript Funktion mit AJAX-Call implementieren
4. Javascript Funktionsaufrufe integrieren

Lösung

 

1. Zuerst erstellen wir ein Hilfs-Item, mit dem wir die Parent-ID (in unserem Falle die Dept-No) austauschen können 

  • Create Page Item
  • Item Type: HIDDEN
  • Name: P<x>_DEPTNO_REFVAL (“Px_” bitte austauschen durch die entsprechende Page-Nummer)
  • Protected: NO
  • default belassen und “Create…”

2. On-Demand Prozess erzeugen

Dieser Prozess soll entspr. der ausgewählten Department-ID alle Employees lesen und als JSON-String aufbereiten.
Hinweis: Dieser Prozess wird später als Ajax-Call aufgerufen!

  • Shared components -> Application Processes -> Create
  • Name: GET_CASCADING_LOV_JSON (ACHTUNG! hier bitte unbedingt auf Groß-/ Kleinschreibung achten!)
        Sequence: <egal>
        Process Point: On Demand
  • Process Text:

BEGIN
  — hier bitte unbedingt beim item-namen das “x” durch ihre page-nr ersetzen
  APEX_UTIL.JSON_FROM_SQL(’select ename, empno from emp where deptno = ‘||:P<x>_DEPTNO_REFVAL||’ order by 1′);
END;

  • KEINE Conditions angeben und “Create Process” klicken.

3. Javascript Funktion mit AJAX-Call implementieren

Der AJAX-Call wird vollständig in Javascript erstellt. Für dieses Beispiel werde ich den JS-Code einfach in die Form übernehmen.
Bitte gestatten Sie mir die Anmerkung, dass ich in Bezug auf die Widerverwendbarkeit und Lesbarkeit den JS-Codes lieber in einem separatesn File ablege und dieses JS-File über das script-Tag importiere.
Jedoch zur Vereinfachung wollen wir die JS-Funktion einfach in die Form reinkopieren.

JS-Code anpassen
    Damit unsere AJAX-Call später richtig funktioniert, müssen wir zuerst einige Anpassungen machen.
    Bitte tauschen Sie mit Suchen und Ersetzen …

  • “f05” durch die item-nr des DEPTNO-items in ihrem report
  • “f06” durch die item-nr des MGR-items in ihrem report
  • “P<x>_DEPTNO_REFVAL” => das <x> ersetzen durch ihre page-nr.
  • “…APPLICATION_PROCESS=GET_CASCADING_LOV_JSON…” => tauschen gegen den Namen des On-Demand Processes.
  • “myLovObjects.row[i].EMPNO” und “myLovObjects.row[i].ENAME” tauschen gegen die Spalten-Namen, die Sie im On-Demand Prozess verwendet haben.

     Bevor ich die einzelnen Codestellen erkläre, hier erstmal der gesamte JS-Code.

function refresh_cascading_lov(pRowItemObj){

  // separate given html-object, name and ID.
  var tmpRowObj = $(‘#’+pRowItemObj.id);
  var tmpObjName = tmpRowObj.attr(’name’);
  var tmpObjID   = tmpRowObj.attr(‘id’);
 
  // compute begin-position of row-number.
  var pos = tmpObjID.lastIndexOf(‘_’);
  if(pos <= 0)
    return;
 
  var row = tmpObjID.substring(pos+1, tmpObjID.length);
  var rowNum = null;
  try {
    rowNum = parseInt(row);
  } catch (E) {
    rowNum = 1;
  }
 
  var selectTag = $(‘#f06_’+row);
  var selectTagVal = selectTag.val();
 
  // if curr-item is NOT the parent-id item, check if list would be refreshed for this row before.
  // to check this we have to create a help-item per row and call it “hMgrRefreshFlag_<rownum>”
  var mgrRefreshFlag = $(‘#hMgrRefreshFlag_’+row);
  var mgrRefreshFlagVal = mgrRefreshFlag.text();
  // if refresh-flag exists …
  if(mgrRefreshFlagVal!=undefined && mgrRefreshFlagVal!=null && mgrRefreshFlagVal!=””) {
    // EXISTS Path: check if the list had been refreshed before.
    if(mgrRefreshFlagVal==’true’) {
      if(tmpObjName == “f05”) {
        null;
      } else {
        $(‘#f06_’+row).val(selectTagVal);
        return;
      }
    } else {
      // EXISTS Path: if it not had been refreshed before, set value to “true” -> refreshed for this row!
      $(‘#hMgrRefreshFlag_’+row).text(‘true’);
    }
  } else {
    // NOT-EXISTS Path: if not exists, create this help-item and set the value to “true”.
    tmpRowObj.after(‘<span id=”hMgrRefreshFlag_’+row+'” name=”hMgrRefreshFlag” style=”display:none;”>true</span/>’);
  }
 
  /*
  set parent-id (deptno) to ref-item.
  !!! NOTE !!!
  In this case parent-id (deptno) is column f05 in row. If you try this, please check column deptno what column-id it is in your case.
  The easiest way to do that is to view the generated html-code and look for an select-tag with the value of our departments.
  */
  var deptObj  = $x(‘f05_’+row);
  var deptObjVal  = deptObj.value;
  var lovRefID = deptObjVal;
  $(‘#P6_DEPTNO_REFVAL’).val(lovRefID);
 
  /*
  define the Ajax call. The only variable of note in this example is the application_process of type “On Demand” created a view moments before.
  */
  var get = new htmldb_Get(null, $v(‘pFlowId’), ‘APPLICATION_PROCESS=GET_CASCADING_LOV_JSON’,$v(‘pFlowStepId’));
 
  /*
  add the value in f05_xxxx into the session value for P6_DEPTNO_REFVAL. this is important as without this step the APEX server would not know
  what value the user had entered
  */
  get.add(‘P6_DEPTNO_REFVAL’,lovRefID);
 
  /*
  call the ondemand process and accept the returning values
  */
  var gReturn = get.get();
  var myLovObjects = $u_eval(‘(‘ + gReturn + ‘)’); 
 
  /*
  remove all options from select-list
  */
  selectTag.empty();
  /*
  add null-option for select-list
  */
  selectTag.append(‘<option value=””>&nbsp;</option>’);

  /*
  iterate above all returned list-objects and add as options to select-tag.
  */
  var empnoFoundInList=false;
  for(i=0; i<myLovObjects.row.length;i++) {
    var tmpEmpno = myLovObjects.row[i].EMPNO;
    /*
    if current empno in listis equal to mgr-no selected before, set this as default selected value.
    */
    if(tmpEmpno == selectTagVal) {
      selectTag.append(‘<option selected=”selected” value=”‘+tmpEmpno+'”>’+myLovObjects.row[i].ENAME+'</option>’);
      empnoFoundInList = true;
    } else {
      selectTag.append(‘<option value=”‘+tmpEmpno+'”>’+myLovObjects.row[i].ENAME+'</option>’);
    }
  }
 
  // if empno not found in list, add empno as additional option.
  if(!empnoFoundInList) {
    selectTag.append(‘<option selected=”selected” value=”‘+selectTagVal+'”>’+selectTagVal+'</option>’);
  }
 
  /*
  at least mark default value as selected.
  */
  var tmpF06=$(‘#f06_’+row);
  tmpF06.val(selectTagVal);
  var tmp2=$(‘#f06_’+row).val();
}

 

Nun ein paar wichtige Erklärungen zum JS-Code.

Der funktion “refresh_cascading_lov” wird als Argument pRowItemObj übergeben. Hier steht später die Select-Liste als Objekt drin, auf das der Anwender geklickt hat (also entweder DEPTNO oder MGR).
Als erstes ermitteln wir nun den Namen und die ID des übergebenen Objekt und ziehen uns daraus die jeweilige Zeilennummer. Dann ermitteln wir noch die Ziel-Select-Liste und speichern den Ursprungswert.

  var tmpRowObj = $(‘#’+pRowItemObj.id);
  var tmpObjName = tmpRowObj.attr(’name’);
  var tmpObjID   = tmpRowObj.attr(‘id’);
 
  // compute begin-position of row-number.
  var pos = tmpObjID.lastIndexOf(‘_’);
  if(pos <= 0)
    return;
 
  var row = tmpObjID.substring(pos+1, tmpObjID.length);
  var rowNum = null;
  try {
    rowNum = parseInt(row);
  } catch (E) {
    rowNum = 1;
  }

  var selectTag = $(‘#f06_’+row);
  var selectTagVal = selectTag.val();

Refresh-Flag einführen

Der folgende Code ist vielleicht auf den ersten Blick etwas verwirrend. Zum besseren Verständnis erstmal die Problematik, die wir damit lösen wollen.
Solange der Anwender später immer zuerst das Department und dann den Manager auswählt, haben wir kein Problem. Nun kann es aber sein, dass der Anwender OHNE zuvor das Department zu wechseln, direkt auf die Manager-Liste klickt. Beim ersten Mal stehen hier noch alle Werte drin, da diese noch nicht aktualisiert wurde. Später implementieren wir auf der MGR-Liste das Event “onFocus”, was diese Liste beim ersten Klick refreshed. Der folgende Code dient nun dazu zu verhindern, dass jedes Mal, wenn man auf die MGR-Liste klickt, der Server-Call ausgeführt wird. Beim ersten Mal reicht völlig aus.
Am besten wäre es, wenn man die Aktualisierung direkt beim Laden der Page durchführt. Nur leider sehe ich derzeit keine einfache Lösung, um dies auch so umzusetzen, dass es funktioniert. Vielleicht stellt Oracle irgendwann einmal Mechanismen zur Verfügung, mit denen man so etwas machen kann (ähnlich dem POST-QUERY-TRIGGER in Oracle Forms).

  var mgrRefreshFlag = $(‘#hMgrRefreshFlag_’+row);
  var mgrRefreshFlagVal = mgrRefreshFlag.text();
  // if refresh-flag exists …
  if(mgrRefreshFlagVal!=undefined && mgrRefreshFlagVal!=null && mgrRefreshFlagVal!=””) {
    // EXISTS Path: check if the list had been refreshed before.
    if(mgrRefreshFlagVal==’true’) {
      if(tmpObjName == “f05”) {
        null;
      } else {
        $(‘#f06_’+row).val(selectTagVal);
        return;
      }
    } else {
      // EXISTS Path: if it not had been refreshed before, set value to “true” -> refreshed for this row!
      $(‘#hMgrRefreshFlag_’+row).text(‘true’);
    }
  } else {
    // NOT-EXISTS Path: if not exists, create this help-item and set the value to “true”.
    tmpRowObj.after(‘<span id=”hMgrRefreshFlag_’+row+'” name=”hMgrRefreshFlag” style=”display:none;”>true</span/>’);
  }

Hier wird nun der Referenzwert, also die DEPTNO, aus der entspr. Zeile ermittelt und in das Hilfs-Item geschrieben. 
BEACHTE! Das Hilfs-Item haben wir im On-Demand Prozess angegeben.

  var deptObj  = $x(‘f05_’+row);
  var deptObjVal  = deptObj.value;
  var lovRefID = deptObjVal;
  $(‘#P6_DEPTNO_REFVAL’).val(lovRefID);

Nun machen wir den AJAX-Aufruf, um die Aktualisierte Liste zu erhalten und speichern die Response, also unseren JSON-String, in “gReturn”.

  var get = new htmldb_Get(null, $v(‘pFlowId’), ‘APPLICATION_PROCESS=GET_CASCADING_LOV_JSON’,$v(‘pFlowStepId’));
 
  get.add(‘P6_DEPTNO_REFVAL’,lovRefID);
 
  var gReturn = get.get();

Der folgende Aufruf splitet nun den Return-String auf und erstellt ein neues JS-Objekt mit einem JS-Array. Darüber können wir dann auf die neuen LOV-Werte zugreifen.

  var myLovObjects = $u_eval(‘(‘ + gReturn + ‘)’); 

Nun leeren wir die MGR-Liste (also das SELECT-Tag, indem wir alle OPTIONS entfernen) und erstellen Sie komplett neu. Dazu iterieren wir über alle LOV-Werte aus “myLovObjects” und erstellen zu jedem ein neues OPTION-Tag.
Zusätzlich prüfen wir noch, ob die zuvor enthaltene Manager-ID im Array vorhanden ist und, wenn ja, setzen wir diese als “selected-option”. Wenn die Manager-ID nicht enthalten war, fügen wir nach der Iteration noch eine zusätzliche Option ein, die nur die nicht-enthaltene ID darstellt. Das ist jedoch Geschmacksache und kann verändert werden.

  selectTag.empty();
  selectTag.append(‘<option value=””>&nbsp;</option>’);

  var empnoFoundInList=false;
  for(i=0; i<myLovObjects.row.length;i++) {
    var tmpEmpno = myLovObjects.row[i].EMPNO;
    if(tmpEmpno == selectTagVal) {
      selectTag.append(‘<option selected=”selected” value=”‘+tmpEmpno+'”>’+myLovObjects.row[i].ENAME+'</option>’);
      empnoFoundInList = true;
    } else {
      selectTag.append(‘<option value=”‘+tmpEmpno+'”>’+myLovObjects.row[i].ENAME+'</option>’);
    }
  }
 
  if(!empnoFoundInList) {
    selectTag.append(‘<option selected=”selected” value=”‘+selectTagVal+'”>’+selectTagVal+'</option>’);
  }

Als letztes wollen wir noch, dass der Ursprungswert, wenn er in der neuen Liste enthalten ist, auch wieder selektiert wird. Hier könnte man
aber auch einfach die Liste auf den NULL-Wert setzen -> das ist auch Geschmacksache.

  var tmpF06=$(‘#f06_’+row);
  tmpF06.val(selectTagVal);
  var tmp2=$(‘#f06_’+row).val();

Nachdem wir nun den JS-Code angepasst haben, kopieren wir diesen nun einfach in die Page
    => Page Attributes aufrufen über Edit Page Attributes und nach unten scrollen bis “Function and Global Variable Declaration”
    => hier kopieren wir nun den zuvor angepassten JS-Code rein und klicken auf “Apply Changes”.

4: Javascript Funktionsaufrufe integrieren
Im Report müssen wir nun bei den spalten DEPTNO und MGR jeweils eine JS-Aufruf machen. Dazu die Report-Attributes aufrufen und

  • bei Spalte DEPTNO unter “Column Attributes” -> “Element Attributes” folgende onChange Funktion einbauen:
    • onchange=”javascript:refresh_cascading_lov(this);”
  • bei Spalte MGR unter “Column Attributes” -> “Element Attributes” folgende onChange Funktion einbauen:
    • onfocus=”javascript:refresh_cascading_lov(this);”

Erklärung:

  1. Der Aufruf bei DEPTNO sorgt dafür, dass die LOV auf der Spalte MGR aktualisiert wird, wenn sich die DEPTNO ändert.
  2. Der Aufruf bei MGR sorgt dafür, dass wenn man auf die select-list bei MGR klickt, ohne dass zuvor die DEPTNO geändert wurde, die aktuelle LOV entspr. der angezeigten DEPTNO aufgerufen wird.

HINWEIS: Dieser Aufruf sollte eigentlich beim onload-Event für jede Zeile stattfinden. Leider kenne ich derzeit keine Möglichkeit, wie ich APEX dazu bewegen kann, ohne die Tabular Form komplett von Hand zu rendern.

Viel Glück!

Ein generisches Datenmodell für Organigramme

Als IT-Berater waren Volker und ich viele Jahre auch im Bereich Ressourcen- und Personalplanung tätig. Auf diesen Erfahrungen haben wir ein generisches Datenmodell für Organigramme entwickelt, das gerade im Bezug zur Ressourceplanung sehr gut funktioniert.

Im Folgenden stelle ich die grundlegenden technischen Anforderungen für das Datenmodell vor. Die fachlichen Anforderungen dahinter sollten direkt einleuchten, in Einzelfällen weise ich explizit darauf hin.

Ziel ist es ein hierarchisches Organigramm so flexibel wir möglich abzubilden, um die jeweiligen Organisationseinheiten und deren Hierarchien als Grundlage für die Klammerung von Personal, Ressourcen und Berechtigungen zu verwenden.

  • O1: Eine Organisation wird in Organisationseinheiten untergliedert, im Folgenden auch Personengruppen.
  • O2 a: Diese Personengruppen können in beliebig verschachtelten Hierarchien angeordnet sein, müssen es aber nicht
    • z.B. Firma Softcom → Hauptabteilung IT-Services → Abteilung Hosting → Unterabteilung Administration → Gruppe DBA
  • O2 b: Eine Person kann mehreren Gruppenhierarchien angehören. Insbesondere sind auch Querschnittsgruppen erlaubt.
    • z.B. ein abteilungsübergreifendes Projekt
  • O2 c: Dasselbe gilt für Personengruppen

    Die Anforderungen O2 a – O2 c verdeutlichen die fachlichen Hintergründe, lassen sich aber zu einer Anforderung O2 zusammenfassen:

  • O2: Personen und Personengruppen sollen wie „Emailverteiler“ strukturiert sein
    • Eine Person oder eine Personengruppe kann also mehreren Personengruppen angehören.
    • Dies ermöglicht auch hierarchische Strukturen, ist aber wesentlich flexibler.
    • Die Struktur kann über eine einfache Intersection Table von Personengruppen / Personen zu Personengruppen implementiert werden. Es empfiehlt sich Hierarchische Views für diese Struktur anzulegen.
  • R1: Ressourcen werden auch den Organisationseinheiten zugeordnet (in unserem Sprachgebrauch also den Personengruppen).
  • E1: Ein Ereignis kann einer Person zugeordnet sein (z.B. ein „Dienst“).
  • E2: Ein Ereignis kann auch einer Ressource zugeordnet sein.
  • E3: Ein Ereignis hat ein Start- und ein Ende-Zeitpunkt.
  • E4: Ein Ereignis kann ein Mutterereignis besitzen
    • Dadurch kann u.a. eine beliebig detaillierte Projektplanung implementiert werden: Für das Ereignis „IT-Messe“ an den Tagen 1 und 2 werden z.B. die Personen „Meier“ für Tag 1 und 2, „Müller“ für Tag 2 und die Ressourcen „PC“ und „Beamer“ für Tag 1 und 2 geplant.
  • E5: Ein Ereignis besitzt immer einen Ereignistypen
    • Ereignistypen können z.B. Dienstarten in der Personalplanung sein (z.B. Urlaub, Bereitschaft, Arbeitszeit).
  • E6: Ereignistypen werden auch den Organisationseinheiten zugeordnet (in unserem Sprachgebrauch also den Personengruppen). Dabei stehen Ereignistypen übergeordneter Gruppen in der Regel auch den untergeordneten Gruppen zur Verfügung (s.u.).
    • Beispiel: Zur Gruppe DBA gehört die spezielle Dienstart „Bereitschaft DBA“

       

      Die volle Flexibilität zeigt sich erst durch geeigneten Berechtigungen:

  • B1: Lese- und Schreibrechte werden hierarchisch gemäß den Personengruppen vergeben und vererbt
    • Im oberen Beispiel etwa: Schreibrechte für Ereignisse der Hauptabteilung IT-Services beinhalten auch immer Schreibrechte auf die jeweiligen Abteilungen, Unterabteilungen und Gruppen.
    • Die Granularität „Personengruppe“ ist hier wesentlich: So werden Rechte nicht für jede Ressource einzeln festgelegt. Durch die Zuordnung einer neuen Ressource zur Personengruppe, werden automatisch die Berechtigungen der anderen Ressourcen der Gruppe übernommen.
  • B2: Die Anwendergruppen und deren Berechtigungen können analog zu den Organisationseinheiten / Personengruppen strukturiert sein, müssen es aber nicht.
    • Dies muss näher erläutert werden: In der Regel werden die hinterlegten Personen des Organigramms auch Anwender des Systems sein. In der Unterabteilung Administration kann es eine Gruppe „Disponenten“ geben, die die gesamte Personal- und Ressourcenplanung für die ganze Abteilung Hosting durchführt. Den Disponenten können also Planungsrechte für alle Ereignisse der übergeordneten Abteilung Hosting vergeben werden, obwohl sie laut Organigramm nur einer Untergruppe der Abteilung zugeordnet sind.
    • Dadurch lässt sich etwa auch folgendes Konstrukt abbilden: Der Spediteur „Lieferschmidt AG“ hat alle LKWs in die „Lieferschmidt Ressourcen GmbH“ outgesourct. Die Gruppe „Disponenten“ in der Lieferschmidt AG haben trotzdem die Berechtigungen LKWs aus der GmbH für die Touren der AG-Mitarbeiter zu planen.
  • B3 a: Die Berechtigung bestimmte Ereignistypen zuzuweisen wird in der Regel in umgekehrte Richtung zu den Gruppenhierarchien vererbt.
    • Allgemeine Dienstarten wie „Arbeitszeit“, „Urlaub“ oder „Mutterschutz“ können also übergeordnet Firma Softcom zugeordnet werden. Die verschiedenen Disponentengruppen der einzelnen Abteilungen können alle diese Dienstarten vergeben, zusätzlich zu den speziellen Dienstarten den Untergruppen.
  • B3 b: Im Gegensatz dazu kann die Berechtigung bestimmte Ereignistypen zuzuweisen auch auf die direkt zugeordnete Gruppe eingeschränkt werden.
    • Disponenten dieser Personengruppe können also nur die Ereignistypen ihrer Disponenten-Gruppe zuweisen.
  • B4: Wurde ein Ereignis angelegt, ist die Gruppe des Ereignistypen maßgeblich für die Berechtigungen zur weiteren Bearbeitung
    • Hat ein Messeplaner einen Mitarbeiter der Gruppe „DBA“ für das abteilungsübergreifende Projekt „IT-Messe“ mit der Dienstart „Präsentation Messe“ eingeplant, kann nur ein anderer Messeplaner dies ändern. Ein Disponent der Gruppe DBA hat keine Schreibrechte auf den Dienst.
    • Damit solche Messeplaner dadurch keine Schreibrechte auf übergeordnete Dienste wie Urlaub erhalten, erhalten sie nur die eingeschränkten Rechte aus B3 b.
    • Dadurch werden die Berechtigungen auch von den Bewegungsdaten entkoppelt. Kommt es zu einer Neustrukturierung der Organisation, müssen nur die Stammdaten geändert werden, im besten Fall sogar nur die Bezeichnung der Personengruppen und die Hierarchien.

Für eine umfassende Personal- und Ressourcen-Planung gehören natürlich noch eine Vielzahl weiterer Anforderungen. Themen wie „Workflows“, „Schicht-Plan“ oder „Serienereignisse“ habe ich weggelassen. Ziel war es lediglich das Konzept der Personengruppen zu erläutern und wie man es exemplarisch in der Ressourceplanung einsetzen kann, um möglichst generisch Berechtigungen zu vergeben.

Es empfiehlt sich übrigens für den Zweck weitere Views anzulegen, die die Hierarchien auflösen, um z.B. Aspekte wie den folgenden zu kapseln: Für welche Personengruppen hat Mitarbeiter Meier Schreibrechte auf Dienste?

Man sollte darauf achten, dass das hierarchische Konzept mit Personengruppen nicht dazu mißbraucht wird, die relationale Idee auszuhebeln: Letztendlich könnten alle Personenattribute durch Gruppen wie „Frauen“ oder „Männer“ ersetzt werden

Kategorien:Allgemein

Automatisch refreshen nach Auswahl in Popup-LOV

Problembeschreibung

Ich möchte in APEX eine Popup-LOV benutzen, um eine Auswahlliste zu implementieren. Nach der Auswahl in der Popup-LOV soll abhängig von der getroffenen Auswahl eine Region automatisch refreshed werden.

Eigentlich klingt das super einfach! Schon tausend mal gemacht.

Man nimmt eine dynamic Action, hängt sie an ein Change-Event und macht einen Partial-Page-Refresh.

Tja, schade nur, dass das Change-Event nicht zündet.

Bis APEX 4.2 klappt das so nicht. ACHTUNG! Mit APEX 5 haben sie es endlich in den Griff bekommen.

Das Problem hierbei ist, dass das zugrunde liegende Item deaktiviert ist und somit keine Event zünden. Ich bin mir auch nicht sicher, woran es liegt. Eigentlich dachte ich, dass die HTML Spezifikation Schuld daran wäre, aber seitdem ich gesehen habe, dass es mit APEX 5 klappt, denke ich, liegt es wohl doch eher an APEX.

Anyway…

Jedenfall benötigen wir deshalb bis APEX 4.2 einen Workaround. Natürlich gibt es mehrere Wege, wie man einen Workaround hierfür aufsetzen kann. Eine Variante, die mir am einfachsten erscheint, habe ich implementiert und hier im folgenden beschrieben.

Konzept

Die Idee hinter dieser Lösung ist, dass man sich in das on-click Event der Popup-LOV einklingt, die existierende Funktionalität überschreibt bzs. erweitert und die Dynamic-Action im Opener-Window zum refreshen explizit aufruft.

Dabei ist jedoch zu beachten, dass das Change-Event hierbei trotzdem nicht zündet. Wir müssen hier inen kleinen Trick anwenden, damit es klappt – nämlich das Item vor dem refreshen per JQuery aktivieren und anschl. wieder deaktivieren.

 

Lösung

Zuerst erstellen wir im Formular eine JavaScript Funktion mit dem Namen “doRefreshForPopupLov”. Diese Funktion soll später unsere Region explizit refreshen. Der Code sieht in etwa so aus:

function doRefreshForPopupLov() {
  // zuerst enablen wir das Item, in dem wir die Eigenschaft "disabled" entfernen":
  $("#P10_USR_ID").removeAttr('disabled');
  // dann refreshen wir über die dynamic-action mit Hilfe des Event-Triggers:
  apex.event.trigger($("#P10_USR_ID"),"change","");
  // und nun disablen wir das Item wieder, um den Ausgangszustand zu erhalten:
  $("#P10_USR_ID").attr('disabled', 'disabled');
}

Nun müssen wir leider etwas machen, was mir persönlich gar nicht gefällt: wir müssen das Popup-LOV Template ändern (kopieren geht bei diesem Template-Type leider nicht!) und müssen die APEX-Methode “passBack” hacken bzw. neu implementieren.

Hinweis:
Da es bei verschiendenen APEX Versionen theoretisch unterschiedliche Implementierungen geben kann, sollte die “passBack” Methode immer aus der jeweils genutzen Version kopiert werden. Dazu die Popup-LOV aufrufen → rechte Maustaste → Quelltext anzeigen → suchen nach “passBack” → Quellcode der Funktion “passBack” kopieren.

Da dieses Template leider nicht bei den verwendeten Page-Templates angezeigt wird, müssen wir es aufrufen über “Shared Components → Templates → Popup LOV”.
Hier implementieren wir nun unter “Page HTML Head” ein paar benötigte Javascript Funktionen.

ACHTUNG! Die Kopie der passBack Methode, die wie oben beschrieben wurde, kopiert ist, hier nun einfügen und umbenennen in passBackWithRefreh. Dann einfach nun die folgende Zeile an entspr. Stelle einbauen.

// NIDa-Spezifisch:
opener.doRefreshForPopupLov();

Hier nun der gesamte Quelltext für die Popup-LOV:

<script language="JavaScript">
<!--
 
// Neue APEX passBack Function mit eigener Refresh-Erweiterung
function passBackWithRefresh(pReturn, pDisplay){
  var lDisplayField = opener.$x("P10_USR_ID");
  try {
  opener.$s(lDisplayField, pReturn, pDisplay);
  } catch(e) {}
 
  // NIDa-Spezifisch:
  opener.doRefreshForPopupLov();
 
  window.close();
  if(!(lDisplayField.disabled || lDisplayField.type == 'HIDDEN')){lDisplayField.focus();}
};
 
// Die neue "passBack"-Funktion wird nun hierüber an alle Anchor-Tags der LOV-Return-Links angeheftet.
$(function(){
    $('.lovLinks a').each(function(){
        $(this).attr('href', $(this).attr('href').replace('passBack', 'passBackWithRefresh'));
    });
});
 
-->
</script>

Nun sollte alles funktionieren.

Kategorien:APEX 3, APEX 4

APEX 5 auf Oracle 12c installieren

Betreff:

  • APEX 5.0
  • Oracle 12c R1
  • ORDS 3.0.1

Seit einiger Zeit ist die Oracle 12c Datenbank und mittlerweile auch das lang ersehnte APEX 5 verfügbar, was uns natürlich wieder vor neue Herausforderungen stellt. Die Installation einer Oracle 12c war noch recht einfach. Also sollte APEX 5 auch keine großen Probleme verursachen – dachte ich, weil in der Vergangenheit eine APEX Installation, egal ob Neuinstallation oder Upgrade, immer total easy und klappte ohne große Probleme. So ging ich sehr optimistisch und naiv an die APEX 5 Installation – APEX 4.2 war ja bereits per Default in der 12c enthalten – sollte also kein Problem darstellen.

F A L S C H !!!

Leider musste ich feststellen, dass APEX 5 und alles, was damit zu tun hat, nicht gut funktioniert. Es hat mich mehrere Tage gekostet herauszufinden, wo die einzelnen Probleme liegen und wie ich sie umgehen kann.

An dieser Stelle ein fettes Sorry für diese pauschale Aussage, aber es gibt einfach zu viele Dinge, die derzeit nicht funktionieren. Angefangen

  • mit den neuen Perl Wrapper-Skripten, die bei mir unter Windows 7 leider ständig abgebrochen sind,
  • über einen APEX 5-Bug der das Upgrade auf die 4.2 verhindert (20381781 – Error occurs when upgrading apex 4.2.5.00.08 in 12.1.0.2 DB to APEX 5.0),
  • und die APEX 4.2 Deinstallation, die leider auch nicht reibungslos funktionierte
  • bis hin zu diversen Problemen mit dem neuen APEX Listener, genannt ORDS (Oracle REST Data Services)

Aus all diesen Gründen habe ich mich entschlossen, hier eine Anleitung zu geben, wie man die bekannten Probleme umgeht und möglichst schnell zum Ziel kommt und eine lauffähige APEX 5 Umgebung auf einer 12c aufbauen kann.

Anmerkung: Mir ist sehr wohl bewusst, dass es sicherlich noch andere Wege gibt, die evtl. vielleicht sogar deutlich besser sind. Aber wenn man Tage hinter sich gebracht hat, in denen man unsinnige Fehleranalysen durchführen musste, hat man keine Lust mehr alles wieder rückgängig zu machen und nach “dem richtigen Weg” zu suchen.

Zudem werden wohl (hoffentlich) in neueren Versionen von APEX 5 und ORDS diese Fehler beseitigt sein und somit meine Beschreibung hier überflüssig werden.

Konzept

Da wir hier über Oracle 12c, also die neue “multitenant Database” sprechen, und es hier mehrere Möglichkeiten gibt, APEX zu installieren, möchte ich vorher kurz das Konzept erläutern, was der folgenden Installtionsanleitung zugrunde liegt.

Ich habe versucht, den “recommended way” umzusetzen. Das beudetet, dass APEX 5 im Root Container (CDB) installiert und darüber auf alle PDBs verteilt wird.

Darüber hinaus möchte ich einen APEX Listener verwenden und diesen in einer Tomcat-Instanz bereitstellen.

 

Deinstallation APEX 4.2

Wegen des Bugs 20381781 – Error occurs when upgrading apex 4.2.5.00.08 in 12.1.0.2 DB to APEX 5.0, über den ich natürlich auch gestolpert bin, muss derzeit die per Default in Oracle 12c vorhandene APEX 4.2 deinstalliert werden. Sollte bei Ihnen kein APEX installiert sein, können Sie diesen Punkt natürlich überspringen. Prüfen können Sie das, indem die DB-User abgefragt werden.

SELECT username FROM dba_users WHERE username LIKE ‘APEX%’ OR username LIKE ‘FLOWS%’;

Zur Deinstallation wechseln Sie in das Verzeichnis %ORACLE_HOME%\apex, rufen SQLPLUS als SYSDBA auf und starten das Skript “apxremov.sql”, wobei wir hier unbedingt noch einen DB-Parameter umsetzen müssen, damit alles sauber funktioniert (siehe dazu auch ORA-28014: Cannot drop administrative users)

sqlplus / as sysdba

ALTER SESSION SET “_oracle_script” = TRUE;

@apxremov

Anmerkung:

Eigentlich sollte das so funktionieren. Wenn nicht, kann man APEX 4.2 auch manuell deinstallieren (siehe APEX 4.2 auf Oracle 12c manuell deinstallieren).

Neuinstallation APEX 5

Nun installieren wir APEX 5 in einer APEX-freien Oracle 12c Datenbank.

sqlplus / as sysdba (in CDB)
@apexins.sql SYSAUX SYSAUX TEMP /i/
— ACHTUNG Fehler:
—    catcon: See apexins_cdb_*.lst files for spool files, if any
—    next_proc: total processes (8) != number of live processes (7); giving up
—    Use of uninitialized value $ProcId in concatenation (.) or string at D:\Installationen\Oracle\product\12.1.0\dbhome_1/rdbms/admin/catcon.pm line 1779.
—    Use of uninitialized value $ProcId in concatenation (.) or string at D:\Installationen\Oracle\product\12.1.0\dbhome_1/rdbms/admin/catcon.pm line 1779.
—    A process terminated prior to completion.
—    Review the apexins_cdb*.log files to identify the failure
—        => Died at D:\Installationen\Oracle\product\12.1.0\dbhome_1/rdbms/admin/catcon.pm line 6149.

—    Internetsuchen deuten darauf hin, dass hier evtl. ein problem mit Perl vorhanden ist.
—    https://rt.cpan.org/Public/Bug/Display.html?id=17773

 

— Prüfen APEX-User in CDB
SELECT username
FROM dba_users
WHERE username like ‘APEX%’ or username like ‘ORDS%’ or username like ‘FLOWS%’
ORDER BY username;

— Prüfen APEX-User in PDB
ALTER SESSION SET CONTAINER = pdbora12c;

SELECT username
FROM dba_users
WHERE username LIKE ‘APEX%’ OR username like ‘ORDS%’ OR username like ‘FLOWS%’
ORDER BY username;

 

— Change Admin-Password (über CDB)
ALTER SESSION SET CONTAINER = cdb$root;
@apxchpwd.sql

— !!!!!!!!!!!!!!!!!!!!!!!

— !!! ACHTUNG !!!

— Das muss in PDB explizit gestartet werden.

— !!!!!!!!!!!!!!!!!!!!!!!

ALTER SESSION SET CONTAINER = pdbora12c;
@apxchpwd.sql

 

— APEX_PUPBLIC_USER vorbereiten (in CDB)
ALTER SESSION SET CONTAINER = cdb$root;
ALTER USER APEX_PUBLIC_USER ACCOUNT UNLOCK;
ALTER USER APEX_PUBLIC_USER IDENTIFIED BY oracle;

 

— Prüfen, ob Rest-User existieren
SELECT username
FROM dba_users
WHERE username in (‘APEX_LISTENER’, ‘APEX_REST_PUBLIC_USER’)
ORDER BY username;

Sollten die Listener-User nicht existieren, dann müssen diese installiert werden, bevor ORDS installiert wird.

 

— SQLPLUS als SYSDBA aufrufen (CDB)

sqlplus / as sysdba (in CDB)

Da auch dieses Skript: “@apex_rest_config.sql” mit folgendem Fehler abgebrochen ist:

— ‘Enter: GetConsoleMode failed, LastError=|6| at D:/Installationen/Oracle/product/12.1.0/dbhome_1/perl/site/lib/Term/ReadKey.pm line 277.

habe ich auch das von Hand ausgeführt (naja, zumindest halb-manuell):

— SQLPLUS als SYSDBA (CDB)

sqlplus / as sysdba

— Um wieder die Fehlermeldung “ORA-65096: invalid common user or role name ” zu unterbinden, folgendes Statement absetzen:

ALTER SESSION SET “_ORACLE_SCRIPT” = TRUE;

— nun das innerste Skript verwenden:

@apex_rest_config_core.sql

 

— und nun das Gleiche noch in der PDB machen

ALTER SESSION SET CONTAINER = pdbora12c;

@apex_rest_config_core

— Den Parameter _ORACLE_SCRIPT brauchen wir hier nicht zu setzen. Offensichtlich dürfen wir in den PDB machen, was wir wollen.

 

Installation und Konfiguration Oracle REST Data Services (ORDS 3.0.1)

Nachdem wir nun APEX 5 erfolgreich installiert haben, installieren und konfigurieren wir noch die ORDS.

Anmerkung!

Wenn wir das PLSQL Gateway verwenden möchten, brauchen wir ORDS natürlich nicht zu installieren. In diesem Fall einfach das PLSQL Gateway konfigurieren (mit @apex_epg_config.sql gegen die PDB) und dann sollte es auch schon funktionieren.

Zuerst entpacken wir das ZIP-File mit ORDS 3.0.1 und verschieben es an einen Ort, wo es dauerhaft liegen bleiben kann. Ich persönlich finde, dass das ORACLE_HOME ein guter Ort ist. Aber das ist Geschmacksache.

Nun wechseln wir in diesen ORDS-Ordner. Eigentlich reicht es aus, wenn wir einen simplen Java-Aufruf machen, den wir auch später benutzen können, um ORDS im Standalone-Modus zu starten. Aber um bewusst zu machen, dass wir explizit installieren und konfigurieren möchten, hier der vollständige Aufruf:

java -jar ords.war install advanced

Durch diesen Aufruf werden nun z.B. ein Konfig-Verzeichnis, in dem die Konfigs abgelegt werden, abgefragt und natürlich auch alle DB-Connect Infos und einiges mehr. Bitte hier sorgfältig vorgehen, da sonst eine wahrscheinlich ziemlich umfangreiche Fehlersuche ansteht. Dazu aber später noch einiges mehr.

Hinweis: Wenn man die Konfiguration einmal durchgeführt hat, aber noch mal durchführen will (weil man z.B. einen Fehler gemacht hat oder etwas ändern möchte), funktioniert es mit diesem Aufruf leider nicht mehr (liebe Oracle-Developer, was habt ihr euch dabei gedacht?). Anstatt im Konfig-Modus zu starten wird direkt der Standalone-Modus ausgeführt und eine Konfiguration ist nicht mehr möglich (hierzu bitte weiter unten unter “Weitere Tipps und Tricks zu ORDS” nachlesen, wie man wieder in den Konfig-Modus kommt).

Hinweis: Bei mir hat die DB-seitige Installation der ORDS-Komponenten aus dem normalen Installationsprozess nicht funktioniert, da der Connect als SYSDBA schief gegangen ist. In diesem Fall können die Komponenten manuell installiert werden. Siehe dazu weiter unten “Weitere Tipps und Tricks zu ORDS”.

WICHTIG!

Bitte prüfen Sie nun unbedingt, ob im ORDS Konfig-Ordner unter “.\ords\conf” eine Datei mit dem Namen “apex.xml” existiert. Wenn nicht kopieren Sie bitte direkt die Datei “apex_pu.xml” nach “apex.xml” (ohne diese Datei läuft nichts!) (siehe auch ORDS 3 404 Error – unbedingt auch Kommentar von “krissco”, vom 14.05.2015, 10:41 beachten).

 

Jetzt müssen wir noch die APEX-Images als WAR-File bereitstellen, damit wir diese im Tomcat deployen können. Dazu:

— hier wird nun das War-file benutzt, um ein neues WAR-File für den Java-Container zu erzeugen.

— Aufruf: java -jar ords.war static <Image Context-Path> <Pfad zu dem Ort, an dem meine APEX-Images liegen.

java -jar ords.war static /i /ora01/product/db12c/apex/images

Hieraus entsteht eine Datei mit dem Namen “i.war”. Diese Datei deployen wir nun im Tomcat. Da es hierzu eine Benutzeroberfläche gibt, spare ich mir die genauen Anweisungen.

Wichtiger Hinweis:

Hier sollte nun im CATALINA_HOME des Tomcats unter “.\webapps\i” alle APEX-Images auftauchen. Leider hat das bei mir unter Windows nicht funktioniert. 

Für den Fall, dass es bei Ihnen ebenfalls schief gelaufen ist, hier ein Workaround:

Prüfen Sie, ob im CATALINA_HOME unter “.\webapps” order “i” existiert. Wenn ja, kopieren Sie die APEX_Images mit allen Unterordnern einfach in diesen i-Ordner.

 

So, nun sollte alles funktionieren. Also Browser öffnen, URL eingeben (ACHTUNG, der virtuelle Pfad heißt nun “ords”, nicht mehr “apex”) und testen. Wenn man “locahost” als Servernamen eingibt und es klappt nicht, unbedingt auch mal mit dem Rechnernamen versuchen. Mit Localhost hatte ich auch meine Probleme!!!

Wenn es trotzdem nicht funktionieren sollte, hier noch einige Tipps und (hoffentlich) nützliche Anmerkungen.

Weitere Tipps und Tricks zu ORDS

 

Hilfe bzw. Parameterliste aufrufen

Um eine Liste aller möglichen Aufrufparameter zu bekommen, einfach folgenden Befehl in Kommandozeile absetzen:

java -jar ords.war help

und benötigt man weitere Informationen zu den Aufrufparametern kann man den Hilfebefehl erweitern. Z.B.:

java -jar ords.war help standalone

java -jar ords.war help setup

 

ORDS-Skripte fehlen bzw. wo finde ich die ORDS-Skripte

Die ORDS-Skripte können ganz einfach aus dem mitgelieferten WAR-File extrahiert werden.

java -jar ords.war ords-scripts

Durch diesen Befehl werden alle notwendigen DB-Skripte des ORDS extrahiert und in einem Unterordner “ords_scripts” abgelegt.

Debugging

Im ORDS kann das Debugging aktiviert werden, wodurch zusätzliche Meldungen im Startfenster des Tomcat bzw. des Standalone-Listeners ausgegeben werden. Dazu:

In den ords-3.0.1 Konfig-Ordner wechseln und und die Datei .\ords\defaults.xml öffnen.

Nach folgenden Einträgen suchen und die Werte auf “true” setzen:

<entry key=”debug.debugger”>false</entry>
<entry key=”debug.printDebugToScreen”>false</entry>

Anmerkung: In den Produktions-Instanzen das Debugging unbedingt wieder ausschalten, da die Performance dadurch deutlich verschlechtert wird.

 

Manuelle Installation der ORDS DB-Komponenten

Wenn die Installation der DB-seitigen ORDS-Komponenten schief geht, gibt es die Möglichkeit, diese manuell zu installieren. Aber Vorsicht! Diese Komponenten müssen nun in der/den PDB(s) installiert werden.

cd ords_scripts\scripts\install\core

sqlplus / as sysdba

ALTER SESSION SET CONTAINER = pdbora12c;

— Aufruf: ords_manual_install.sql <Inst-Tablespace> <Temp-Tablespace> <log-Verzeichnis>

@ords_manual_install.sql SYSAUX TEMP d:\…\ords-3.0.1\log

 

Erneute Konfiguration des ORDS durchführen

Wenn während der Installation oder Konfiguration des ORDS etwas schief gelaufen ist, kann es notwendig werden, die Aktion erneut auszuführen. Leider führt ein erneuter Aufruf von

java -jar ords.war install advanced

nicht zur Konfiguration, sondern startet den ORDS im Standalone-Modus mit der vorhandenen Konfiguration.

Sollte der Fehler an der DB-seitigen Installation liegen, bitte einfach die manuelle Installation der DB-Komponenten durchführen, wie oben beschrieben. Ist allerdings etwas bei der System-seitigen Installation schief gelaufen, haben wir 2 Möglichkeiten.

  1. Konfiguration vorher löschen
    • Tomcat oder Standalone Instanz stoppen
    • ORDS-Konfigurations-Verzeichnis löschen
    • Aufruf: java -jar ords.war install
  2. Konfiguration explizit aufrufen (empfohlen)
    • Tomcat oder Standalone Instanz stoppen
    • Aufruf: java -jar ords.war setup
    • => Durch diesen Aufruf wird eine erneute Konfiguration ohne Installation erzwungen.

 

Quellen

Danksagung

An dieser Stelle und nach diesen ganzen Problemen muss ich mich noch mal ganz herzlichen bei der APEX Community bedanken, ohne die es noch viel länger gedauert hätte.

Liebe APEX Gemeinde, danke, danke, danke!

APEX 4.2 auf Oracle 12c manuell deinstallieren

September 4, 2015 1 Kommentar

Bei meinen ersten “Gehversuchen” mit Oracle 12c und einer APEX 5 Installation kam ich leider in den “Genuss”, die von 12c mitgebrachte APEX 4.2 Installation deinstallieren zu müssen. Dabei lief ich leider auf diverse Schwierigkeiten, die verhinderten, dass der Aufruf von “@apxremov.sql” durchgelaufen ist. Nach diversen Versuchen habe ich den diesen Versuch aufgegeben und APEX 4.2 von komplett manuell deinstalliert.

Tipp: Das Skript “apxremov.sql” ruft in der 4.2 2 weitere Skripte auf. Ich habe mir einfach diese Skript angeschaut und die wichtigen Anweisungen rauskopiert und manuell ausgeführt.

Um das Ganze aber zu vereinfachen, hier eine Anleitung der manuellen Schritte.

Deinstallation in CDB

— sqlplus.exe aufrufen als SYSDBA in CDB connecten

sqlplus / as sysdba

— CURRENT_SCHEMA setzen: das sollte APEX_040200 sein

ALTER SESSION SET CURRENT_SCHEMA = APEX_040200;

begin
wwv_flow_upgrade.drop_public_synonyms;
end;

/

— speziellen undokumentierten Parameter setzen, um den Fehler ORA-28014 zu umgehen

ALTER SESSION SET “_oracle_script” = TRUE;

DROP USER apex_040200 CASCADE;

DROP USER flows_files CASCADE;

DROP USER apex_public_user CASCADE;

DROP ROLE apex_administrator_role;

 

— Cleanup XDB

declare
cfg XMLType;
l_dad_list dbms_epg.varchar2_table;
begin

if dbms_xdb.existsresource(‘/i/’) then
dbms_xdb.deleteresource(‘/i/’, dbms_xdb.delete_recursive_force);
end if;

if dbms_xdb.existsresource(‘/images/’) then
dbms_xdb.deleteresource(‘/images/’,dbms_xdb.delete_recursive_force);
end if;

dbms_epg.get_dad_list( l_dad_list );
for i in 1..l_dad_list.count loop
if upper(l_dad_list(i)) = ‘APEX’ then
dbms_epg.drop_dad(‘APEX’);
end if;
end loop;

cfg := dbms_xdb.cfg_get();

if cfg.existsNode(‘/xdbconfig/sysconfig/protocolconfig/httpconfig/webappconfig/servletconfig/servlet-mappings/servlet-mapping/servlet-name[text()=”PublishedContentServlet”]’) = 1 then
cfg := cfg.deleteXML(‘/xdbconfig/sysconfig/protocolconfig/httpconfig/webappconfig/servletconfig/servlet-mappings/servlet-mapping/servlet-name[text()=”PublishedContentServlet”]/..’);
end if;

if cfg.existsNode(‘/xdbconfig/sysconfig/protocolconfig/httpconfig/webappconfig/servletconfig/servlet-list/servlet/servlet-name[text()=”PublishedContentServlet”]’) = 1 then
cfg := cfg.deleteXML(‘/xdbconfig/sysconfig/protocolconfig/httpconfig/webappconfig/servletconfig/servlet-list/servlet/servlet-name[text()=”PublishedContentServlet”]/..’);
end if;

dbms_xdb.cfg_update(cfg);
commit;
dbms_xdb.cfg_refresh;

end;

/

— SYS owned Objects:

DROP PROCEDURE validate_apex;
DROP PACKAGE wwv_flow_val;
DROP PACKAGE wwv_dbms_sql;
DROP PACKAGE wwv_flow_key;
DROP LIBRARY wwv_flow_val_lib;
DROP VIEW wwv_flow_gv$session;

Deinstallation in PDB(s)

— SQLPLUS als SYSDBA aufrufen

sqlplus / as sysdba

— connecten gegen PDB (ich habe nur eine PDB und die heißt “PDBORA12C”.

ALTER SESSION SET CONTAINER=pdbora12c;

— CURRENT_SCHEMA auf APEX_040200 setzen

ALTER SESSION SET CURRENT_SCHEMA = apex_040200;

BEGIN
wwv_flow_upgrade.drop_public_synonyms;
END;

/

DROP USER apex_040200 CASCADE;
DROP USER FLOWS_FILES CASCADE;
DROP USER APEX_PUBLIC_USER CASCADE;
DROP ROLE APEX_ADMINISTRATOR_ROLE;
DROP USER APEX_LISTENER CASCADE;
DROP USER APEX_REST_PUBLIC_USER CASCADE;

— Cleanup XDB

declare
cfg XMLType;
l_dad_list dbms_epg.varchar2_table;
begin

if dbms_xdb.existsresource(‘/i/’) then
dbms_xdb.deleteresource(‘/i/’, dbms_xdb.delete_recursive_force);
end if;

if dbms_xdb.existsresource(‘/images/’) then
dbms_xdb.deleteresource(‘/images/’,dbms_xdb.delete_recursive_force);
end if;

dbms_epg.get_dad_list( l_dad_list );
for i in 1..l_dad_list.count loop
if upper(l_dad_list(i)) = ‘APEX’ then
dbms_epg.drop_dad(‘APEX’);
end if;
end loop;

cfg := dbms_xdb.cfg_get();

if cfg.existsNode(‘/xdbconfig/sysconfig/protocolconfig/httpconfig/webappconfig/servletconfig/servlet-mappings/servlet-mapping/servlet-name[text()=”PublishedContentServlet”]’) = 1 then
cfg := cfg.deleteXML(‘/xdbconfig/sysconfig/protocolconfig/httpconfig/webappconfig/servletconfig/servlet-mappings/servlet-mapping/servlet-name[text()=”PublishedContentServlet”]/..’);
end if;

if cfg.existsNode(‘/xdbconfig/sysconfig/protocolconfig/httpconfig/webappconfig/servletconfig/servlet-list/servlet/servlet-name[text()=”PublishedContentServlet”]’) = 1 then
cfg := cfg.deleteXML(‘/xdbconfig/sysconfig/protocolconfig/httpconfig/webappconfig/servletconfig/servlet-list/servlet/servlet-name[text()=”PublishedContentServlet”]/..’);
end if;

dbms_xdb.cfg_update(cfg);
commit;
dbms_xdb.cfg_refresh;

end;

/

Nun können wir noch mal prüfen, ob die APEX Schemata alle entfernt wurden:

— connecten als SYSDBA gegen CDB

sqlplus / as sysdba

show con_name

— hier sollte CDB$ROOT angezeigt werden

SELECT username FROM dba_users WHERE username LIKE ‘APEX%’ OR username LIKE ‘FLOWS%’;

— hier sollte nun nichts mehr angezeigt werden.

 

— connecten gegen PDB(s)

ALTER SESSION SET CONTAINER = pdbora12c;

show con_name

— hier sollte PDBORA12C bzw. der Name Ihrer PDB angezeigt werden.

SELECT username FROM dba_users WHERE username LIKE ‘APEX%’ OR username LIKE ‘FLOWS%’;

— hier sollte nun auch nichts mehr angezeigt werden.

Nun sollten wir eine saubere, APEX-freie Datenbank haben.

Kategorien:APEX 4, APEX 5, Oracle 12c Schlagwörter:,

ORA-28014: Cannot drop administrative users

September 4, 2015 2 Kommentare

Als ich versuchte ein Upgrade mit APEX 5 in einer Oracle 12c R1 auf APEX 4.2 durchzuführen musste ich feststellen, dass es wohl einen Bug bei der APEX 5 Installation gibt, der nur mit einem Patch oder durch das vorherige deinstallieren von APEX 4.2 behoben werden kann.

Also versuchte ich, APEX 4.2 in der 12c zu deinstallieren.

sqlplus / as sysdba

@apxremov

Hierbei ist dann dieser Fehler aufgetreten:

ORA-28014: Cannot drop administrative users

Abhilfe

Die Abhilfe ist ganz einfach. Durch setzen eines “undokumentierten” Parameters in der Datenbank kann man die Fehlermeldung abschalten:

ALTER SESSION SET “_oracle_script” = TRUE;

@apxremov

Und schon funktioniert es.

Kategorien:APEX 5, Oracle 12c Schlagwörter:, , , ,

ORA-65096: invalid common user or role name

Der Fehler ORA-65096 wird angezeigt, wenn in einer Oracle 12c Datenbank versucht wird, ein User oder Schema anzulegen, was so heißt wie ein spezieller Oracle Repository User (davon gibt es ja mittlerweile eine ganze Menge. Z.B. APEX_LISTENER, ORDS_PUBLIC_USER, usw.).

Typisches Beispiel ist, dass man ein von Oracle mitgeliefertes Installations-Skript startet, indem ein neuer User angelegt wird. Bei mir ist es während der APEX 5 Installation aufgetreten. Die Installations-Skripte, die (dummerweise) mittlerweile über Perl-Wrapper Skripte aufgerufen werden, sind abgebrochen, was mich dazu veranlasst hat, die SQL-Skripte, die ich benötigte, zu untersuchen und die Teile, die ich brauchte, von Hand aufzurufen.

Beim Anlegen des Users APEX_LISTENER in der CDB kam dann der o.g. Fehler.

 

Abhilfe

Die Abhilfe ist Gott sei Dank ganz einfach.

Es gibt einen “undokumentierten” Parameter namens _ORACLE_SCRIPT. Setzt man diesen auf TRUE, ist alles gut und die Skripte laufen durch.

ALTER SESSION SET “_ORACLE_SCRIPT” = TRUE;

Einen tabellarischen HTML-Bericht per Apex Mail versenden

April 23, 2015 1 Kommentar

Beim Betrieb einer Oracle Apex-Anwendung ist es oft notwendig tabellarische SQL-Berichte per Mail zu versenden. Dies kann eine Admin-Mail über den Zustand der Apex-Anwendung sein aber auch inhaltliche Benachrichtigungen an bestimmte Benutzerkreise oder Stakeholder der Apex-Anwendung. Gerade im Letzteren Fall sollte die Formatierung des SQL-Berichtes stimmen und das Layout ansprechend sein. Auf Nummer sicher geht man, wenn man in diesem Fall einen PDF-Bericht erzeugt und ihn im Anhang der Mail mitsendet. Dies ist allerdings auch entsprechend aufwendig.

Als einfachere Alternative bietet sich an, den SQL-Bericht als HTML-Tabelle in einer HTML-Mail per Apex Mail zu versenden.

Im Folgenden stelle ich eine generische Funktion vor, die einen beliebigen SQL-Select ausführt und die Ergebnismenge als HTML-Tabelle rendered.

Zusätzlich stelle ich ein Beispielskript vor, das die HTML-Tabelle per Apex Mail versendet. Dabei bereite ich das Layout des Berichts über mehrere allgemeine CSS-Regeln auf.

testmail

Funktion PRINT_RESULT:

Hierbei habe ich einfach die Funktion in http://www.java2s.com/Tutorial/Oracle/0601__System-Packages/Printtablewithdynamicquery.htm um die HTML-Option erweitert.

create or replace FUNCTION "PRINT_RESULT" ( p_query IN VARCHAR2, p_markup IN VARCHAR2 DEFAULT 'TEXT')
 RETURN CLOB
 AUTHID CURRENT_USER
 IS
 /**
 * Prints the result of the SQL-Statement (p_query) as TEXT-Output or HTML-Output (p_markup)
 * This function enhances the following code
 * http://www.java2s.com/Tutorial/Oracle/0601__System-Packages/Printtablewithdynamicquery.htm
 */
l_theCursor INTEGER DEFAULT dbms_sql.open_cursor;
l_columnValue VARCHAR2(32676);
l_status integer;
l_descTbl dbms_sql.desc_tab;
l_colCnt number;
l_even BOOLEAN;
l_row_css VARCHAR2(1000);
l_return_str CLOB :='';
BEGIN
DBMS_SESSION.SET_ROLE('ALL');

dbms_sql.parse(l_theCursor,p_query,dbms_sql.native);

dbms_sql.describe_columns( l_theCursor, l_colCnt, l_descTbl);

for i in 1 .. l_colCnt loop
dbms_sql.define_column(l_theCursor, i, l_columnValue, 4000);
end loop;

l_status := dbms_sql.execute(l_theCursor);

IF p_markup = 'HTML' THEN
l_return_str := l_return_str || '

'; END IF;for i in 1 .. l_colCnt loop
l_columnValue := l_descTbl(i).col_name;

IF p_markup = 'HTML' THEN
l_columnValue := ''|| l_columnValue || '

';
END IF;
l_return_str := l_return_str ||substr(l_columnValue, 0, 300);

end loop;

IF p_markup = 'HTML' THEN
l_return_str := l_return_str || '

';
ELSE
for i in 1 .. l_colCnt loop
l_return_str := l_return_str || rpad( '----', 200, '-' );
end loop;
END IF;

l_return_str := l_return_str ||CHR(10);

l_even := FALSE;
while ( dbms_sql.fetch_rows(l_theCursor) > 0 ) loop
IF p_markup = 'HTML' THEN
IF l_even THEN
l_row_css := 'style="background-color:#f5f5ff;"';
ELSE
l_row_css := '';
END IF;

l_return_str := l_return_str || '
<tr '||l_row_css||'>';
END IF;

for i in 1 .. l_colCnt loop

dbms_sql.column_value( l_theCursor, i, l_columnValue );
IF p_markup = 'HTML' THEN
l_columnValue := ''||l_columnValue || '

';
END IF;

l_return_str := l_return_str || substr(l_columnValue, 0, 300);

end loop;

IF NOT l_even THEN
l_even := TRUE;
ELSE
l_even := FALSE;
END IF;

IF p_markup = 'HTML' THEN
l_return_str := l_return_str || '

';
END IF;
l_return_str := l_return_str || CHR(10);

end loop;

IF p_markup = 'HTML' THEN
l_return_str := l_return_str || '

';
END IF;

RETURN l_return_str;
EXCEPTION
when others then dbms_sql.close_cursor( l_theCursor ); RAISE;
END;
/

Versand der Apex-Mail inkl. CSS-Regeln:

declare
    l_mail_text       CLOB;
    l_mail_text_html  CLOB;
    l_statement       VARCHAR2(32676);    
    l_mail_id         NUMBER;
    
    PROCEDURE setup_apex_mail
    IS
      l_security_group_id NUMBER;
    BEGIN
      SELECT workspace_id
      INTO    l_security_group_id
      FROM   apex_workspaces
      WHERE  workspace='YOUR_WORKSPACE'; /* Use your workspace name here */ 
  
      wwv_flow_api.set_security_group_id(l_security_group_id);
    END;    
      
    FUNCTION get_html_mail_header
      RETURN VARCHAR2
    IS
      l_home_url  VARCHAR2(1000);
    BEGIN
      l_home_url := 'intranet.home-site';
  
      RETURN '<html xmlns="http://www.w3.org/1999/xhtml">
   <head>
    <meta content="text/html; charset=utf-8" http-equiv="CONTENT-TYPE" />
    <style type="text/css">#content, body { color:#324242; font-family: Arial, sans-serif; width:60em; border:1px solid #EEE; padding:3em 5em 5em; margin:2em; } body { color:#324242; } a { color:#1F4F80; } .i
  nfo { font-size:75%; color:#324242; padding:0; } .details { background:#1F4F80 none repeat scroll 0 0; color:#CDD6DF; font-weight:normal; border:2px solid #F18900; text-align:left; } .content th { col
  or:#CDD6DF; font-weight:normal; } .details td { color:#ffffff; font-weight:bold; } .header { background:#1F4F80 none repeat scroll 0 0; border:2px solid #F18900; text-align:center; border-collapse:col
  lapse; color:#CDD6DF; } .header td { padding:4px 0; } .detailsth { background:#1F4F80; color:#4D5508; font-weight:bold; color:#ffffff; } .event { margin:auto; text-align:center; width:85%; } .statusDe
  scr { font-size:0.85em; line-height:2em; } .hide { display: none; } td {border:1px solid #999;} th {border:1px solid #99b; background-color:#dde;}  </style>
  
   </head>
   <body text="#000000" bgcolor="#ffffff">
    <div id="content">
     <a href="'||l_home_url||'">
  
      <i>Home-Site</i>
     </a>
  
    <div style="display:none">
      <br/><br/><br/>
  
                  	 to display this HTML-Mail properly  <br/>
                  	 enable **HTML-format** in your Mail-client <br/>
    </div>
  
     <br />
     <br />
     <br />
     <b>Hello ,</b>
  
     <br />
     <br />';
  
    END;
  
  
    FUNCTION get_html_mail_footer
      RETURN VARCHAR2
    IS
    BEGIN
  
      RETURN '
     <br />
     <br />
     <br />
     <b>
      <span class="firstLetter">Regards,</span>
     </b>
     <br />
     <br />
     Your ApexTipps-Team
     <br />
     <br />
      This email is being sent automatically, please do not answer it
      </div>
   </body>
  </html></div>';
  
    END;
      
    
  BEGIN
      l_mail_text := get_html_mail_header;
      
      l_mail_text := l_mail_text || 'Apex DB-Info '||'<br/>'||'<br/>'||sysdate;
      
      l_statement := 'select owner, table_name, tablespace_name, status, num_rows
                      from all_tables
                      where owner=''HR''';

      l_mail_text := l_mail_text || '<br/>' ||'<br/>' || '<strong>HR-Tables</strong> '||'<br/>'||'<br/>'||
        print_result(l_statement, 'HTML');

      l_mail_text := l_mail_text || get_html_mail_footer;
    
      setup_apex_mail;

     l_mail_id := apex_mail.send(
          p_to       => 'some.address@email.domain',   -- change to your email address
          p_from     => 'some.address@email.domain',
          p_subj     => '[Apex-Info] Query-Report',
          p_body     => l_mail_text,
          --p_body     => null,
          p_body_html=> l_mail_text);


end;
/

BEGIN
  APEX_MAIL.PUSH_QUEUE;
END;
/


Kategorien:Allgemein
Folgen

Erhalte jeden neuen Beitrag in deinen Posteingang.