Logo Oracle Deutschland   Application Express Community
Automatisches Versionieren von APEX-Anwendungen: Ein Ansatz

Erscheinungsmonat APEX-Version Datenbankversion
April 2014 alle ab 11.2

Gerade von Entwicklern, die in Teams arbeiten (und wer tut das nicht?), hört man recht häufig die Frage, welche Versionierungsmöglichkeiten APEX für Entwicklungsstände von Anwendungen anbietet. Auf den ersten Blick ist die Antwort enttäuschend, denn APEX bietet keine fertige Integration mit Versionierungssystemen wie Subversion, CVS, GIT oder anderen an - auch eine eigene Versionierungsmöglichkeit ist nicht vorhanden.

Dennoch lassen sich mit APEX sehr fortgeschrittene Möglichkeiten finden, Anwendungen zu versionieren - besonders die Tatsache, dass APEX ein (zentraler) Entwicklungsserver ist, wird noch sehr hilfreich sein. In diesem Community-Tipp stellen wir einen Ansatz vor, mit dem Sie APEX-Anwendungen von verschiedenen Datenbankservern automatisch exportieren und in eine zentrales "Versionsrepository" speichern können. Alle dazu nötigen Werkzeuge bringen APEX und die Oracle-Datenbank bereits mit.

Hinweis: In diesem Tipp wird ein Werkzeug verwendet, welches auf der Java-Engine innerhalb der Oracle-Datenbank basiert. Daher kann der Tipp in einer OracleXE-Datenbank nicht nachvollzogen werden - es ist mindestens eine Standard Edition erforderlich.

Abbildung 1 zeigt die Vorgehensweise: Es wird ein "zentrales" Datenbankschema mitsamt einer APEX-Anwendung aufgesetzt. Von hier aus sollen die zu versionierenden APEX-Anwendungen auch aus anderen Datenbanken exportiert werden - das kann sogar jobgesteuert geschehen. Das dazu nötige Exportwerkzeug ist im APEX-Lieferumfang enthalten, legt die Dateien aber ins Dateisystem ab. Also müssen sie aus diesem ausgelesen und in Datenbanktabellen gespeichert werden. Schließlich wird eine APEX-Anwendung erstellt, mit der man sich die vorhandenen Versionen ansehen und bei Bedarf abrufen kann.

Arbeitsweise des automatischen Versionierungsansatzes
Abbildung 1: Arbeitsweise des automatischen Versionierungsansatzes

Verwendete Werkzeuge und Pakete

Wie Sie APEX-Anwendungen von der Kommandozeile aus exportieren, wurde bereits in einem früheren Community-Tipp beschrieben. Die dazu nötigen Java-Programme APEXExport und APEXExportSplitter sind im APEX-Paket enthalten. Der Community-Tipp beschreibt, wie ein Shellskript aufgesetzt werden muss, damit man sie aufrufen kann: APEXExport exportiert dabei eine APEX-Anwendung und legt die Datei ins Filesystem ab - mit dem APEXExportSplitter kann diese dann in die einzelnen APEX-Komponenten zerlegt werden; es entsteht eine Verzeichnisstruktur mit einzelnen Dateien für die einzelnen Bestandteile der APEX-Anwendung.

Diese könnten dann direkt in Systeme wie Subversion oder CVS eingecheckt werden - die nötigen Kommandozeilentools stellen die Versionierungssysteme bereit. Für diesen Tipp wollen wir jedoch APEX nutzen - somit können auch solche Anwender von einer Versionierung profitieren, die kein fertiges Versionierungssystem besitzen.

Die Dateien, die nach Anwendung des APEXExportSplitter im Dateisystem liegen, müssen also in eine Datenbanktabelle geladen werden. Außerdem sollen die o.g. Kommandozeilenwerkzeuge aus der Datenbank heraus gestartet werden können. Hier können die PL/SQL-Pakete zur Interaktion mit dem Betriebs- und Dateisystem helfen. Mit dem Paket OS_COMMAND kann ein Betriebssystem-Kommando gestartet werden; mit dem Paket FILE_PKG kann eine Dateisystem-Verzeichnis mit all seinen Inhalten (rekursiv) in eine Tabelle geladen werden.

Vorbereitungen

Unsere Anwendung soll also mit Hilfe des erwähnten Pakets OS_COMMAND ein Betriebssystem-Kommando starten - die APEX-Exportdateien werden vor dem Laden in die Datenbanktabelle temporär ins Dateisystem abgelegt. Suchen Sie sich hierfür auf dem Datenbankserver ein Verzeichnis aus - bspw. /tmp/apex. Legen Sie darin ein Skript unter dem Namen apexexport.sh ab. Die folgenden Skripte sind für Unix/Linux-Umgebungen erstellt - wenn Ihre Datenbank auf Windows läuft, müssen Sie die Skripts entsprechend anpassen. Für Oracle11g-Datenbanken verwenden Sie dieses Skript ...

