PL/SQL Unit testing

Frameworks zum Unit-Testing von PL/SQL Programmen.

Name Version Licence Test style Link
utPLSQL 2.2 GPL Stored Procedures URL
Quest code tester ? proprietary declarative URL
PL/Unit ? proprietary PL/SQL packages URL
PLUTO – PL/SQL Unit Testing for Oracle 49 Artistic License/GPL PL/SQL classes URL
ruby-plsql-spec 0.2.1 free Ruby DSL URL
DBFit 1.1 GPL functional specifikation URL
DBUnit 2.4.8 Lesser GPL Java/XML URL
SQLDeveloper 3.0 proprietary Wizards, SQL URL

utPLSQL – entwickelt von Steven Feuerstein, ist ein bekanntestes und funktionsreichstes Framework für UnitTesting von PL/SQL Programmen. Es existieren Plugins für Build Tools wie Hudson oder Maven, die eine Integration in Build und Test Infrastruktur erleichtern. Leider wird dieses Framework in letzten Jahren praktisch nicht mehr weiterentwickelt, obwohl man noch viel machen könnte (z.B. bessere Tool Unterstützung, scaffolding usw.)

Quest Code Tester – ein weiteres Produkt von Steven Feuerstein (gestartet unter dem Namen Qute), wird heute von Firma Quest weiterentwickelt und verkauft, es existiert auch eine freeware Version von dem Produkt, die aber stark beschränkt ist, und kann deswegen nicht produktiv eingesetzt werden. Im Unterschied zum anderen Frameworks wird beim Quest Code Tester viel Wert auf Test Driven Development gelegt. Die Tests werden in deklarativer Form vor dem PL/SQL Code entwickelt.

PL/Unit – ist eine vereinfachte Version von dem utPLSQL, die aber in vielen fällen ausreichend ist. Leider ist das Projekt zwar frei, aber nicht opensource, sodass man keine eigenen Erweiterungen und Bugfixes implementieren kann.

PLUTO – PL/SQL Unit Testing for Oracle. Ein neuer Nachfolger von utPLSQL, im Unterschied zum Vorgänger verwendet es ein objektorientiertes Paradigma – jeder Test ist eine Ableitung von einem abstraktem Test Class. Das Framework ist sehr jung und leicht erweiterbar, aber funktional noch seinem Vorgänger unterlegen.

ruby-plsql-spec – ist eine Ruby DSL zum Entwickeln von PL/SQL Test. Die Vorteile einer DSL sind einfachere und lesbare Tests. Das Framework kann auch für Test Driven Development verwendet werden. Die Ruby Programmiersprache stellt aber den Haupt-Nachteil von dem Framework, weil man zusätzliche Sprache und Entwicklungs-Werkzeuge lernen muss, bevor man mit Test Development anfangen kann.

DBFit – eine Erweiterung von FitNesse Projekt (ein Acceptance test framework). Die Tests werden in Form von lesbarer Beschreibung der System-Funktonalität geschrieben (von nicht Programmierern), und werden dann mit Hilfe von „fixtures“ in ausführbare Tests umgewandelt.

DBUnit – ein Datenbank-unabhängiges Java Framework, das für PL/SQL Unit-Testing verwendet werden kann. Ist populär in Java Welt, hat mehrere Erweiterungen z.B. für Maven, Spring, Eclipse usw. Die Tests werden als Kombination von XML Datasets und Java Code entwickelt.

SQL Developer – ein Unit-Testing Framework integriert ins SQL Developer, verwendet ein proprietäres Datenbank Repository. Die Tests werden mit Hilfe von Wirzards zusammengestellt und können dann manuell ausgeführt werden. Da das Framework auf proprietärem Repository basiert, kann es nicht immer leicht in die existierende Build Infrastruktur und Version Management eingebunden werden.

Datenbank unit testing best practise:

  • Eine private Datenbank Instanz für jeden Developer. Beim Entwickeln von Tests für Datenbanken ist sehr wichtig, dass der Zustand von dem Datenbank Schema und Daten während der Entwicklung sich nicht verändert. Deswegen, in Projekten mit mehreren Entwicklern, sollte jeder Entwickler in eigener privater Instanz arbeiten, um Konflikten mit anderen Entwicklern oder Programmen zu vermeiden.
  • Tests müssen unabhängig von Resultaten der anderen Tests sein. Am einfachsten kann man das erreichen, indem man vor jedem Test die Datenbank in initialen Zustand versetzt. Als mögliche Lösung kann man nach jedem Test die Datenbank Schema vollständig aufräumen, oder einfach keine Test Daten und Ergebnisse speichern (Rollback nach jedem Test).
  • Eine Cleanup Funktion. Es ist oft nötig oder unvermeidlich (automatisches commit beim DDL Operationen in Oracle), dass während der Test-Ausführung, Daten in der Datenbank gespeichert werden. Man sollte für diese Fälle eine Cleanup Funktion entwickeln, die das ganze Schema in Initialzustand versetzt, zum Beispiel durch das Einspielen von DB Dump, oder vollständiges Aufräumen der Inhalte aller Tabellen.
  • Kleine Datasets. Für die meisten Unit-Tests ist das vollständige Datenbank Inhalt nicht erforderlich. Stattdessen sollte man womöglich nur kleine, spezifische Datasets für jeden Test erstellen, was die Fehleranalyse erleichtert und einzelnen Tests unabhängig voneinander macht.
  • Gemeinsame Datasets. Viele Tests verwenden ähnliche Initialdaten, um die Entwicklung von solchen Tests zu vereinfachen, sollte man gemeinsame Datasets definieren, die von unterschiedlichen Tests wiederverwendet werden können. Die gemeinsame Datasets können auch vor der Ausführung von Test-Suits (die auf gemeinsamen Daten basieren), direkt in die Datenbank gespeichert werden, um die Test-Initialisierung zu verkürzen. Nach der Test-Suite Ausführung muss die Cleanup Funktion ausgeführt werden.

Weitere Links:

Presentation von Steven Feuerstein über UnitTesting von PL/SQL

Veröffentlicht in Oracle

DB Schema documentation

„Documentation is like sex: when it is good, it is very, very good; and when it is bad, it is better than nothing.“ Dick Brandon

Datenbank ist eine der Ressourcen in Software-Projekten, die immer gründlich und rechtzeitig dokumentiert werden soll.
Datenbank-Schema wird nicht so oft wie das Programmcode geändert (zumindest gut modelliertes Schema), das ist aber oft ein Grund, wieso ausgerechnet Datenbank Dokumention gar nicht oder nicht rechtzeitig gepflegt wird.
Dabei existiert es eine Reihe von Tools, mit denen automatisch aktuelle Dokumentation für den Team oder Projekt-Management/Stakeholder generiert werden kann.

