Geschäftslogik in die Datenbank – Umstellung eines Kernbanksystems – Michael Meyer Berenberg Bank Hamburg Schlüsselworte: PL/SQL, Objektorientierung, Oracle Types, XML, Zentralisierung der Geschäftslogik, komponentenbasierte Programmierung Einleitung Im Rahmen eines mehrjährigen Projektes wird ein komplettes Kernbankapplikation von einer cobolbasierten Lösung auf PL/SQL ungestellt. Die gesamte Geschäftslogik wird unmittelbar in der Datenbank abgebildet. Es entstanden/entstehen viele zentrale Komponenten, die einmal entwickelt werden und in vielen Sachgebieten Anwendung finden. Hierbei kommen diverse Oracle Techniken zu Einsatz (Oracle Types, Streams, XML). Gezeigt werden Beispiele, wie diese Techniken in der Praxis angewendet werden, wo sie sinnvoll nutzbar sind und wo nicht. Nach einer Vorstellung der Grundideen des Systems, wird die Umsetzung an Best Practice-Fällen erläutert. Abschließend wird kurz auf die Problematik der Bereitstellung von Entwicklungs- und Testdatenbanken bzgl. Datenanonymisierung eingegangen. Geschäftslogik in der Datenbank Häufig werden Datenbanken als reiner Ablage- oder Speicherort benutzt. Datenbanken stellen dem Entwickler heutzutage aber deutlich mehr Funktionalität zur Verfügung. In SQL/PLSQL können komplexe Logiken direkt in der Datenbank programmiert werden. Daten müssen damit nicht von einem anderen Layer (Tier) geladen, dort verarbeitet und dann wieder zurück geschrieben werden. Alles kann in unmittelbarer „Nähe“ der Daten stattfinden. Weiterhin bietet die Datenbank zahlreiche Packages, die Basisfunktionalitäten bereitstellen (Mailversand, Dateihandling, ...). -- define constructors , CONSTRUCTOR FUNCTION ROW_DH_MAKLER RETURN SELF AS RESULT , CONSTRUCTOR FUNCTION ROW_DH_MAKLER(DH_MAKLER_ID NUMBER, SCHLUESSEL VARCHAR2, BESCHREIBUNG VARCHAR2, ERSTELLT_ID NUMBER, ERSTELLT_ZST DATE, GEAENDERT_ID NUMBER, GEAENDERT_ZST DATE, GEAENDERT_ZAEHLER NUMBER, GELOESCHT_ZST DATE) RETURN SELF AS RESULT , CONSTRUCTOR FUNCTION ROW_DH_MAKLER(IN_DH_MAKLER_ID NUMBER) RETURN SELF AS RESULT , CONSTRUCTOR FUNCTION ROW_DH_MAKLER(IN_SCHLUESSEL VARCHAR2, IN_GELOESCHT_ZST DATE) RETURN SELF AS RESULT , CONSTRUCTOR FUNCTION ROW_DH_MAKLER(IN_SCHLUESSEL VARCHAR2) RETURN SELF AS RESULT -- define member functions , MEMBER FUNCTION ROW_EXISTS(IN_DH_MAKLER_ID NUMBER) RETURN BOOLEAN , MEMBER FUNCTION ROW_EXISTS(IN_SCHLUESSEL VARCHAR2, IN_GELOESCHT_ZST DATE) RETURN BOOLEAN , MEMBER FUNCTION ROW_EXISTS(IN_SCHLUESSEL VARCHAR2) RETURN BOOLEAN , OVERRIDING MEMBER FUNCTION compare(in_type1 GLOBAL.TYPE_OBJECT,in_type2 GLOBAL.TYPE_OBJECT) RETURN INTEGER -- define member procedures , MEMBER PROCEDURE ROW_INSERT , MEMBER PROCEDURE ROW_UPDATE , MEMBER PROCEDURE ROW_MERGE , MEMBER PROCEDURE ROW_SAVE , MEMBER PROCEDURE ROW_DELETE , MEMBER PROCEDURE ROW_SELECT(IN_DH_MAKLER_ID NUMBER) , MEMBER PROCEDURE ROW_SELECT(IN_SCHLUESSEL VARCHAR2, IN_GELOESCHT_ZST DATE) , MEMBER PROCEDURE ROW_SELECT(IN_SCHLUESSEL VARCHAR2) , MEMBER PROCEDURE ROW_DEFAULT , MEMBER PROCEDURE ROW_LOCK , MEMBER PROCEDURE ROW_LOCK(IN_DH_MAKLER_ID NUMBER) , MEMBER PROCEDURE ROW_LOCK(IN_SCHLUESSEL VARCHAR2, IN_GELOESCHT_ZST DATE) , MEMBER PROCEDURE ROW_LOCK(IN_SCHLUESSEL VARCHAR2) ) NOT FINAL Der ROW-Type wird bereits von einem allgemeinen Type (GLOBAL.TYPE_OBJECT) abgeleitet, der Standardroutinen z. B. zur Ausgabe oder zum Vergleich eines Objektes definiert. Direkte DMLBefehle im PLSQL-Programmcode werden mit der Verfügbarkeit solcher ROW-Types in den meisten Fällen überflüssig. Die Änderung eines Attributes wird mit folgendem Code erledigt: declare r_dh bbs.row_dh_makler; begin r_dh := bbs.row_dh_makler(1); r_dh.beschreibung := 'geänderte Beschreibung'; r_dh.row_save(); end; Businesstypes nutzen typischerweise diverse ROW-Types und andere Businesstypes und stellen darüber komplexe Geschäftslogik bereit. So verbindet der TYPE_KUNDE alle Depots und Konten eines Kunden: CREATE OR REPLACE TYPE TYPE_KUNDE UNDER BBS.ROW_KUNDE ( -- %Version $Revision: 54699 $ -- attributes DEPOT BBS.TABLE_DEPOT, KONTEN_BBS BBS.TABLE_KONTO_BBS, -- ... -- ... -- ... -- define constructors , CONSTRUCTOR FUNCTION TYPE_KUNDERETURN SELF AS RESULT , CONSTRUCTOR FUNCTION TYPE_KUNDE(IN_STAMMNR VARCHAR2) RETURN SELF AS RESULT -- define member procedures , MEMBER FUNCTION is_bank RETURN BOOLEAN -- ... ) NOT FINAL Der hier enthalte Type BBS.TABLE_KONTO_BBS enthält wiederum Types für die Darstellung der Konditionen und Gebühren: CREATE OR REPLACE TYPE TYPE_KONTO_BBS UNDER BBS.ROW_KONTO_BBS ( -- %Version $Revision: 54699 $ t_konditionen BBS.TYPE_KONTO_BBS_KONDITIONEN, tab_gebuehrbetrag BBS.TABLE_GEBUEHRBETRAG, -- Constructor CONSTRUCTOR FUNCTION TYPE_KONTO_BBS(IN_KONTO_BBS_ID IN NUMBER) RETURN SELF AS RESULT, CONSTRUCTOR FUNCTION type_konto_bbs(in_kontonummer IN VARCHAR2) RETURN SELF AS RESULT, -- ... -- ... ) NOT FINAL Wird z. B. die Rechtsform eines Kunden geändert, erfolgt dies im Prinzip über den folgenden Code: declare t_kunde bbs.type_kunde; begin t_kunde := bbs.type_kunde('1234567890'); t_kunde.rechtsformid := 52; t_kunde.row_save(); end; Der bei Nutzung der Types entstehende Programmcode ist u. E. leichter les- und wartbar als „traditionelle“ DML-Anweisungen. Geeignet ist diese Vorgehen für eher einzelsatzorientierte Programme – Massendatenupdates sollten über normale DML-Anweisungen erfolgen. Nachteilig bei dem Verfahren ist, dass bei UPDATEs immer alle Spalten einer Zeile aktualisiert werden. Data-Masking für Entwicklungsdatenbanken Für Entwicklungs- und Testzwecke müssen realistische Datenkonstellationen und -mengen zur Verfügung stehen, so dass man schnell auf die Idee kommt, dafür Kopien der Produktionsdatenbank zu nutzen. Diese enthalten aber schützenswerte Daten (Kundennamen, Adressen, Umsatzdaten), die für einen Entwickler nicht zugänglich sein dürfen. Das Oracle-Data-Masking-Pack (Option!) erzeugt PLSQL-Code für eine solche Anonymisierung (Masking genannt). Dafür werden die sensitiven Tabellenspalten identifiziert und deren Inhalte dann während eines Maskierungslaufes anonymisiert. Es stehen diverse Anonymisierungsalgorithmen bereit (Zufallswerte, Shuffle, Substring, User Defined Function, ...). Die referenzielle Integrität der Daten bleibt erhalten. Nach anfänglichen Problemen läuft das Data-Masking bei uns inzwischen recht problemlos. Die Laufzeit für etwa 60 Tabellen mit ca. 250 anonymisierten Spalten beträgt etwa 2 Stunden. Sichtbarkeiten mit Virtual Private Database (Column-Level-Security) Teilweise ist es erforderlich, dass Entwickler lesenden Zugriff auf Produktionsdatenbanken erhalten, um Fehlersituationen analysieren zu können. Hier kann das „Column-Level-Security“-Feature der VPD helfen. Dabei werden alle Zeilen einer Tabelle gezeigt, aber es werden statt der „echten“ Inhalte der zu schützenden Spalten NULL-Werte angezeigt. In aller Regel sind die schützenswerten Spalten einer Tabelle für die Analyse von Fehlersituationen nicht wichtig. Zur Aktivierung der Column-Level-Security sind zunächst wiederum die sensitiven Tabellenspalten zu identifizieren und für diese dann eine Policy anzulegen: begin DBMS_RLS.ADD_POLICY (object_schema=>'BBS', object_name=>'KUNDE', policy_name=>'BBS_KUNDE', function_schema=>'BV', policy_function=>'pa_vpd_reader.fu_policy', sec_relevant_cols=>'VORNAME,NACHNAME', sec_relevant_cols_opt=>dbms_rls.ALL_ROWS); end; / Entfernen einer Policy: begin DBMS_RLS.DROP_POLICY ('BBS', 'KUNDE', 'BBS_KUNDE'); end; / In der Policyfunktion selbst wird für den festen Anmeldeuser (bei uns „READER“), der die Problemanalyse durchführt, ein Wert, der zu FALSE evaluiert wird zurückgegeben. Alle anderen User erhalten hier ein TRUE, damit sie die Spalteninhalte weiterhin sehen können. FUNCTION VARCHAR2 IS fu_policy( obj_schema VARCHAR2, obj_name VARCHAR2 ) RETURN BEGIN v_retVal := '0=0'; IF USER = 'READER' THEN v_retVal := '1=0'; END IF; RETURN v_retVal; END; Ein: SELECT VORNAME, NACHNAME FROM KUNDE; liefert dann für den READER-User: Der Einsatz dieser Technik für etwa 60 Tabellen mit ca. 250 relevanten Spalten zeigt keine spürbaren Performancebeeinträchtigungen