#!/bin/sh
# Code für Oracle11g
ORACLE_HOME=/opt/oracle/product/11.2.0/dbhome_1

APEX_HOME=$ORACLE_HOME/apex

CLASSPATH=$ORACLE_HOME/jdbc/lib/ojdbc5.jar
CLASSPATH=$CLASSPATH:$APEX_HOME/utilities

export APEX_HOME ORACLE_HOME PATH CLASSPATH

$ORACLE_HOME/jdk/bin/java oracle.apex.APEXExport -db $1 -user $2 -password $3 -applicationid $4
$ORACLE_HOME/jdk/bin/java oracle.apex.APEXExportSplitter f$4.sql

... und für Oracle12c dieses - natürlich muss die Umgebungsvariable ORACLE_HOME in beiden Fällen an Ihre Umgebung angepasst werden.

#!/bin/sh
# Code für Oracle12c
ORACLE_HOME=/opt/oracle/product/12.1.0/db

APEX_HOME=$ORACLE_HOME/apex

CLASSPATH=$ORACLE_HOME/jdbc/lib/ojdbc6.jar
CLASSPATH=$CLASSPATH:$APEX_HOME/utilities

export APEX_HOME ORACLE_HOME PATH CLASSPATH

$ORACLE_HOME/jdk/bin/java oracle.apex.APEXExport -db $1 -user $2 -password $3 -applicationid $4
$ORACLE_HOME/jdk/bin/java oracle.apex.APEXExportSplitter f$4.sql

Das Skript geht davon aus, dass das heruntergeladene APEX-Archiv nach $ORACLE_HOME/apex ausgepackt wurde. Also muss sich die Datei apexins.sql im Verzeichnis $ORACLE_HOME/apex befinden. Am besten prüfen Sie, ob das bei Ihnen der Fall ist - wenn nicht, muss das Skript angepasst werden.

Machen Sie die Datei danach mit einem chmod u+x ausführbar und legen Sie im gleichen Verzeichnis ein Unterverzeichnis namens files an. Danach sollte Ihr Verzeichnis /tmp/apex in etwa wie folgt aussehen.

[oracle@sccloud033 apex]$ pwd
/tmp/apex
[oracle@sccloud033 apex]$ chmod u+x apexexport.sh
[oracle@sccloud033 apex]$ ls -la
total 32
drwxr-xr-x  3 oracle oinstall 4096 Mar 24 17:17 .
drwxrwxrwt 10 root   root     4096 Mar 24 17:25 ..
-rwx------  1 oracle oinstall  384 Mar 24 17:12 apexexport.sh
drwx------  2 oracle oinstall 4096 Mar 24 17:25 files

Für die Datenbank- und APEX-Anwendung erzeugen Sie nun am besten ein eigenes Datenbankschema und einen eigenen APEX-Workspace. Im folgenden sei dieser VERSION genannt. Dieses Schema braucht neben den "normalen" Privilegien zum Erzeugen von Tabellen und PL/SQL-Objekten zusätzlich noch ein EXECUTE-Privileg für das Package DBMS_CRYPTO. Damit eine PL/SQL-Prozedur in diesem Schema das Shellskript mit OS_COMMAND aufrufen und die generierten Dateien mit FILE_PKG lesen kann, müssen es vom DBA zusätzliche Java-Privilegien für den Umgang mit dem Dateisystem erhalten. Das geschieht mit dem folgenden Skript.

grant execute on dbms_crypto to version
/

begin
  dbms_java.grant_permission( 
    grantee           => 'VERSION', 
    permission_type   => 'SYS:java.io.FilePermission', 
    permission_name   => '/tmp/apex/apexexport.sh', 
    permission_action => 'execute' 
  );
  dbms_java.grant_permission( 
    grantee           => 'VERSION', 
    permission_type   => 'SYS:java.io.FilePermission', 
    permission_name   => '/tmp/apex/files/-', 
    permission_action => 'read,write,delete' 
  );
end;
/

Nun wird deutlich, warum die Verzeichnisstruktur so gewählt wurde ...

  • Unterhalb von /tmp/apex/files kann das Schema VERSION nun Dateien anlegen, ändern oder löschen, aber nichts ausführen.
  • Ausgeführt werden darf allein die Datei /tmp/apex/apexexport.sh. Für diese hat das Schema VERSION aber keine Lese-, Schreib- oder Löschprivilegien. Das Skript kann also nur ausgeführt werden - Änderungen sind nicht möglich.