SchemaSpy

  • Betriebsystem: Alle Betriebssysteme (Java)
  • Datenbanken: Unterstüzt alle gängige Datenbanken!
  • Output: Generiert sehr detaillierte verlinkte HTML Seiten, mit einfachen ER Diagrammen zum Überblicken von Tabellen-Relationen.
  • Ausführung: Command-line, SchemaSpyGUI (java client), Maven
  • Konfiguration: Sehr gut
  • Besonderheiten: Einfache Qualitäts-Prüfungen (Fehlender Index, falsher Datentyp etc.). Verliert in der Detallierung spezialisierten Tools wie SQLDeveloper, aber nicht viel.
  • Kosten: Opensource
  • Beispiel

Oracle SQL Developer

  • Betriebsystem: Alle Betriebssysteme (Auf Eclipse basis)
  • Datenbanken: Nur Oracle!
  • Output: sehr detallierte verlinkte HTML Seiten
  • Ausführung: Nur manuell aus SQL Developer
  • Konfiguration: Auswahl von DB Objekten und Detallierung
  • Besonderheiten: Aussehen änelt dem standartem Tabular-Ansicht von SQL Developer
  • Kosten: Frei
  • Dokumentation

Toad’s HTML Schema Doc Generator

  • Betriebsystem: Nur windows
  • Datenbanken: Oracle, DB2
  • Output: Verlinkte HTML seiten.
  • Ausführung: Nur manuell aus TOAD
  • Konfiguration: Auswahl von DB Objekten und Detalliereung
  • Kosten: Toad Lizenz

DBScribe

  • Betriebsystem: Nur Windows
  • Datenbanken: SQL Server, Oracle, MySQL, Postgresql, DB2
  • Output: HTML, CHM, Word
  • Ausführung: Windows GUI, command line
  • Konfiguration: Templates, Auswahl von DB Objekten und Detallierungsgrad
  • Besonderheiten: Gute Detallierung
  • Kosten: 100$

SqlSpec

  • Betriebsystem: Nur Windows
  • Datenbanken: Alle gängige Datenbanken, manche sehr spezifische Datenbanken wie Teradata, Microsoft Integration Server
  • Output: HTML, CHM
  • Ausführung: Windows GUI, command line
  • Konfiguration: Auswahl von DB Objekten und Detallierungsgrad, rudimentär Aussehen vom generiertem Dokument (CSS, Logo, Header, Footer)
  • Besonderheiten: Sehr gute Detallierung für manche DB Objekte z.B. funktion call graph’s, ER Diagramme.
  • Kosten: 150$
  • Beispiel

DBDesc

  • Betriebsystem: Nur Windows
  • Datenbanken: SQL Server, MySQL, Access, Firebird
  • Output: HTML, RTF, Word, PDF
  • Ausführung: Windwos GUI, command line
  • Konfiguration: XSLT Templates, Auswahl von DB Objekten
  • Besonderheiten: XLST Templates
  • Kosten: 100$
  • Beispiel

SchemaToDoc

  • Betriebsystem: Nur Windows
  • Datenbanken: Oracle und SQL Server!
  • Output: HTML, verlinkte Seiten, Word Dokument
  • Ausführung: nur GUI, manuell
  • Konfiguration: rudimintär
  • Besonderheiten:
  • Kosten: 100$
  • Beispiel

SQL Documentation Tool

  • Betriebsystem: Nur Windows
  • Datenbanken: SQL Server
  • Output: HTML, RTF, Plain Text
  • Ausführung: Windows GUI, command line
  • Konfiguration: rudimentär
  • Kosten: 50$
  • Beispiel

Diese Liste hat kein Anspruch auf die Vollständigkeit. Das sind Tools, die ich während einer Evaluierungs-Phase bei einem Software-Projekt analysiert habe.
Am Ende wurde SchemaSpy ausgewählt, wegen der Kosten, sehr guten Konfigurations-Möglichkeiten (Opensource, Java) und guten Ergebnisse für Oracle Datenbank. Dabei haben manche kommerzielle Produkte bessere Ergebnisse im Punkt „Detallierung“ geliefert, waren aber nicht kostenlos und nicht so gut konfigurierbar/einsetzbar.

Veröffentlicht in Oracle

Restore failed Oracle JMS queue messages

Wiederherstellung von durchgefallenen JMS Nachrichten

Angenommen wir haben eine Oracle JMS Queue mit dem Namen „QUEUE_NAME“ und eine JMS Queue Tabelle „QUEUE_TABLE_NAME“, beim Erstellen von Queue hat Oracle automatisch eine Exception Queue angelegt mit dem Namen „AQ$_QUEUE_NAME_E“ (und Exception Queue wurde gestartet)…

Aus verschiedenen Gründen kann es passieren, dass die Messages in Exception Queue landen, zum Beispiel wegen dem Fehler auf der Empfänger Seite. Nach N Versuchen die Nachricht zuzustellen, wid die Nachricht in die Exception Queue verschoben, die meistens von der gleichen Tabelle bedient wird (Oracle verwendet Feld Q_NAME in Queue Tabelle als Queue Identifikator, deswegen kann die gleiche Tabelle für mehrere Queues’s verwendet werden)

Die Nachrichten können aber sehr wichtig sein, und müssen auf jeden Fall zugestellt und abgearbeitet werden.
Am einfachsten kann man Nachrichten zurück in die normale Queue verschieben, in dem man die entsprechende Felder in der Queue Tabelle anpasst (mit dem folgendem Script)

update QUEUE_TABLE_NAME
set q_name = 'QUEUE_NAME',
    state = 0, 
    retry_count = 0, 
    exception_queue = null
where state = 3
  and q_name like 'AQ$_QUEUE_NAME_E';

