Prozesslogik "im Hintergrund" mit Fortschrittsbalken

Wenn Sie in Ihrer Application Express-Anwendung einen Seiten- oder Anwendungsprozeß erstellen, so wird dieser im "Vordergrund" ausgeführt - Aus Sicht des Endbenutzers wird die nächste Anwendungsseite während dieser Zeit geladen. Für "normale" Prozesse wie Einfüge-, Änderungs- oder Löschoperationen auf Tabellen ist dies unproblematisch, da sie nur kurze Zeit in Anspruch nehmen. Nimmt die Ausführung der hinterlegten Logik jedoch längere Zeit in Anspruch, so ist es meist nicht sinnvoll, mit der Darstellung der Seite zu warten, bis die Ausführung des Prozesses abgeschlossen ist. Vielmehr sollte die nächste Seite sofort (zusammen mit einigen Status-Informationen) dargestellt werden. Wie Sie dies erreichen, erfahren Sie in dieser Ausgabe.

Als Beispiel soll die nachfolgende PL/SQL-Prozedur dienen. Um einen langlaufenden Prozeß zu simulieren, wird die Prozedur DBMS_LOCK.SLEEP verwendet. Normalerweise hat nur der Datenbankadministrator Zugriff auf dieses PL/SQL-Paket - um das Beispiel nachvollziehen zu können, benötigen Sie also EXECUTE-Privilegien auf DBMS_LOCK. Erstellen Sie die Prozedur mit dem SQL Workshop, dem SQL Developer oder SQL*Plus.

create or replace procedure lang_laufend as
begin
  for i in 1..10 loop
    DBMS_LOCK.SLEEP(6);
  end loop;
end;

Würden Sie diesen Code als normalen onSubmit- oder onLoad-Prozeß hinterlegen, müsste der Endbenutzer eine Minute auf den Seitenaufbau warten. Daher wählen wir nun die andere Strategie - die Ausführung im Hintergrund.

In der Online-Hilfe zu Application Express ist ebenfalls ein Kapitel Running Background PL/SQL vorhanden - dort ist dargestellt, wie die Aufgabe mit den Mitteln von Application-Express (APEX_PLSQL_JOB) realisiert werden kann. In diesem Beispiel wird jedoch der "Standard" für Jobs in der Datenbank (DBMS_SCHEDULER) verwendet. DBMS_SCHEDULER ist ab Oracle10g verfügbar.

Dies hat für Sie den den Vorteil, dass Sie das Wissen auch außerhalb von Application Express nutzen können - weiterhin ist DBMS_SCHEDULER wesentlich mächtiger als APEX_PLSQL_JOB. So können Sie Jobs mit DBMS_SCHEDULER zu einem bestimmten Zeitpunkt in der Zukunft oder wiederholt ausführen lassen. Darüber hinaus erfahren Sie, wie Sie Informationen über den Arbeitsstand ihres Prozesses in der Datenbank bekannt machen, so dass der Datenbankadministrator über solche Prozesse informiert ist. Zur Nutzung von DBMS_SCHEDULER wird das Systemprivileg CREATE JOB benötigt - normalerweise ist es Ihrem Application Express-Workspace bereits eingeräumt.

Erstellen Sie nun eine einfache Application Express-Anwendung mit einer HTML-Region auf einer Seite. Fügen Sie eine Schaltfläche Job starten hinzu.

Ausgangssituation: Eine Seite in einer Application Express-Anwendung

Abbildung 1: Eine Seite in einer Application Express-Anwendung

Wenn auf die Schaltfläche geklickt wird, soll der oben dargestellte PL/SQL-Code (Prozedur LANG_LAUFEND) im Hintergrund ausgeführt werden. Dies erreichen Sie, indem Sie einen PL/SQL-Prozess mit folgendem Code erstellen:

begin
  dbms_scheduler.create_job(
    job_name   => 'APEX__JOB_LANG_LAUFEND'
   ,job_type   => 'stored_procedure'
   ,job_action => 'lang_laufend'
   ,start_date => null
   ,enabled    => true
  );
end;
Prozess erstellen - Ausführung bei Klick auf die Schaltfläche

Abbildung 2: Seitenprozess erstellen - Ausführung bei Klick auf die Schaltfläche