Installieren Sie nun die oben erwähnten PL/SQL-Pakete zur Interaktion mit dem Betriebs- und Dateisystem im Schema VERSION. Damit sind die Vorbereitungen abgeschlossen.

Einrichten des Datenmodells

Damit die Exports auch automatisiert ablaufen können, müssen die zu exportierenden APEX-Anwendungen in einer Tabelle gespeichert werden. Eine weitere Tabelle speichert die für jede Anwendung durchgeführten Exportvorgänge und in eine dritte Tabelle kommen die konkreten (mit dem APEXExportSplitter zerlegten) Exportdateien - für jede Komponente einer APEX-Anwendung (Seite, Werteliste, etc) entsteht eine Datei. Erzeugen Sie die Tabellen mit dem folgenden Skript.

create sequence seq_app_version_artefacts
/

create sequence seq_app_version
/

create table tab_settings (
  id               number(10)    not null,
  app_id           number        not null,
  app_name         varchar2(100) not null,
  db_url           varchar2(100) not null,
  db_user          varchar2(30)  not null,
  db_passwd        raw(200)      not null,
  active           char(1 char)  not null,
  constraint pk_tabsettings primary key (id),
  constraint ck_tabsettings_active check (active in ('Y','N'))
)
/

create or replace trigger tr_pk_tabsettings
before insert on tab_settings
for each row 
begin
  select seq_app_version.nextval into :new.id from dual;
end;
/
sho err

create table tab_app_versions(
  id                number(10),
  version_name      varchar2(50)  not null,
  setting_id        number(10)    not null,
  datetime          date          not null,
  constraint pk_tabappversions primary key(id),
  constraint fk_tabversions_settings foreign key (setting_id) references tab_settings(id)
)
/

create table tab_app_versions_artefacts (
  id                number(10)    not null,
  version_id        number(10)    not null,
  app_id            number        not null, 
  path              varchar2(500) not null,
  filename          varchar2(100),
  content           clob,
  constraint pk_tabappversartefacts primary key (id),
  constraint fk_tabartefacts_verion foreign key (version_id) references tab_app_versions(id)
)
/

Die Tabelle TAB_SETTINGS enthält, wie man sehen kann, Verbindungsdaten für das Werkzeug APEXExport. Das Passwort sollte natürlich verschlüsselt gespeichert werden; daher die Spalte vom Typ RAW.

PL/SQL-Logik: Package PKG_VERSIONING

Das folgende Package PKG_VERSIONING enthält neben der PL/SQL-Prozedur DO_THE_EXPORT auch die Funktionen zum Ver- und Entschlüsseln des Passworts - in der Spezifikation enthalten und damit von außen aufrufbar ist jedoch nur ENCRYPT. Natürlich ist auch der Schlüssel, mit dem die Passwörter verschlüsselt werden, aus dem Code ersichtlich - der Code sollte also mit dem Oracle-Werkzeug wrap (Dokumentation) unleserlich gemacht und das Schema VERSION sollte stark geschützt werden.

create or replace package pkg_versioning is
  function encrypt(p_text in varchar2) return raw;
  procedure do_the_export(p_version_name in varchar2);
end pkg_versioning;
/
sho err