Alternative Methode (sauberer, aber nicht immer möglich), ist die Verwendung von PL/SQL und DBMS_AQ Package. Dabei werden alle Nachrichten in der Schleife aus einem Queue gelesen (in unserem Fall aus Exception Queue) und in ein anderes Queue geschrieben (zurück in das normalle Queue).

 DECLARE
  dequeue_options dbms_aq.dequeue_options_t;
  enqueue_options dbms_aq.enqueue_options_t;
  message_properties dbms_aq.message_properties_t;
  message_handle RAW(32);
  message  SYS.AQ$_JMS_OBJECT_MESSAGE; -- Type of your message queue e.g. AQ$_JMS_MAP_MESSAGE

  deq_qname varchar2(50) := 'AQ$_QUEUE_NAME_E';
  enq_qname varchar2(50) := 'QUEUE_NAME';

  ex_no_messages exception;
  ex_dequeue exception;
  pragma exception_init(ex_no_messages, -25263);
  pragma exception_init(ex_dequeue, -25228);
  msg_count number := 0;
 BEGIN
  dequeue_options.wait := DBMS_AQ.NO_WAIT;

  LOOP
    --Loop over all message in source queue
    dbms_aq.dequeue(queue_name         => deq_qname,
                    dequeue_options    => dequeue_options,
                    message_properties => message_properties,
                    payload            => message,
                    msgid              => message_handle);
    -- Put message into destination queue
    dbms_aq.enqueue(queue_name         => enq_qname,
                    enqueue_options    => enqueue_options,
                    message_properties => message_properties,
                    payload            => message,
                    msgid              => message_handle);
   
    msg_count := msg_count + 1;
  
    DBMS_OUTPUT.PUT_LINE('Processed ' || msg_count || ' messages');
    
  END LOOP;

 EXCEPTION
  -- exception handling
  WHEN ex_no_messages THEN
    DBMS_OUTPUT.PUT_LINE('No of Messages Moved: ' || msg_count);
    COMMIT;
  WHEN ex_dequeue THEN
    DBMS_OUTPUT.PUT_LINE('No of Messages Moved: ' || msg_count);
    COMMIT;
 END;

Bei dieser Methodik muss der User das Recht auf DBMS_AQ Package besitzen und beide Queues müssen mit entsprechenden enqueu/dequeue Einstellungen gestartet sein (was auch nicht immer erwünscht ist). Ausserdem, muss man eventuell die geänderte Reihenvolge der Nachrichten berücksichtigen, weil die Nachrichten in diesem Fall ein neues „enque“ Zeitstempel bekommen, was bei der Ersten Methodik nicht verändert wird.

Veröffentlicht in Java, Oracle

Oracle JMS performance optimization

Oracle JMS verwendet polling (bei default 1 SQL Abfrage pro Sekunde) um neue Nachrichten in der Queue zu finden…

Dabei werden zwei system View’s abgefragt ALL_QUEUES und ALL_QUEUE_TABLES (sucht nach Queue Name und Queue Tabelle, die in JMS Konfiguration definiert wurden). Leider die Abfragen von System View’s mit Prefix ALL_ sind sehr langsam. Warum hat Oracle an dieser Stelle kein Namen Cache verwendet, ist mir nicht ganz klar, wahrscheinlich wird dadurch gewisse Dynamik ermöglicht …

Um die Abfragen zu beschleunigen, kann man lokale Tabellen mit gleichen Namen im User Schema anlegen (gecached). Dafür werden Administrator Rechte benötigt.

Man muss aber beachten, dass viele Datenbank-Tools (Reverse-Engineering Tools zum Beispiel) können auf die globale View’s zugreifen … Man soll auf die Benutzung von solchen Tools für diesen konkreten User verzichten, oder den Inhalt von lokalen Tabellen aktualisieren nach jeder Änderung von Queue Definitionen.

Vielen Dank an Maxim Bolshakov für den Tipp …

Veröffentlicht in Java, Oracle

Oracle Date <-> Java milliseconds conversion

Die Umrechnungsformel wird relative oft benötigt in Java projekten (Quartz Tabellen im Oracle zum Beispiel).
Die Formeln ist nicht sehr genau, berücksichtigt die Zeitzonen nicht, für meisten Fälle ist aber ausreichend…


Date -> Milliseconds
select to_number(sysdate – to_date(’01-JAN-1970′,’DD-MON-YYYY‘)) * (24 * 60 * 60 * 1000) from dual;

Milliseconds -> Date
select to_date(’01-JAN-1970′,’DD-MON-YYYY‘) + (timestamp_value / (24 * 60 * 60 * 1000)) from dual;

Veröffentlicht in Java, Oracle

Oracle JMS Queue Tables

Kurze beschreibung von Oracle Advanced Queue Tabellen (Benutzt für JMS)

Column Type Value
Q_NAME VARCHAR2(30) Der Name von Queue, wo sich das Message befindet (mehr als ein Queue kann die gleiche Queue Tabelle benutzen, zum Beispiel Exception Queues „AQ$_QUEUE_NAME“)
MSGID RAW(16 BYTE) NOT NULL, eindeutiger Identifikator der Message
CORRID VARCHAR2(128) Correlation ID, kann beim Erstellen von Message vom Benutzer angegeben werden.
STATE NUMBER READY = 0
WAITING = 1
RETAINED or PROCESSED = 2
EXPIRED = 3
DELAY TIMESTAMP(6) Zeitpunkt, nach dem die Zustellung der Nachrichten stattfinden kann.
EXPIRATION NUMBER Intervall in Sekunden zwischen dem Zeitpunkt als Message wurde in Zustand READY gesetzt und Ablauf (falls nicht erfolgreich zugestellt werden konnte)
TIME_MANAGER_INFO TIMESTAMP(6)
LOCAL_ORDER_NO NUMBER
CHAIN_NO NUMBER
CSCN NUMBER
DSCN NUMBER
ENQ_TIME TIMESTAMP(6) Zetstempel, wann das Message in Queue platziert wurde
ENQ_UID VARCHAR2(30) UserID der das Message erzeugt hat
ENQ_TID VARCHAR2(30) Transaktions Indentifkator, der Transaktion in der das Mesage erzeugt wurde
DEQ_TIME TIMESTAMP(6) Zeitstempel, wann das Message zugestellt wurde.
DEQ_UID VARCHAR2(30) UserID mit dem das Message zugestellt wurde
DEQ_TID VARCHAR2(30) TransactionsID, der Transaction in der das Message zugestellt wurde
RETRY_COUNT NUMBER Anzahl der Zustellungsversuche, die bereits statgefundend sind
EXCEPTION_QSCHEMA VARCHAR2(30) Schema der Exception Queue
EXCEPTION_QUEUE VARCHAR2(30) Name der Exception Queue (wo die fehlgeschlagene Nachrichten abgelegt werden)
STEP_NO NUMBER
RECIPIENT_KEY NUMBER
DEQUEUE_MSGID RAW Message ID, der bei der Zustellung dieser Message benutzt wurde.
SENDER_NAME RAW(16 BYTE)
SENDER_ADDRESS VARCHAR2(30)
SENDER_PROTOCOL VARCHAR2(1024)
USER_DATA AQ$_JMS_OBJECT_MESSAGE()
USER_DATA ANYDATA() Inhalt der Message
Veröffentlicht in Java, Oracle

Eventual consistency