Mit dem PL/SQL-Paket DBMS_SCHEDULER (Dokumentation) wird der Job erzeugt, in diesem Fall sofort gestartet und nach Beendigung gelöscht. Bevor Sie Ihre Seite starten, fügen Sie ihr noch einen Bericht hinzu. Wählen Sie SQL Bericht aus und hinterlegen Sie die in Abbildung 3 dargestellte SQL-Abfrage.

Bericht erzeugen: aktuelle laufende Hintergrund-Prozesse

Abbildung 3: Bericht erzeugen: aktuelle laufende Hintergrund-Prozesse

Die Data Dictionary View USER_SCHEDULER_JOBS (Dokumentation) gibt Informationen über die vorhandenen Jobs aus. Dieser Bericht selektiert nur den Namen (JOB_NAME) und den Status ( STATE). Starten Sie die Seite nun neu und klicken Sie auf die Schaltläche Job starten. Das Ergebnis sollte wie folgt aussehen.

Gestarteter Hintergrundprozess

Abbildung 4: Gestarteter Hintergrundprozess

Sie sehen, dass der Endbenutzer die Kontrolle über die Applikation sofort zurückbekommen hat. Mit Hilfe des Berichts erfahren Sie, ob ein Job im Hintergrund läuft. Nun wäre es sinnvoll, dem Endbenutzer eine Information über die voraussichtliche Dauer zu geben - quasi eine Art "Fortschrittsbalken" zu implementieren.

Mit dem PL/SQL-Paket DBMS_APPLICATION_INFO (Dokumentation) können Sie der Datenbank (und damit auch dem Datenbankadministrator) Informationen über die gerade laufende Anwendung geben. So kennt der DBA die Dictionary View V$SESSION_LONGOPS (Dokumentation) - diese enthält Informationen über langlaufende Prozesse in der Datenbank.

Warum V$SESSION_LONGOPS und keine eigene Tabelle?

Wenn Sie die Status-Informationen in eine eigene Tabelle schreiben, haben Sie zunächst den Nachteil, dass ein Bericht auf diese Tabelle keine Ergebnisse zurückliefert, da der Hintergrund-Job in einer eigenen Datenbanksitzung abläuft. Solange hier kein COMMIT erfolgt, können Sie die Inhalte nicht in einem Bericht anzeigen.
Man könnte auch dieses Problem mit autonomen Transaktionen lösen, jedoch stellt sich dann die Frage, wie die Tabelle wieder aufgeräumt wird. Außerdem kennt der Datenbankadministrator Ihre Tabelle wahrscheinlich nicht. V$SESSION_LONGOPS ist als Datenbank-Standard genau für diese Anforderung vorhanden.

Im folgenden wird die zu Beginn erstellte Prozedur LANG_LAUFEND so geändert, dass Informationen über den Bearbeitungsstand in der View V$SESSION_LONGOPS bereitgestellt werden. Dies geschieht mit dem Aufruf von DBMS_APPLICATION_INFO.SET_SESSION_LONGOPS.

create or replace procedure lang_laufend as
  rindex pls_integer := -1;
  slno   pls_integer;
begin
  dbms_application_info.set_session_longops(
    RINDEX      => rindex
   ,SLNO        => slno
   ,OP_NAME     => 'APEX__JOB_LANG_LAUFEND'
   ,SOFAR       => 0
   ,TOTALWORK   => 60
  );

  for i in 1..10 loop
    DBMS_LOCK.SLEEP(6);
    dbms_application_info.set_session_longops(
      RINDEX      => rindex
     ,SLNO        => slno
     ,OP_NAME     => 'APEX__JOB_LANG_LAUFEND'
     ,SOFAR       => (i * 6)
     ,TOTALWORK   => 60
    );
  end loop;
end;

Ändern Sie nun noch den Bericht auf Ihrer Anwendungsseite. Es wird nicht USER_SCHEDULER_JOBS, sondern V$SESSION_LONGOPS abgefragt.

Änderung der Status-Abfrage: View V$SESSION_LONGOPS

Abbildung 5: Änderung der Status-Abfrage: View V$SESSION_LONGOPS

Starten Sie nun nochmals die Seite und klicken Sie auf Job starten . Wenn Sie die Seite daraufhin in regelmäßigen Abständen mit der Taste <F5> neu laden, dann stellen Sie fest, dass die Spalte PERCENT sich langsam der 100 nähert.