create or replace package body pkg_versioning is
  -- Das ist der geheime Schlüssel ...
  g_key     raw(32) := utl_raw.cast_to_raw('as189389!20919090we##cv900;13112');

  function encrypt(p_text in varchar2) return raw is
    l_algo    PLS_INTEGER := 
      DBMS_CRYPTO.ENCRYPT_AES256 +  
      DBMS_CRYPTO.CHAIN_CBC +
      DBMS_CRYPTO.PAD_ZERO;
  begin
    return DBMS_CRYPTO.ENCRYPT(
       src => UTL_I18N.STRING_TO_RAW (p_text, 'AL32UTF8'),
       typ => l_algo,
       key => g_key
    );
  end encrypt;

  function decrypt(p_encrypted in raw) return varchar2 is
    l_algo    PLS_INTEGER := 
      DBMS_CRYPTO.ENCRYPT_AES256 +  
      DBMS_CRYPTO.CHAIN_CBC +
      DBMS_CRYPTO.PAD_ZERO;
  begin
    return UTL_I18N.RAW_TO_CHAR(
      DBMS_CRYPTO.DECRYPT(
        src => p_encrypted,
        typ => l_algo,
        key => g_key
      ), 
      'AL32UTF8' 
    );
  end decrypt;

  procedure do_the_export (
    p_version_name in varchar2
  ) is
    l_version_id tab_app_versions.id%type;
    l_dirpath    varchar2(100) := '/tmp/apex/files/';
    l_dir        file_type;
  
    l_os_retcode number;
    l_cmd_str_t  varchar2(500) := '/tmp/apex/apexexport.sh #DBURL# #DBUSER# #DBPASSWD# #APPID#';
    l_cmd_str    varchar2(500);
  begin
    for a in (
      select 
        id,
        app_id, 
        db_url,
        db_user,
        db_passwd
      from tab_settings 
      where active='Y'
    ) loop
  
      -- Schritt 1: temporäres Verzeichnis erstellen
      l_dir := file_pkg.get_file(l_dirpath||'EXP'||a.id);
      l_dir := l_dir.make_dir();
      l_dir := file_pkg.get_file(l_dirpath||'EXP'||a.id);
    
      -- Schritt 2a: Betriebssystem-Kommando für APEXExport und APEXExportSplitter vorbereiten
   --   os_command.set_exec_in_shell;
      os_command.set_working_dir(l_dir);
    
      l_cmd_str := l_cmd_str_t;
      l_cmd_str := replace(l_cmd_str, '#DBURL#',    a.db_url);
      l_cmd_str := replace(l_cmd_str, '#DBUSER#',   replace(a.db_user, '$', '\$'));
      l_cmd_str := replace(l_cmd_str, '#DBPASSWD#', replace(decrypt(a.db_passwd), '$', '\$'));
      l_cmd_str := replace(l_cmd_str, '#APPID#',    a.app_id);
    
      -- Schritt 2a: Betriebssystem-Kommando für APEXExport und APEXExportSplitter ausführen
      l_os_retcode := os_command.exec(l_cmd_str);
  
      -- Schritt 3: Generierte Dateien in die Tabelle laden
      --            Nur bei erfolgreicher Ausführung
      if l_os_retcode = 0 then
  
        insert into tab_app_versions (
          id, version_name, setting_id, datetime
        ) values (
          seq_app_version.nextval, p_version_name, a.id, sysdate
        )
        returning id into l_version_id;
  
        insert into tab_app_versions_artefacts (
          select 
            seq_app_version_artefacts.nextval, 
            l_version_id, 
            a.app_id, 
            replace(f.file_path, l_dir.file_path||'/', '/'),
            f.file_name,
            value(f).get_content_as_clob('utf-8')
          from table(file_pkg.get_recursive_file_list(l_dir)) f
        );
      end if;
   
      -- Schritt 4: Temporäres Verzeichnis löschen
      l_dir := l_dir.delete_recursive();
    end loop;
  end do_the_export;
end pkg_versioning;
/
sho err

Das "Unleserlichmachen" des Codes mit dem wrap-Werkzeug müssen Sie auf dem Datenbankserver oder auf einem System mit installiertem Oracle-Client machen. Typischerweise behandelt man nur den Package Body, nicht die Deklaration.

$ wrap iname=pkg-vers-body.sql oname=pkg-vers-body-wrapped.sql

PL/SQL Wrapper: Release 11.2.0.3.0- 64bit Production on Tue Mar 25 12:11:34 2014

Copyright (c) 1993, 2009, Oracle.  All rights reserved.

Processing pkg-vers-body.sql to pkg-vers-body-wrapped.sql

$ more pkg-vers-body-wrapped.sql

create or replace package body pkg_versioning wrapped
a000000
1
abcd
abcd
abcd
:

Den gewrappten Code können Sie genauso einspielen wie PL/SQL-Code im Klartext. Die Prozedur DO_THE_EXPORT tut nun für jede Zeile der Tabelle TAB_SETTINGS folgendes:

  • Es wird ein Verzeichnis unterhalb von /tmp/apex/files erzeugt
  • Mit diesem Verzeichis als Working Directory , wird das Shellskript apexexport.sh mit Hilfe des Pakets OS_COMMAND aufgerufen; es bekommt die Einträge der Tabelle TAB_SETTINGS als Parameter übergeben. Wenn es fertig ist, enthält das Working Directory einen Verzeichnisbaum mit den einzelnen Artefakten der APEX-Anwendung.
  • Mit dem Paket FILE_PKG wird der gesamte Unterverzeichnisbaum und damit alle Artefakte in die Tabelle TAB_APP_VERSIONS_ARTEFACTS kopiert.
  • Schließlich wird das Working Directory gelöscht.

Erster Test

Fügen Sie nun, zum Testen, ein oder zwei Zeilen in die Tabelle TAB_SETTINGS ein. Passen Sie die folgenden SQL INSERT-Kommandos an Ihre APEX-Umgebung an.