Die neue „Buzz words“ in verteilten Datenbanken sind „CAP Theorem“ und „Eventual consistency„.
Fast in jedem Artikel zum NoSQL Produkt wie „Apache Cassandra“, „Amazon SimpleDB“, „CouchDB“ etc. findet man diese Begriffe in der Liste von wichtigsten Features.
Was die Wörter bedeuten, ist wahrscheinlich noch nicht jedem bekannt (es gibt ja noch kein Wikipedia Artikel auf Deutsch zu diesem Thema 😉 ).

CAP Theorem sagt, das in verteilten Systemen gibt es drei (unten angegebene) konkurrierende Ziele. Von den drei Zielen können nur zwei gleichzeitig in verteiltem System erreicht werden.

  • Strong Consistency: Alle Clients sehen selbe Daten.
  • High Availability: Hohe Verfügbarkeit und Ausfallsicherheit. Ausfall oder Trennung von Subsystemen kann nicht zum Ausfall von ganzem System führen. Die formale Definition von „Availability“ bedeutet, dass ALLE Operationen müssen nach dem Ausfall von Subsystem weiterhin erfolgreich ausgeführt werden können. Falls zum Beispiel nur Leseoperationen, aber keine Schreiboperationen möglich sind, dann ist das System nicht mehr „available“. Oder wenn das System Schreiboperationen erlaubt, aber die Daten dadurch inkonsistent werden, ist das System trotzdem „available“.
  • Partition tolerance: Ein verteiltes System kann trotz dem willkürlichem Verlust von Nachrichten zwischen den einzelnen Subsystemen funktionieren. Das heißt – Subsystemen können autonom arbeiten, dabei wird entweder die Konsistenz der Daten verloren (kann eventuell automatisch wiederherstellt werden), oder nicht alle Operationen werden erfolgreich ausgeführt (nur Leseoperationen, oder nur Operationen auf einem Teil der Daten)

Die Begriffe „Availability“ und „Partition tolearance“ sind sehr stark miteinander Verbunden, und werden oft missverstanden. Beide Begriffe haben etwas mit der Verfügbarkeit und verteilten Systemen zu tun, unterscheiden sich aber in den Zielen.

CAP explanations
Availability and Partition Tolerance
CAP Confusion: Problems with „partition tolerance“

NoSQL
A Brief History of NoSQL
List of NoSQL Databases
Amazon SimpleDB Consistency Enhancements

Eventual consistency
BASE: An Acid Alternative
Werner Vogels on ACM Queue
Eventually Consistent – Revisited
Wikipedia
CouchDB Book

Veröffentlicht in NoSQL

Log4j Additivity flag

Ich müsste meinen Bekannten ein Paar Mal die Funktionalität vom „Additivity“ Flag im Log4J erklären.
Um mir das in der Zukunft zu ersparen, habe ich diese Kleine Anleitung zum Additivity Flag erstellt.

Es wird ein kleines Java Projekt benötigt, mit insgesamt 4 Klassen in 4 Paketen:
foo.additivity
foo.additivity.pckg1
foo.additivity.pckg1.sub
foo.additivity.pckg2

die 4 Java Klassen (eine Klasse pro Package)

package foo.additivity;
import org.apache.log4j.Logger;
import foo.additivity.pckg1.FirstClass;
import foo.additivity.pckg1.sub.FirstSubClass;
import foo.additivity.pckg2.SecondClass;

public class Main {
    private static Logger logger = Logger.getLogger(Main.class);    
    public static void main(final String[] args) {
        logger.debug("main");
        new FirstClass();
        new SecondClass();
        new FirstSubClass();
    }
}
package foo.additivity.pckg1;
import org.apache.log4j.Logger;

public class FirstClass {
    private static Logger logger = Logger.getLogger(FirstClass.class);    
    public FirstClass() { logger.debug("created!"); }
}
package foo.additivity.pckg1.sub;
import org.apache.log4j.Logger;

public class FirstSubClass {
    private static Logger logger = Logger.getLogger(FirstSubClass.class);    
    public FirstSubClass() { logger.debug("created!"); }
}
package foo.additivity.pckg2;
import org.apache.log4j.Logger;

public class SecondClass {
    private static Logger logger = Logger.getLogger(SecondClass.class);
    public SecondClass() { logger.debug("created!");}
}

Das Programm wird gestartet, in dem man die Main Klasse ausführt.
Im Java classpath muss noch eine log4j.xml Datei abgelegt werden, wir werden 3 Konfigurationen davon testen:

Erste Konfiguration:
Minimale Log4J Konfiguration, nur „default“ ROOT Logger, mit einem Console Appender „A1“

<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false">
    <appender name="A1" class="org.apache.log4j.ConsoleAppender">
        <layout class="org.apache.log4j.PatternLayout"> 
            <param name="ConversionPattern" value="A1 %5p %c - %m%n"/> 
        </layout> 
    </appender>
    <root>
        <level value="DEBUG" />
        <appender-ref ref="A1" />
    </root>
</log4j:configuration>

Die Ausgabe wird wie folgt aussehen:

A1 DEBUG foo.additivity.Main - main
A1 DEBUG foo.additivity.pckg1.FirstClass - created!
A1 DEBUG foo.additivity.pckg2.SecondClass - created!
A1 DEBUG foo.additivity.pckg1.sub.FirstSubClass - created!

Zweite Konfiguration:
Zusätzlich zum „default“ Logger werden noch zwei weitere Loggers definiert:
1) foo.additivity Logger (default additivity = true)
2) foo.additivity.pckg1 Logger (explizit additivity = true und zusätzlich Appender „A2“)

<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false">
    <appender name="A1" class="org.apache.log4j.ConsoleAppender">
        <layout class="org.apache.log4j.PatternLayout"> 
            <param name="ConversionPattern" value="A1 %5p %c - %m%n"/> 
        </layout> 
    </appender>
    <appender name="A2" class="org.apache.log4j.ConsoleAppender">
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="A2 %5p %c - %m%n"/> 
        </layout>
    </appender>
    <logger name="foo.additivity">
        <level value="DEBUG" />
    </logger>
    <logger name="foo.additivity.pckg1" additivity="true">
        <level value="DEBUG" />
        <appender-ref ref="A2" />
    </logger>
    <root>
        <level value="DEBUG" />
        <appender-ref ref="A1" />
    </root>
</log4j:configuration>

Die Ausgabe von der Zweiten Konfiguration wird wie folgt aussehen:

A1 DEBUG foo.additivity.Main - main
A2 DEBUG foo.additivity.pckg1.FirstClass - created!
A1 DEBUG foo.additivity.pckg1.FirstClass - created!
A1 DEBUG foo.additivity.pckg2.SecondClass - created!
A2 DEBUG foo.additivity.pckg1.sub.FirstSubClass - created!
A1 DEBUG foo.additivity.pckg1.sub.FirstSubClass - created!

Man sieht, das alle Logging ausgaben ab dem Package foo.additivity.pckg1 in beide Appenders geschrieben wurden „A1“ und „A2“.

Dritte Konfiguration:
Beim „foo.additivity.pckg1“ Logger setzen wir den Additivity Flag auf „FALSE“

<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false">
    <appender name="A1" class="org.apache.log4j.ConsoleAppender">
        <layout class="org.apache.log4j.PatternLayout"> 
            <param name="ConversionPattern" value="A1 %5p %c - %m%n"/> 
        </layout> 
    </appender>
    <appender name="A2" class="org.apache.log4j.ConsoleAppender">
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="A2 %5p %c - %m%n"/> 
        </layout>
    </appender>
    <logger name="foo.additivity">
        <level value="DEBUG" />
    </logger>
    <logger name="foo.additivity.pckg1" additivity="false">
        <level value="DEBUG" />
        <appender-ref ref="A2" />
    </logger>
    <root>
        <level value="DEBUG" />
        <appender-ref ref="A1" />
    </root>
</log4j:configuration>

Output:

A1 DEBUG foo.additivity.Main - main
A2 DEBUG foo.additivity.pckg1.FirstClass - created!
A1 DEBUG foo.additivity.pckg2.SecondClass - created!
A2 DEBUG foo.additivity.pckg1.sub.FirstSubClass - created!

Wie man sieht, falls Additivity Flag auf „FALSE“ gesetzt wird, werden die Appenders von höher liegenden Loggern nicht mehr in diesem Package (und allen sub packages) „addiert“. In unserem Fall, wird im Package „foo.additivity.pckg1“ der „A1“ Appender (vom ROOT Logger) nicht mehr verwendet.

Der Additivity Flag kann nützlich sein um:
1) Ausgabe von einem Package (und allen Subpackages) in ein eigenen Appender (Log File?) umzuleiten. Und zwar nur in diesen Appender!
2) Logging in diesem Java Package komplett unterbinden (kein appender definiert).
Dabei wird eine ganze Reihe von „log4j:WARN Please initialize the log4j system properly.“ Warnungen generiert.
Für Test-Zwecke kann man mit „org.apache.log4j.helpers.LogLog.setQuietMode(true);“ diese Warnungen ausschalten (nur wenn Sie wissen was Sie machen!).

Weitere Informationen über Additivity Flag gibt es hier

Veröffentlicht in Allgemein, Java

JMS mit Spring und Oracle Advanced Queuing

In jedem Software Projekt, in dem Oracle Datenbank und Spring Framework zusammen verwendet werden, kann mit Hilfe von „Oracle Advanced Queueing“ auf einfache Art und Weise ein fast vollständiger JMS-Service aufgebaut werden. Diese Funktionalität ist in Oracle Datenbank „Out-of-the-Box“ vorhanden und einsatzbereit.

Für die Verwendung von JMS im Oracle werden folgende Bibliotheken benötigt:

WARNUNG: Oracle AQ API kann nur zusammen mit Oracle Universal Connection Pool verwendet werden, leider funktioniert es nicht mit dem BasicDataSource von Apache Commons (wundert mich nicht so sehr), dafür bietet der Oracle UCP ein Paar wirklich interessante und nützliche Funktionen wie Failover und Caching. Außerdem kann das UCP mit beliebigen JDBC Datenbanken verwendet werden (z.B. MySQL).

Es werden zwei Destination Factories benötigt, ich habe sie als einen abstrakten OracleAqFactoryBean und zwei Ableitungen davon – OracleAqQueueFactoryBean und OracleAqTopicFactoryBean implementiert.
Oracle AQ unterscheidet Destination Typen, wenn Sie ein „Oracle Queue“ als „Multiple_consumers => TRUE“ definieren, werden Sie den OracleAqTopicFactoryBean verwenden müssen, und umgekehrt, für ein „Multiple_consumers => FALSE“ ist ein OracleAqQueueFactoryBean nötig.

public class OracleAqFactoryBean {
    private ConnectionFactory connectionFactory;
    private String oracleQueueName = null;
    private String oracleQueueUser = null;    

    @Required
    public void setConnectionFactory(final ConnectionFactory connectionFactory) {
        this.connectionFactory = connectionFactory;
    }    

    public String getOracleQueueName() {
        return oracleQueueName;
    }    

    @Required
    public void setOracleQueueName(final String oracleQueueName) {
        this.oracleQueueName = oracleQueueName;
    }    

    public String getOracleQueueUser() {
        return oracleQueueUser;
    }    

    public void setOracleQueueUser(final String oracleQueueUser) {
        this.oracleQueueUser = oracleQueueUser;
    }    

    public boolean isSingleton() {
        return false;
    }    

    protected AQjmsSession getSession() throws JMSException {
        // Oracle AQ sessions seems to be always session transacted!
        final AQjmsSession session = (AQjmsSession) connectionFactory.createConnection().createSession(true, Session.SESSION_TRANSACTED);
        return session;
    }
}

public class OracleAqQueueFactoryBean extends OracleAqFactoryBean implements FactoryBean {    
    public Class getObjectType() {
        return javax.jms.Queue.class;
    }    

    public Queue getObject() throws JMSException {
        final AQjmsSession session = getSession();
        return session.getQueue(getOracleQueueUser(), getOracleQueueName());
    }    
}

public class OracleAqTopicFactoryBean extends OracleAqFactoryBean implements FactoryBean {    
    public Class getObjectType() {
        return javax.jms.Topic.class;
    }    

    public Topic getObject() throws Exception {
        final AQjmsSession session = getSession();
        return session.getTopic(getOracleQueueUser(), getOracleQueueName());
    }    

}

Für den Oracle Benutzer, der die Oracle Advanced Queuing verwenden wird, müssen entsprechende Rechte erteilt werden, ich habe für den Test einen „JMSUSER“ angelegt.

grant execute on sys.dbms_aqadm to jmsuser;
grant execute on sys.dbms_aq to jmsuser;
grant execute on sys.dbms_aqin to jmsuser;
grant execute on sys.dbms_aqjms to jmsuser;

begin 
  DBMS_AQADM.GRANT_TYPE_ACCESS('jmsuser'); --  Only for topics!!!
end;

begin 
  dbms_aqadm.grant_system_privilege('ENQUEUE_ANY','jmsuser');
  dbms_aqadm.grant_system_privilege('DEQUEUE_ANY','jmsuser');
