von Thomas Geisel (Kommentare: 0)
Zusammenspiel von Oracle und Dropbox oder anderen Filehosting-Diensten - auch ohne API
Wie Sie Dateien mit Oracle zu Filehosting-Diensten (Dropbox et al.) auslagern und in Datenbanken nutzen können
Inhalt
- Einleitung
- Vorarbeiten und Vorbemerkungen
- Aufgabenstellung
- Schritt 1: Stammverzeichnis für die LOB-Dateien einrichten
- Schritt 2: Script für das Auflisten eines Verzeichnisinhalts
- Schritt 3: Objekte für die Rückgabe eines Verzeichnislistings anlegen
- Schritt 4: Hilfsroutine zur Ausführung externer Scripte
- Schritt 5: Die Table-Function für das Verzeichnis-Listing erstellen
- Schritt 6: Den Dropbox-Client in Betrieb nehmen
- Ausblick und nächste Schritte
Einleitung
In einer Oracle-Datenbank können binäre Dokumente in LOB-Spalten abgelegt werden. Aber nicht immer ist das der Weisheit letzter Schluss. Gelegentlich möchte man Dokumente weiterhin im Dateisystem des Betriebssystems verwalten, dennoch aber einen Zugriff darauf aus der Oracle-DB heraus ermöglichen.
Ein Grund dafür kann beispielsweise sein, dass Sie eine Oracle-XE einsetzen, welche nur einen limitierten Speicherplatz von 10 GB hat, der durch das Speichern von Dokumente in der Datenbank schnell belegt würde.
Ein weiterer Grund ist aber auch der Gedanke, dass man Dokumente, die über einen Dropbox-Account bereits zwischen Geräten synchronisiert werden, in der Oracle-Datenbankanwendung bereitstellen möchte.
Hierzu bietet z.B. Dropbox die Core-API an, welche theoretisch zu diesem Zweck angesprochen werden kann, allerdings zwei Nachteile mit sich bringt:
- Die Anwendung ist dann fest auf Dropbox eingestellt und funktioniert nicht mit anderen Cloud-Systemen wie Owncloud/Nextcloud, Skydrive oder ähnlichen Lösungen
- Die Dropbox-API erfordert eine SSL-gesicherte Kommunikation, was aber von der Oracle-XE nicht unterstützt wird
Die in diesem Artikel vorgestellte Lösung bietet eine Alternative dazu an, in dem durch "external-jobs" die benötigten Dateioperationen durchgeführt werden und die Lösung somit unabhängig von SSL oder spezifischen APIs ist.
Umgesetzt wird das nachstehend beispielhaft auf der Windows-Plattform, allerdings kann die Lösung durch geringe Modifikation der externen Script-Befehle leicht auf Unix portiert werden.
Vorarbeiten und Vorbemerkungen
In diesem Artikel werden die Daten des Oracle-Sample-Schemas HR genutzt und die Aktionen als User HR ausgeführt.
Daher ist es nötig, diesen User – sofern noch nicht geschehen – freizuschalten und ihm ein Passwort zuzuweisen:
sqlplus sys/<pw>@<db> as sysdba ALTER USER HR IDENTIFIED BY hr ACCOUNT UNLOCK;
Darüber hinaus benötigt der HR-User noch einige Rechte, um den Scheduler anzusprechen und Directories erzeugen bzw. löschen zu können:
grant execute on dbms_scheduler TO hr; grant create job to hr; grant create external job to hr; grant create any directory to hr; grant drop any directory to hr; grant create table to hr; exit
Um die weiter unten aufgeführten SQL- bzw. PL/SQL-Scripte dann direkt ausführen zu können, führen Sie eine Anmeldung als User "HR" durch:
sqlplus hr/hr@<db>
Vorab sei noch angemerkt, dass es sich bei den folgenden Schritten um ein exemplarisches Beispiel handelt, dessen Sinnhaftigkeit und Vollständigkeit hier nicht diskutiert werden soll. Es dient lediglich dazu, die nötigen Techniken zu vermitteln, die bei einer vergleichbaren Aufgabenstellung als Grundlagen dienen.
Aufgabenstellung
Basierend auf den Employee-Daten im HR-Schema, soll eine Möglichkeit geschaffen werden, binäre Dokumente wie Fotos, PDF-Dateien oder Ähnliches zu jedem Employee hinterlegen und abrufen zu können.
Da hierfür reine PL/SQL-Techniken sowie einige wenige Befehle des Betriebssystems verwendet werden, kann zudem die Synchronisation mit Dropbox, Nextcloud, Skydrive oder ähnlichen Filehosting-Lösungen darauf angewendet werden.
Schritt 1: Stammverzeichnis für die LOB-Dateien einrichten
Zunächst benötigen wir ein Verzeichnis, in welchem die Shell-Scripte mit den Betriebssystembefehlen hinterlegt und darunter die individuellen Verzeichnisse je Employee angelegt werden.
Dazu starten Sie mit [Windows]-[R] und der Eingabe von cmd.exe ein Shell-Umgebung und führen dort aus:
md c:\lob_data
Auf diesem Verzeichnis (und seinen künftigen Unterverzeichnisse) muss der Oracle-Prozess die Rechte erhalten:
- Scripte auszuführen sowie
- Dateien und Verzeichnisse anzulegen,
- zu beschreiben und auch wieder zu löschen:
icacls c:\lob_data /grant orainst:(OI)(CI)(F)
In diesem Beispiel läuft der Oracle-Dienst unter dem Benutzeraccount ORAINST und es wird die Vererbung aktiviert und Vollzugriff (F) erteilt.
(Sofern Ihre Oracle-Instanz unter dem lokalen Systemkonto läuft, können Sie diesen Schritt ignorieren)
Als User "HR" erzeugen Sie nun ein Directory, welches auf genau dieses Verzeichnis im Dateisystem verweist:
create or replace directory LOB_DIR as 'c:\lob_data';
Schritt 2: Script für das Auflisten eines Verzeichnisinhalts
Als nächstes benötigen wir ein Betriebssystem-Script, welches die Dateiinhalte eines bestimmten Verzeichnisses auflistet und in eine Textdatei schreibt.
Legen Sie dazu in dem oben erstellten Verzeichnis die Datei list_files.cmd an und tragen dort folgenden Inhalt ein:
@echo off rem Codepage wechseln, damit Umlaute korrekt ausgegeben werden chcp 1252 > nul rem Das Script erwartet mind. 1 Argument mit einer eindeutigen SessionID SID rem und ein optionales 2.tes Argument mit dem Unterverzeichnisnamen. rem Wenn nicht mind. die SID angegeben, dann nichts tun if "%1" equ "" goto :EOF rem Drive und Path des Scripts (Param 0) legen das Basisverzeichnis fest set basedir=%~dp0 rem Die Ausgabedatei wird in diesem Basisverzeichnis mit der SID als Dateiname erzeugt set outfile=%basedir%\%1.txt set listdir=%basedir% rem Wird als 2.tes Argument das Unterverzeichnis angegeben, dann an ListDir anhängen if "%2" neq "" set listdir=%listdir%\%2 rem Header in die Ausgabedatei schreiben echo name,extension,size,timestamp > %outfile% rem alle Dateien in dem Listdir enumerieren und Name, Extension, Size und Timestamp komma-separiert in Ausgabedatei schreiben for %%D in (%listdir%\*) do ( rem nur wenn Datei nicht mit Punkt beginnt, dann auch rausschreiben (Umweg über Env-Variable ist notwendig!) set f=%%~nD set f=!f:~0,1! if "!f!" neq "." ( echo "%%~nD","%%~xD","%%~zD","%%~tD" >> %outfile% ) )
Die so erzeugte Textdatei, welche später in der Oracle-DB als external-table angebunden und ausgelesen wird, könnte dann z.B. folgenden Inhalt haben:
name,extension,size,timestamp "Anstellungsvertrag",".docx","79605","06.03.2017 11:21" "foto",".jpg","50542","31.05.2016 17:16" "Lebenslauf",".pdf","123710","27.01.2017 11:15"
Schritt 3: Objekte für die Rückgabe eines Verzeichnislistings anlegen
Die Technik hierzu ist bereits in "Oracles-OO-Techniken für effiziente Abfragen nutzen" beschrieben, so dass hier nicht mehr ausführlich darauf eingegangen wird.
Wir brauchen also nun einen Object-Type, der eine einzelne Zeile eines Directory-Listings aufnimmt sowie eine Collection, die den kompletten Verzeichnisinhalt aufnehmen kann:
CREATE OR REPLACE TYPE T_FILEINFO FORCE AS OBJECT ( FName varchar2(200), -- der Dateiname FExt varchar2(20), -- die Extension FSize integer, -- die Dateigröße in Bytes FModified date -- das Datum der letzten Änderung ) / CREATE OR REPLACE TYPE T_FILEINFOLIST AS TABLE OF T_FILEINFO; /
Schritt 4: Hilfsroutine zur Ausführung externer Scripte
Da wir in der Folge mehrere unterschiedliche externe Scripte ausführen werden, ist es sinnvoll, eine Hilfsfunktion zu entwickeln, die in PL/SQL einfach mit dem Scriptnamen und den Argumenten aufgerufen werden kann.
Zum Ausführen eines externen Scripts nutzen wir den Oracle-Scheduler, dem wir einen adhoc-Job übermitteln und diesen unmittelbar zur Ausführung bringen.
Dazu muss allerdings der OracleJobScheduler-Dienst aktiviert sein und laufen! Sie können das in der Computerverwaltung–Dienste prüfen und ändern, oder pauschal mit folgenden Befehlen erledigen.
Starten Sie dazu zunächst eine cmd.exe als Administrator ([Windows], cmd.exe eintippen, rechtsklick, "Als Administrator ausführen") und aktivieren und starten Sie den Dienst mit Hilfe von sc.exe:
sc config OracleJobScheduler<instanzname> start= auto sc start OracleJobScheduler<instanzname>
wobei <instanzname> für die SID der Oracle-Instanz steht, also z.B. ORCL oder XE …
CREATE OR REPLACE PROCEDURE runextjob( p_scriptname VARCHAR2, p_sid INTEGER, p_subDir VARCHAR2 DEFAULT NULL ) AS basedir VARCHAR2(100) := 'c:\lob_data'; extcmd VARCHAR(100) := 'C:\windows\system32\cmd.exe'; jobname VARCHAR2(100) := 'os_command_job_' || p_sid; argcnt INTEGER := 3; BEGIN IF ( p_scriptname IS NULL OR p_sid IS NULL) THEN RETURN; END IF; IF (p_subDir IS NOT NULL) THEN argcnt := argcnt + 1; END IF; -- job mit der entsprechenden Anzahl Argumente anlegen DBMS_SCHEDULER.create_job( job_name => jobname, job_type => 'EXECUTABLE', number_of_arguments => argcnt, job_action => extcmd, auto_drop => FALSE, enabled => FALSE); -- Argumente übergeben, wobei Args 1 (/c), 3 (OS-Script) und 3 (SID) immer vorhanden sind DBMS_SCHEDULER.set_job_argument_value(jobname,1,'/c'); DBMS_SCHEDULER.set_job_argument_value(jobname,2, basedir || '\' || p_scriptname ); DBMS_SCHEDULER.set_job_argument_value(jobname,3, to_char(p_sid)); IF (argcnt > 3) THEN DBMS_SCHEDULER.set_job_argument_value(jobname, 4, convert( p_subDir, 'WE8MSWIN1252')); END IF; -- job ausführen und danach wieder löschen dbms_scheduler.run_job(job_name => jobname, use_current_session=> TRUE); dbms_scheduler.drop_job(jobname,force => TRUE); EXCEPTION WHEN others THEN -- bei Fehlern den Job auch wieder löschen dbms_scheduler.drop_job(jobname,force => TRUE); raise; END; /
Jetzt können wir die Procedure bereits testen und uns den Inhalt des Verzeichnisses c:\lob_data ausgeben lassen:
CALL runextjob(p_scriptname => 'list_files.cmd' , p_sid => sys_context('USERENV','SID') , p_subdir => NULL);
Nach erfolgreicher Ausführung sollte sich im Verzeichnis c:\lob_data nun eine Textdatei mit dem Namen zu aktuellen SessionID befinden (z.B. 183.txt), deren Inhalt voraussichtlich so aussieht:
name,extension,size,timestamp "list_files",".cmd","1184","22.08.2017 11:39"
Schritt 5: Die Table-Function für das Verzeichnis-Listing erstellen
Die eigentliche Funktion, die uns nun in unserer Anwendung den Inhalt eines Verzeichnisses anzeigen soll, muss nur noch die vorbereiteten Elemente zusammenfügen und dabei
- das externe Script für das gewünschte Verzeichnis ausführen
- die von dem Script erzeugte Ergebnisdatei als external-table anbinden
- und dessen Inhalt in die Collection überführen und diese zurückliefern
Demzufolge ist diese Funktion leicht zu bauen, aber vorher benötigen wir noch einen TYPE, der einen REF-CURSOR darstellt, da die Funktion Code enthalten soll, um eine Tabelle auszulesen, die zum Compile-Zeitpunkt noch garnicht existiert.
Ein solcher Ref-Cursor muss in einem Package deklariert werden und wird daher wie folgt erzeugt:
CREATE OR REPLACE PACKAGE RC AS TYPE ref_cur IS REF CURSOR; END; /
Bei der Table-Function ist nun noch ein Detail zu beachten: da die Function auch in SQL-Abfragen genutzt werden soll, jedoch während Abfragen eigentlich keine DDL-Operationen zulässig sind, muss diese als AUTONOMOUS_TRANSACTION ausgeführt werden.
CREATE OR REPLACE FUNCTION getDirList(p_subDir VARCHAR2 DEFAULT NULL) RETURN t_FileInfoList AS PRAGMA AUTONOMOUS_TRANSACTION; ret T_FileInfoList := T_FileInfoList(); inf t_FileInfo; sid INTEGER := sys_context('USERENV','SID'); cmd VARCHAR2(4000); cur rc.ref_cur; s VARCHAR2(4000); BEGIN -- externen Job ausführen, um Directory-Listing in Datei zu schreiben runextjob('list_files.cmd', sid, p_subDir ); -- Dynamic-SQL Befehl für External-Table zusammenbauen cmd := 'CREATE TABLE ext_' || sid || '(' || chr(10) || ' fname VARCHAR2(200),' || chr(10) || ' fext VARCHAR2(200),' || chr(10) || ' fsize VARCHAR2(20),' || chr(10) || ' modified VARCHAR2(40)' || chr(10) || ')' || chr(10) || ' ORGANIZATION EXTERNAL (' || chr(10) || ' TYPE oracle_loader' || chr(10) || ' DEFAULT DIRECTORY LOB_DIR' || chr(10) || ' ACCESS PARAMETERS (' || chr(10) || ' RECORDS DELIMITED BY 0X''0D0A''' || chr(10) || ' CHARACTERSET WE8MSWIN1252' || chr(10) || ' STRING SIZES ARE IN CHARACTERS' || chr(10) || ' TERRITORY "AMERICA"' || chr(10) || ' BADFILE '''||sid||'.bad''' || chr(10) || ' LOGFILE '''||sid||'.log''' || chr(10) || ' SKIP 1' || chr(10) || ' FIELDS TERMINATED BY "," OPTIONALLY ENCLOSED BY ''"''' || chr(10) || ' MISSING FIELD VALUES ARE NULL' || chr(10) || ' REJECT ROWS WITH ALL NULL FIELDS' || chr(10) || ' (' || chr(10) || ' fname,' || chr(10) || ' fext,' || chr(10) || ' fsize,' || chr(10) || ' modified' || chr(10) || ' )' || chr(10) || ' )' || chr(10) || ' LOCATION ('''||sid||'.txt'')' || chr(10) || ' )' || chr(10) || ' REJECT LIMIT 1' || chr(10) || ' PARALLEL' || chr(10) || ' NOMONITORING'; -- Datei mit Directory-Listing als external-table einbinden EXECUTE IMMEDIATE cmd; BEGIN -- Inhalt der neuen External-Table per dynamic-SQL lesen cmd := 'SELECT fname, fext, fsize, modified FROM ext_' || sid; OPEN cur FOR cmd; LOOP inf := T_FileInfo(NULL, NULL, NULL, NULL); FETCH cur INTO inf.FName, inf.FExt, inf.FSize, s; EXIT WHEN cur%NOTFOUND; inf.FModified := to_date(s, 'dd.mm.yyyy hh24:mi'); ret.extend; ret(ret.last) := inf; END LOOP; CLOSE cur; -- nachdem alles gelesen ist, die External-Table wieder löschen EXECUTE IMMEDIATE 'drop table ext_' || sid; -- und nun die resultierende Collection zurückgeben COMMIT; RETURN ret; EXCEPTION WHEN others THEN EXECUTE IMMEDIATE 'drop table ext_' || sid; ROLLBACK; RAISE; END; END; /
Diese Table-Function kann bereits auf dem Verzeichnis c:\lob_data getestet werden:
SELECT * FROM TABLE(getdirlist(p_subdir => NULL));
Das Ergebnis sollte etwa wie folgt aussehen:
FNAME FEXT FSIZE FMODIFIED 244 .txt 32 22.08.2017 14:15:00 list_files .cmd 1184 22.08.2017 11:39:00
Schritt 6: Den Dropbox-Client in Betrieb nehmen
Nachdem wir nun beliebige Verzeichnisinhalte auflisten können, ist es an der Zeit den Dropbox-Client bzw. die entsprechenden Module von Nextcloud oder Skydrive zu installieren.
Auf die Installation wird hier nicht eingegangen, da diese Tools jeweils über eigene Installer verfügen, die zuverlässig und leicht verständlich durch die Installation und Erstkonfiguration führen.
Wichtig ist nur, dass das zu synchronisierende Verzeichnis ebenfalls auf c:\lob_data eingestellt wird.
Nehmen wir nun an, dass wir für jeden Employee ein eigenes Unterverzeichnis haben wollen, in welchem korrespondierende Dokumente hinterlegt sein sollen, dann würde sich folgende Verzeichnisstruktur ergeben:
C: \lob_data \emp100 \emp101 \emp102 …
Um dann die vorliegenden Dokumente zu einem Employee abzurufen, kann z.B. eine SQL-Anweisung wie die Folgende verwendet werden:
SELECT e.employee_id, e.email, f.* FROM employees e CROSS JOIN TABLE(getDirList(p_subDir => 'emp'||e.employee_id)) f WHERE ( e.last_name = 'Kochhar' AND e.first_name = 'Neena' ) OR ( e.last_name = 'Seo' AND e.first_name = 'John' );
Das Resultat könnte dann z.B. so aussehen:
EMPLOYEE_ID EMAIL FNAME FEXT FSIZE FMODIFIED 101 NKOCHHAR Anstellungsvertrag .docx 79605 06.03.2017 11:21:00 101 NKOCHHAR foto .jpg 50542 31.05.2016 17:16:00 101 NKOCHHAR Lebenslauf .pdf 123710 27.01.2017 11:15:00 139 JSEO Dienstwagenvereinbarung .docx 79605 06.03.2017 11:21:00 139 JSEO foto .jpg 50542 31.05.2016 17:16:00 139 JSEO Oracle-Zertifikat .pdf 123710 27.01.2017 11:15:00
Ausblick und nächste Schritte
Es ist offensichtlich, dass diese Technik selbstverständlich nicht dazu geeignet ist, Massendaten abzufragen, da für jeden Employee das externe Script aufgerufen, dessen Ergebnisdatei als External-Table angebunden und ausgelesen und diese schließlich wieder gelöscht wird. (Aber wäre eine exzessive Nutzung von z.B. der Dropbox-API letztlich wirklich performanter?)
Eine Optimierung könnte also z.B. sein, in einem nächtlichen Job die Liste der Dokumente pro Employee mit einer internen Tabelle zu synchronisieren, so dass zumindest Anzahl, Namen, Typen und Dateigrößen sowie –Timestamp in der Datenbank für Übersichtsabfragen schnell abrufbar sind und nur beim Aufruf einer speziellen Maske in der jeweiligen Anwendung noch mal konkret den aktuellen Verzeichnisinhalt zu laden.
Um dann diese externen Dokument abzurufen und deren Inhalt an eine Anwendung zurückzugeben, muss schließlich aber noch eine Function erstellt werden, welchen diesen als BLOB zurückgibt.
Hier bietet sich das Lesen des Dateiinhalts via BFile in einen BLOB an, allerdings muss dazu erst dynamisch das Directory angelegt werden, da wir zwar weiter oben das Directory LOB_DIR erzeugt haben, von dort aus aber nicht in den EMPxxx-Unterverzeichnissen gelesen werden kann.
Die Function könnte sinngemäß also so aussehen:
CREATE OR REPLACE FUNCTION getDocContent( p_EmpID INTEGER, p_DocName VARCHAR2) RETURN BLOB AS PRAGMA AUTONOMOUS_TRANSACTION; extfile BFILE; ret BLOB; dirname VARCHAR2(100) := 'doc' || sys_context('USERENV','SID');; BEGIN EXECUTE IMMEDIATE 'create directory ' || dirname || ' as c:\lob_data\emp' || p_EmpID; extfile := BFILENAME(dirname, convert( p_DocName, 'WE8MSWIN1252') ); DBMS_LOB.OPEN(extfile, DBMS_LOB.LOB_READONLY); DBMS_LOB.CREATETEMPORARY(lob_loc => ret, cache => TRUE, dur => dbms_lob.session); DBMS_LOB.OPEN(ret, DBMS_LOB.LOB_READWRITE); DBMS_LOB.LOADFROMFILE(dest_lob => ret, src_lob => extfile, amount => DBMS_LOB.getLength(extfile)); DBMS_LOB.CLOSE(extfile); DBMS_LOB.CLOSE(ret); EXECUTE IMMEDIATE 'drop directory ' || dirname; COMMIT; RETURN ret; EXCEPTION WHEN others THEN EXECUTE IMMEDIATE 'drop directory ' || dirname; RAISE; END; /
Über unsere erstellte Hilfsroutine runExtJob können natürlich noch weitere OS-Scripte ausgeführt werden.
Z.B. um Dokumente zu packen/entpacken, um diese in ein anderes Format zu konvertieren, per Hilfsprogramme an SFTP-Freigaben zu schieben oder per S/SMTP zu mailen (beides geht von Hause aus z.B. in der XE nicht, da diese kein SSL unterstützt) oder an Druckserver zu übermitteln.
Ein abschließender Satz zu dem eigentlichen Titel dieses Artikels - denn wir sind im Laufe der letzten Seiten doch sehr wenig auf Dropbox, Owncloud/Nextcloud, Skydrive und wie sie alle heißen, eingegangen:
Die anfängliche Hürde, ein Oracle-Datenbanksystem mit diesen Diensten zu koppeln und dann komplexe APIs anzusprechen durchzuführen, wird durch die hier vorgestellte Technik ganz leicht entschärft: lassen Sie die Synchronisation des jeweiligen Cloud-Speichersystems durch deren native Clients durchführen und interagieren Sie mit den Inhalten letztlich nur über den transparenten und leicht portierbaren Weg, simple OS-Kommandos aus der Oracle heraus auszuführen und deren Ergebnisse innerhalb der Datenbank zu verwerten.
Einen Kommentar schreiben