Gestarteter Hintergrundprozeß: Sicht auf V$SESSION_LONGOPS (Spalte PERCENT)

Abbildung 6: Gestarteter Hintergrundprozeß: Sicht auf V$SESSION_LONGOPS (Spalte PERCENT)

Erweitern Sie zum Abschluß die Seite so, dass mit Hilfe der AJAX-Technologie automatisch ein grafischer Balken angezeigt wird, der nach und nach auf 100% läuft. Zunächst erzeugen Sie einen Anwendungsprozeß - dieser gibt zurück, zu wieviel Prozent der Job bearbeitet ist. Geben Sie dem Prozeß den Namen getJobStatus.

declare
  v_percent number;
begin
  begin
    select (sofar / totalwork) * 100
      into v_percent
    from v$session_longops
    where opname = 'APEX__JOB_LANG_LAUFEND' and sofar < totalwork;
  exception
    when NO_DATA_FOUND then
      v_percent := -1;
    when TOO_MANY_ROWS then
      v_percent := -1;
  end;
  htp.p(v_percent);
end;
Anwendungsprozess erstellen

Abbildung 7: Anwendungsprozess erstellen

Hinterlegen Sie nun im Seiten-Footer etwas JavaScript-Code zum Laden und Darstellen des Fortschrittsbalkens.

<script type="text/javascript">
<!--
var g_interval;

function getJobStatus() {
  var get = new htmldb_Get(
     null,html_GetElement('pFlowId').value,'APPLICATION_PROCESS=getJobStatus',0
  );
  var p;
  try {
    p = new XMLHttpRequest();
  } catch (e) {
    p = new ActiveXObject("Msxml2.XMLHTTP");
  }
  try {
   p.open("POST", get.base, false);
   p.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
   p.send(get.queryString == null ? get.params : get.queryString );
   get.response = p.responseText;
   l_status = get.getPartial();
  } catch (e) {
   return;
  }
  l_status = parseInt(l_status);
  if (l_status != -1) {
    l_status = l_status * 2;
    document.getElementById("PROGRESSBAR").innerHTML =
      "<img src='#IMAGE_PREFIX#1px_gray.gif' width='"+l_status.toFixed(0)+"' height='10'>" +
      "<img src='#IMAGE_PREFIX#1px_white.gif' width='"+(200 - l_status).toFixed(0)+"' height='10'>" +
      " " + (l_status / 2).toFixed(0)+ "%";
  } else {
    document.getElementById("PROGRESSBAR").innerHTML = "<b>Job nicht aktiv</b>";
    window.clearInterval(g_interval);
  }
}

g_interval = window.setInterval("getJobStatus()",1000);
getJobStatus(); 
//-->
</script>

Zum Abschluß muss folgender HTML-Code hinterlegt werden - an dieser Stelle wird dann der Fortschrittsbalken erscheinen. Sie können eine bestehende Region nehmen oder eine neue HTML-Region anlegen. Wichtig dabei ist die Element-ID PROGRESSBAR - diese wird vom JavaScript-Code aus referenziert.

<div style="width: 500px; height: 300px">
  <b>Fortschritt: </b><span id="PROGRESSBAR"></span>
</div>
"Container" für den Fortschrittsbalken erzeugen

Abbildung 8: "Container" für den Fortschrittsbalken erzeugen

Starten Sie die Seite nun neu und klicken Sie auf Job starten. Das Ergebnis sollte dann wie folgt aussehen:

Fortschrittsbalken: 10%
Fortschrittsbalken: 50%
Fortschrittsbalken: fertig

Abbildung 9: Das Ergebnis: Fortschrittsbalken

Neben dieser Darstellung in der Applikation selbst können die Informationen auch über SQL*Plus abgerufen werden - schließlich verwenden Sie hier eine dem Oracle-DBA wohlbekannte View. Anhand der Spalten SOFAR und TOTALWORK ist erkennbar, dass der Job zu 50% erledigt ist.

In der nächsten Ausgabe befassen wir uns näher mit DBMS_SCHEDULER - dann lernen Sie, wie Sie einen Job wiederholt ausführen und dabei auch einen komplexeren Zeitplan verwenden können.

V$SESSION_LONGOPS auf der Kommandozeile

Abbildung 10: V$SESSION_LONGOPS auf der Kommandozeile

Zurück zur Community-Seite