end; 

Jetzt können wir das Queue in der Datenbank anlegen.
Das Payload Type vom Queue legt fest, welche Typen von Nachrichten können durch diesen Queue übertragen werden (z.B. ein Queue mit sys.aq$_jms_text_message Payload Type kann NUR String Nachrichten übertragen).

Folgender Script definiert ein Queue mit JMS MapMessage als Nachrichten Typ. Im Fehlerfall, nach 10 Sekunden wird es versucht die Nachricht noch mal zuzustellen, insgesamt wird es maximal 5 Zustellungs-Versuche geben. Falls die Zustellung erfolgreich war, oder 5-mal fehlschlug, wird die Nachricht aus der Tabelle entfernt (retention).

BEGIN 
    DBMS_AQADM.CREATE_QUEUE_TABLE(
        Queue_table        =>'QT_TEST',
        -- sys.aq$_jms_text_message, sys.aq$_jms_map_message, sys.aq$_jms_bytes_message, sys.aq$_jms_object_message
        Queue_payload_type =>'sys.aq$_jms_map_message',
        Storage_clause     => NULL,
        Sort_list          => 'PRIORITY,ENQ_TIME', -- list of queue table fields
        Multiple_consumers => FALSE, -- Queue or Topic!
        Message_grouping   => DBMS_AQADM.NONE, -- DBMS_AQADM.TRANSACTIONAL
        Comment            => 'Test JMS Queue!',
        Primary_instance   => 0, 
        Secondary_instance => 0, 
        Secure             => FALSE
    );
    
    DBMS_AQADM.CREATE_QUEUE(
        Queue_name          => 'Q_TEST',
        Queue_table         => 'QT_TEST',
        Queue_type          =>  DBMS_AQADM.NORMAL_QUEUE,
        Max_retries         =>  5, -- Count of retries
        Retry_delay         =>  10, -- seconds
        Retention_time      =>  0, -- seconds, DBMS_AQADM.INFINITE - retain forever
        dependency_tracking =>  FALSE,
        Comment             =>  NULL
    );

    DBMS_AQADM.START_QUEUE(
        Queue_name            => 'Q_TEST'
    );   
END;

Zusätzliche Informationen über AQ API finden Sie unter folgendem link

Mit diesem Script kann die Queue wieder gelöscht werden (für testzwecke).

begin
    dbms_aqadm.stop_queue('Q_TEST');
    dbms_aqadm.drop_queue('Q_TEST');
    dbms_aqadm.drop_queue_table('QT_TEST');  
end;

Spring Konfiguration für einen JMS Queue (Beim JMS Topic müssen Sie einfach den anderen ConnectionFactory verwenden und entsprechenden Oracle Queue anlegen)

    <bean id="dataSource" class="oracle.ucp.jdbc.PoolDataSourceFactory" factory-method="getPoolDataSource">
        <property name="connectionFactoryClassName" value="oracle.jdbc.pool.OracleDataSource"/>
        <property name="URL" value="jdbc:oracle:thin:@localhost:1521/orcl"/>
        <property name="user" value="jmsuser"/>
        <property name="password" value="jsmpassword"/>
        <property name="connectionPoolName" value="CONN_POOL"/>
        <property name="minPoolSize" value="5"/>
        <property name="maxPoolSize" value="10"/>
        <property name="initialPoolSize" value="5"/>
        <property name="inactiveConnectionTimeout" value="120"/>
        <property name="validateConnectionOnBorrow" value="true"/>
        <property name="SQLForValidateConnection" value="select 1 from dual" />
        <property name="maxStatements" value="10"/>
    </bean>
            
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
            
    <bean id="jmsQueueConnectionFactory" class="oracle.jms.AQjmsFactory" factory-method="getQueueConnectionFactory" >
        <constructor-arg index="0" ref="dataSource"/>
    </bean>    
        
    <!--  Send messages -->
    <bean id="testQueue" class="foo.OracleAqQueueFactoryBean">
        <property name="connectionFactory" ref="jmsQueueConnectionFactory"/>
        <property name="oracleQueueName" value="Q_TEST"/>
    </bean>    
    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
        <property name="connectionFactory" ref="jmsQueueConnectionFactory"/>
        <property name="defaultDestination" ref="testQueue"/>
    </bean>

    <bean id="messagingService" class="foo.MessagingService">
        <property name="jmsTemplate" ref="jmsTemplate"/>
    </bean>
            
    <!--  Receive messages -->
    <bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
        <property name="connectionFactory" ref="jmsQueueConnectionFactory"/>
        <property name="destination" ref="testQueue"/>
        <property name="messageListener" ref="testListener" />
        <property name="sessionTransacted" value="true"/>
        <property name="transactionManager" ref="transactionManager"/>
    </bean>
    <bean id="testListener" class="foo.TestMessageListener"/>

Der Versand kann folgendermassen implementiert werden:

public class MessagingService {
    private static Logger logger = Logger.getLogger(TestMessageListener.class);

    private JmsTemplate jmsTemplate;
    
    public JmsTemplate getJmsTemplate() {
        return jmsTemplate;
    }
    
    public void setJmsTemplate(final JmsTemplate jmsTemplate) {
        this.jmsTemplate = jmsTemplate;
    }
    
    public void sendMapMessage() {        
        final Map message = new HashMap();
        message.put("1", "First value!");
        message.put("2", "Second value!");
        
        jmsTemplate.convertAndSend(message);        
        
        logger.debug("Sent a message!");
    }
}

Wenn die Nachricht versandt wurde, aber noch nicht konsumiert (kein Listener definiert, es gab ein Fehler bei der Zustellung), oder wenn Retention lang genug bzw. INFINITE gesetzt ist, werden Sie die Nachricht als einen Eintrag in der QT_TEST Tabelle sehen.

So kann der Listener für Empfang von Nachrichten aussehen. (Spring bietet zusätzlich ein MessageListenerAdapter, mit dessen Hilfe Empfang von Nachrichten kann im JavaBean Style implementiert werden)

public class TestMessageListener implements MessageListener {
    private static Logger logger = Logger.getLogger(TestMessageListener.class);
    
    public void onMessage(Message message) {
        if (message instanceof MapMessage) {
            final MapMessage jmsMessage = (MapMessage) message;
            logger.debug("Got a message " + jmsMessage.toString());
        } else {
            throw new IllegalArgumentException("Message must be of type MapMessage");
        }
    }
    
}

Zusätzliche Informationen:

Oracle JMS Introduction

Veröffentlicht in Allgemein, Java, Oracle