insert into tab_settings (
  app_id, app_name, db_url, db_user, db_passwd, active
) values (
  106, 'Application 1', 'apexserver:1521/orcl', 'SCOTT', pkg_versioning.encrypt('tiger'), 'Y'
)
/

insert into tab_settings (
  app_id, app_name, db_url, db_user, db_passwd, active
) values (
  108, 'Application 2','apexserver:1521/orcl', 'SCOTT', pkg_versioning.encrypt('tiger'), 'Y'
)
/

commit
/

Wenn Sie die Tabelle danach selektieren, finden Sie darin für die Passwörter verschlüsselte Werte; Sie brauchen den Code des Pakets PKG_VERSIONING, um diese zu entschlüsseln - und das haben Sie ja "gewrappt". Testen Sie dann, ob die Prozedur DO_THE_EXPORT funktioniert.

begin
  pkg_versioning.do_the_export('RUN_1');
end;
/

PL/SQL-Prozedur erfolgreich abgeschlossen.

commit
/

Transaktion mit COMMIT abgeschlossen.

Nun sollte Ihre Tabelle TAB_APP_VERSIONS_ARTEFACTS Zeilen enthalten ...

select version_id, app_id, filename 
from tab_app_versions_artefacts;

VERSION_ID     APP_ID FILENAME
---------- ---------- -------------------------
         3        106 value_attribute_pairs.sql
         3        106 standard.sql
         3        106 f106
         3        106 application
         3        106 deployment
         3        106 buildoptions.sql
         3        106 checks.sql
         3        106 definition.sql
         3        106 install.sql
         3        106 end_environment.sql
         :          : :

Die Spalte CONTENT enthält das konkrete Exportfile für die jeweilige Komponente. Der Export kann nun manuell gestartet oder mit Hilfe des PL/SQL-Pakets DBMS_SCHEDULER als regelmäßiger Job in der Datenbank eingerichtet werden (allerdings braucht Ihr Schema dazu das Privileg CREATE JOB).

declare 
  l_block varchar2(32767) := 
    'begin ' ||
    '  pkg_versioning.do_the_export('||
    '    ''EXPRUN''||to_char(sysdate, ''YYYYMMDDHH24MISS'')'||
    '  );'||
    'end;';
begin
  dbms_scheduler.create_job(
    job_name            => 'APEX_VERSIONING_JOB',
    job_type            => 'plsql_block',
    job_action          => l_block,
    number_of_arguments => 0,
    -- bei START_DATE aktuelles Datum einsetzen
    start_date          => to_timestamp('2014-04-15 21:00:00', 'YYYY-MM-DD HH24:MI:SS'), 
    repeat_interval     => 'FREQ=DAILY; INTERVAL=1',
    end_date            => null,
    job_class           => 'DEFAULT_JOB_CLASS',
    enabled             => true,
    auto_drop           => true,
    comments            => null
  );
end;
/

Gedanken zum Frontend

Nun fehlt nur noch eine kleine APEX-Anwendung als Frontend - aber dies soll in diesem Tipp nicht weiter verfolgt werden; schließlich unterscheidet sich das Erstellen dieser Anwendung durch nichts von jeder anderen. Als "Sprungbrett" können Sie diese APEX-Anwendung verwenden (Abbildung 2). Bauen Sie sie einfach nach Belieben aus.

APEX-Anwendung als "Frontend" für den Versionierungsansatz
Abbildung 2: APEX-Anwendung als "Frontend" für den Versionierungsansatz

Im Verzeichnisbaum links können Sie (nach Auswahl einer Anwendung und einer konkreten Version) durch die Artefakte navigieren - klickt man auf eine konkrete Exportdatei rechts, so wird diese heruntergeladen. Sie kann nun in die Entwicklungsumgebung eingespielt und ein älterer Stand so wiederhergestellt werden.

Abrufen eines Artefakts
Abbildung 3: Abrufen eines Artefakts

Fazit

Der hier vorgestellte Ansatz zeigt, dass auch mit APEX ein professionelles Versionsmanagement möglich ist. Zwar bietet APEX selbst nur wenig an, mit den vorhandenen Werkzeugen und den Möglichkeiten der Datenbank ist es jedoch gar nicht so schwer, eigenständig etwas aufzusetzen. Und dabei zeigt sich besonders der Vorteil eines zentralen Entwicklungsservers: Denn mit Hilfe von Datenbank-Jobs kann die Versionierung komplett automatisiert werden, so dass der Entwickler sich um nichts mehr kümmern muss. Nightly Builds sind somit auch für APEX überhaupt kein Problem.

Zurück zur Community-Seite