Hibernate/Oracle und hierarchische denormalizierte daten

Hibernate und Oracle sind die Platzhirsche in jeweils eigenen Produktnischen.

Bei der Verwendung von diesen beiden Produkten können Problemen gelöst werden, für die Hibernate eigentlich nicht gedacht wurde, zum Beispiel – Umgang mit denormalizierten Tabellen.

Ich habe 3 Möglichkeiten gefunden wie man Hibernate und Oracle zum lesen/schreiben von hierarchischen denormalizierten Daten verwenden kann, die als dimensionale Tabellen in Data-Warehouse häufig zu finden sind.

  1. Descriminator Formula (JPA, nur lesend, keine OUTER JOINS, alle Datenbanken)
  2. VIEW’s (JPA, nur lesend, alle Datenbanken)
  3. VIEW’s mit INSTEAD OF Trigger (JPA, lesend und schreibend, nur Oracle)

Gleiche Techniken können auch für Zugriff auf beliebige denormalizierten Daten mit Hibernate/JPA benutzt werden.

Zum Test habe ich eine DIM_ORG Tabelle angeleg, die in 3-Normal Form, 3 Organisations-Hierachien (COUNTRY, BRANCH, STORE) enthalten soll. Ich bin davon ausgegangen, dass ID und entsprechender surrogat OBJECT_ID immer gleich sind, das sollte man mit CHECK CONSTRAINT prüfen. Ausserdem sollte man Orgranistaions-Hierarchie auf dem Datenbank Level prüfen (STORE muss immer in einem BRANCH liegen und BRANCH in einem COUNTRY).

CREATE TABLE DIM_ORG
(  ID NUMBER(10,0) NOT NULL ENABLE,
   COUNTRY_ID NUMBER(10, 0),
   COUNTRY_NAME VARCHAR(128 BYTE),
   BRANCH_ID NUMBER(10,0),
   BRANCH_NAME VARCHAR(128 BYTE),
   STORE_ID NUMBER(10,0),
   STORE_NAME VARCHAR(128 BYTE),
   CONSTRAINT "DIM_ORG_PK" PRIMARY KEY ("ID")
);

Tabelle enthält am Anfang folgende Werte:

ID COUNTRY_ID COUNTRY_NAME BRANCH_ID BRANCH_NAME STORE_ID STORE_NAME
1 1 germany
2 1 germany 2 Berlin
3 1 germany 2 Berlin 3 A1
4 1 germany 2 Berlin 4 A2
5 1 germany 2 Berlin 5 A3
6 1 germany 6 Munich
7 1 germany 6 Munich 7 X1
8 1 germany 6 Munich 8 X2
9 9 spain
10 9 spain 10 Madrid
11 9 spain 10 Madrid 11 M1

Jede der drei Lösungen ist vereinfacht dargestellt, vollständige Eclipse Projekte mit den Lösungen gibt es am Ende des Beitrags.

Descriminator Formula

@Entity
@Table(name="DIM_ORG")
@Immutable
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorFormula("(case " +
		"when store_id is not null then 'S' " +
		"when (branch_id is not null and store_id is null) then 'B' " +
		"when (country_id is not null and branch_id is null) then 'C' " +
		"else null end)")
@ForceDiscriminator
public class OrgEntity {
	public static final String ST_DESCRIMINATOR = "S";
	public static final String BR_DESCRIMINATOR = "B";
	public static final String C_DESCRIMINATOR = "C";

	@Id
	@Column(name = "id")
	private int id;

	public int getId() {return id;}
}
@Entity
@DiscriminatorValue(value = OrgEntity.C_DESCRIMINATOR)
public class Country extends OrgEntity {
	@Column(name = "country_name")
	private String name;

	@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
	@JoinColumn(name = "COUNTRY_ID")
	private Set branches;

	public String getName() {return name;}
	public Set getBranches() {return branches;}
}

Um auf die Daten in der 3 Normal-Form lesend zugreifen, wird ein SINGLE_TABLE Inheritance verwendet mit @ForceDiscriminator Annotation, einzelne Klassen werden mit einem Oracle „CASE“ Ausdruck erkannt.

Vorteile:

  1. Keine Anpassungen in der Datenbank nötig.

Nachteile:

  1. Nur lesend!
  2. Durch die Verwendung von @ForceDiscriminator Annotation keine OUTER JOIN’s mehr möglich. Kann aber durch Caching gemildert werden.
  3. Nicht für jeden Datenbank Verwaltungs-System geeignet, wegen DiscriminatorFormula.

VIEW

CREATE OR REPLACE FORCE VIEW DIM_ORG_COUNTRY_VIEW (COUNTRY_ID, COUNTRY_NAME) AS 
select country_id, country_name 
from dim_org
where country_id is not null and branch_id is null;

CREATE OR REPLACE FORCE VIEW DIM_ORG_BRANCH_VIEW (COUNTRY_ID, BRANCH_ID, BRANCH_NAME) AS 
select country_id, branch_id, branch_name 
from dim_org
where branch_id is not null and store_id is null;
 
CREATE OR REPLACE FORCE VIEW DIM_ORG_STORE_VIEW (BRANCH_ID, STORE_ID, STORE_NAME) AS 
select distinct branch_id, store_id, store_name 
from dim_org
where store_id is not null;
@Entity
@Table(name = "DIM_ORG_COUNTRY_VIEW")
@Immutable
public class Country {
	@Id
	@Column(name = "country_id")
	private int id;

	@Column(name = "country_name")
	private String name;

	@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
	@JoinColumn(name = "COUNTRY_ID")
	private Set branches;

	public int getId() {return id;}
	public String getName() {return name;}
	public Set getBranches() {return branches;}
}

Vorteile:

  1. OUTER JOIN’s möglich

Nachteile:

  1. Nur lesend!
  2. Definition und Anpassung von VIEW’s nötig.

VIEW + INSTEAD OF Trigger

CREATE OR REPLACE TRIGGER DIM_ORG_COUNTRY_VIEW_TRIGGER 
INSTEAD OF INSERT OR DELETE OR UPDATE ON DIM_ORG_COUNTRY_VIEW
BEGIN
    IF INSERTING THEN 
      INSERT INTO DIM_ORG(ID, COUNTRY_ID, COUNTRY_NAME, BRANCH_ID, BRANCH_NAME, STORE_ID, STORE_NAME)
      VALUES (:NEW.COUNTRY_ID, :NEW.COUNTRY_ID, :NEW.COUNTRY_NAME, null, null, null, null);
    END IF;
    
    IF UPDATING THEN 
      UPDATE DIM_ORG -- Update single country record
      SET ID = :NEW.COUNTRY_ID, COUNTRY_ID = :NEW.COUNTRY_ID, COUNTRY_NAME = :NEW.COUNTRY_NAME
      WHERE ID = :OLD.COUNTRY_ID;
  
      UPDATE DIM_ORG -- Update child records
      SET COUNTRY_ID = :NEW.COUNTRY_ID, COUNTRY_NAME = :NEW.COUNTRY_NAME
      WHERE COUNTRY_ID = :OLD.COUNTRY_ID;
    END IF;
    
    IF DELETING THEN 
      DELETE FROM DIM_ORG WHERE COUNTRY_ID = :OLD.COUNTRY_ID; -- delete from dim_org
    END IF;
END;

/
ALTER TRIGGER DIM_ORG_COUNTRY_VIEW_TRIGGER ENABLE;
 
CREATE OR REPLACE TRIGGER DIM_ORG_BRANCH_VIEW_TRIGGER 
INSTEAD OF INSERT OR DELETE OR UPDATE ON DIM_ORG_BRANCH_VIEW
BEGIN
    IF INSERTING THEN 
      INSERT INTO DIM_ORG(ID, COUNTRY_ID, COUNTRY_NAME, BRANCH_ID, BRANCH_NAME, STORE_ID, STORE_NAME)
      SELECT :NEW.BRANCH_ID, COUNTRY_ID, COUNTRY_NAME, :NEW.BRANCH_ID, :NEW.BRANCH_NAME, NULL, NULL
      FROM DIM_ORG WHERE ID = :NEW.COUNTRY_ID AND BRANCH_ID IS NULL;
    END IF;
    
    IF UPDATING THEN 
      UPDATE DIM_ORG -- Update single branch
      SET ID = :NEW.BRANCH_ID, 
          COUNTRY_ID = :NEW.COUNTRY_ID, 
          COUNTRY_NAME = (SELECT COUNTRY_NAME FROM DIM_ORG WHERE ID = :NEW.COUNTRY_ID),
          BRANCH_ID = :NEW.BRANCH_ID, 
          BRANCH_NAME = :NEW.BRANCH_NAME
      WHERE ID = :OLD.BRANCH_ID;  
      
      UPDATE DIM_ORG -- Update child records
      SET COUNTRY_ID =  :NEW.COUNTRY_ID),
          COUNTRY_NAME = (SELECT COUNTRY_NAME FROM DIM_ORG WHERE ID = :NEW.COUNTRY_ID),
          BRANCH_ID = :NEW.BRANCH_ID, 
          BRANCH_NAME = :NEW.BRANCH_NAME
      WHERE BRANCH_ID = :OLD.BRANCH_ID;      
    END IF;
    
    IF DELETING THEN 
      DELETE FROM DIM_ORG WHERE BRANCH_ID = :OLD.BRANCH_ID; -- delete from dim_org
    END IF;
END;

/
ALTER TRIGGER DIM_ORG_BRANCH_VIEW_TRIGGER ENABLE;
  
CREATE OR REPLACE TRIGGER DIM_ORG_STORE_VIEW_TRIGGER 
INSTEAD OF INSERT OR DELETE OR UPDATE ON DIM_ORG_STORE_VIEW
BEGIN
    IF INSERTING THEN 
      INSERT INTO DIM_ORG(ID, COUNTRY_ID, COUNTRY_NAME, BRANCH_ID, BRANCH_NAME, STORE_ID, STORE_NAME)
      SELECT :NEW.STORE_ID, COUNTRY_ID, COUNTRY_NAME, BRANCH_ID, BRANCH_NAME, :NEW.STORE_ID, :NEW.STORE_NAME
      FROM DIM_ORG WHERE BRANCH_ID = :NEW.BRANCH_ID AND STORE_ID IS NULL;
    END IF;
    
    IF UPDATING THEN 
      UPDATE DIM_ORG -- Update store
      SET ID = :NEW.STORE_ID, 
          COUNTRY_ID = (SELECT COUNTRY_ID FROM DIM_ORG WHERE ID = :NEW.BRANCH_ID),
          COUNTRY_NAME = (SELECT COUNTRY_NAME FROM DIM_ORG WHERE ID = :NEW.BRANCH_ID),
          BRANCH_ID = :NEW.BRANCH_ID, 
          BRANCH_NAME = (SELECT BRANCH_NAME FROM DIM_ORG WHERE ID = :NEW.BRANCH_ID), 
          STORE_ID = :NEW.STORE_ID, 
          STORE_NAME = :NEW.STORE_NAME
      WHERE ID = :OLD.STORE_ID;
    END IF;
    
    IF DELETING THEN 
      DELETE FROM DIM_ORG WHERE STORE_ID = :OLD.STORE_ID; -- delete from dim_org
    END IF;
END;

/
ALTER TRIGGER DIM_ORG_STORE_VIEW_TRIGGER ENABLE;

CREATE SEQUENCE  DIM_ORG_SEQ  MINVALUE 1 MAXVALUE 999999999999999999999999999 
INCREMENT BY 1 START WITH 20 NOCACHE ORDER NOCYCLE;
@Entity
@Table(name = "DIM_ORG_COUNTRY_VIEW")
public class Country {
	@Id
	@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SEQ_ORG")
	@SequenceGenerator(name="SEQ_ORG", sequenceName="DIM_ORG_SEQ", allocationSize=20)
	@Column(name = "COUNTRY_ID")
	private int id;

	@Column(name = "COUNTRY_NAME")
	private String name;

	@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
	@JoinColumn(name = "COUNTRY_ID")
	private Set branches;	

	public int getId() {return id;}
	public void setId(int id) {this.id = id;}
	public String getName() {return name;}
	public void setName(String name) {this.name = name;}
	public Set getBranches() {return branches;}
	public void setBranches(Set branches) {this.branches = branches;}
}

Beim Ausführen von DML Operationen auf Views, wird ein Trigger ausgeführt, der diese DML Operationen umdekoriert und auf DIM_ORG Master-Tabelle anwendet. Dabei werden oft Daten aus der DIM_ORG Master-Tabelle benötigt, um die höher liegende Organisations-Hierarchien korrekt zu verwalten.

Vorteile:

  1. Transparenter zugriff auf Daten in 3 Normal-Form! Lesend und schreibend!
  2. OUTER JOIN’s möglich.

Nachteile:

  1. Definition und Anpassung von VIEW’s nötig.
  2. Beim komplexen Hierarchien, Verwaltung von Triggers wird sehr aufwändig und fehleranfällig.

Descriminator
View
View + INSTEAD OF Trigger

Veröffentlicht in Allgemein, Java, Oracle