Montag, 7. Dezember 2015

Bitte Anmelden: Authentifizierung für REST Dienste mit ORDS

In diesem Blog-Posting geht es wieder um Oracle REST Data Services (ORDS) - und hier um die Authentifizierung von Nutzern. Während dieses Thema bei allen anderen Postings in der Vergangenheit außen vor blieb, wollen wir es heute in den Mittelpunkt stellen. Dieses Blog-Posting wurde mit ORDS 3.0.2 erstellt.

ORDS bietet unterschiedliche Varianten an, REST Services mit einer Authentifizierung zu schützen. Der einfachste Weg sind Username und Passwort: etwas fortgeschrittener ist der Weg über das OAuth-Protokoll. Zunächst sei der Weg mit Username und Passwort kurz vorgestellt.

Um die Beispiele auszuprobieren, erzeugt Ihr am besten einen einfachen REST-Service wie folgt. Es wird angenommen, dass ORDS bereits installiert ist und läuft und dass Ihr Zugang zu einem Datenbankschema mit der EMP-Tabelle habt. Das Datenbankschema heißt in diesem Posting RESTAUTH; wenn euer Datenbankschema anders heißt, passt dies an den entsprechenden Stellen (vor allem ORDS-URLs) an. Wenn Ihr noch keine ORDS-Installation habt, findet Ihr Hinweise zur Installation in diesem Blog-Posting.

/*
 * Zuerst im Datenbankschema (hier: RESTAUTH) anmelden.
 * Datenbank-Schema für REST-Services freigeben, falls noch nicht geschehen.
 */

begin
  ords.enable_schema;
end;
/
sho err

commit
/

/*
 * ORDS Service erstellen - noch ohne Authentifizierung
 */

begin
  ords.define_service(
    p_module_name =>    'restauth.emp' ,
    p_base_path  =>     'emp/',
    p_pattern =>        'list/',
    p_method =>         'GET',
    p_source_type =>    ords.source_type_query,
    p_source =>         'select * from emp'
  );
end;
/
sho err

commit
/

Mit einem REST Client könnt Ihr den Service testen - und er wird funktionieren (im Moment noch ohne Authentifizierung). Hier arbeite ich mit dem Kommandozeilentool curl.

D:\> curl http://localhost:8081/ords/restauth/emp/list/ 
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1845    0  1845    0     0   9088      0 --:--:-- --:--:-- --:--:--  9088
{
    "items" : [ {
        "empno" : 7369,
        "ename" : "SMITH",
        "job" : "CLERK",
        "mgr" : 7902,
        "hiredate" : "1980-12-16T23:00:00Z",
        "sal" : 800,
        "comm" : null,
        "deptno" : 20
    }, {
        "empno" : 7499,
        "ename" : "ALLEN",
        "job" : "SALESMAN",
        "mgr" : 7698,

Sicherheit - Schritt 1: Usernamen und Passwort

Nun geht es daran, diesen REST-Service zu schützen. ORDS arbeitet zunächst mit Privilegien und Rollen. Diese werden im zweiten Schritt dann an Usernamen (bei Username/Passwort-Authentifizierung) oder an registierte OAuth-Clients vergeben. Die folgenden PL/SQL Aufrufe erzeugen ein Privileg listEmployees und eine Rolle EmpAdmin.

begin
  ords.create_role(
    p_role_name => 'EmpAdmin'
  );
end;
/
sho err

declare
  l_roles    owa.vc_arr;
  l_patterns owa.vc_arr;
begin
  -- Liste der Rollen, denen das Privileg zugeordnet sein soll 
  -- Weitere werden mit l_roles(2), l_roles(3) usw. hinzugefügt.
  l_roles(1) := 'EmpAdmin';

  -- Liste der URL-Patterns, die geschützt werden sollen.
  l_patterns(1) := '/emp/list/*';

  ords.define_privilege(
    p_privilege_name => 'listEmployees'
   ,p_roles =>          l_roles
   ,p_patterns =>       l_patterns     
   ,p_label =>          'List of Employees'
  );
end;
/

commit
/

Nun sind Rolle und Privileg angelegt; und alle URL, die mit /emp/list beginnen, sind geschützt. Versucht man den curl-Aufruf von vorhin nochmal, so bekommt man nun einen Fehler.

HTTP/1.1 401 Unauthorized
Content-Type: text/html
Content-Length: 11549

:

Im Browser würde man das folgende Bild sehen.

Doch wie meldet man sich an? Im ORDS wurden nur Rollen und Privilegien definiert, aber noch keine Usernamen und Passwörter. Grund ist, dass ORDS selbst keine Benutzernamen und Passwörter verwaltet - diese Aufgabe übernimmt der Application Server, in dem ORDS läuft. Verwendet man einen Tomcat, so müssen nun im Tomcat die Usernamen und Passwörter angelegt werden, gleiches gilt für Weblogic oder jeden anderen Java-Server.

Auch der Standalone-Webserver, mit dem ORDS ausgeliefert ist, bringt eine minimalistische Nutzerverwaltung mit. Darin legen wir nun einen neuen User (rot) an und weisen diesem gleichzeitig die Rolle EmpAdmin (blau) zu.

C:\> java -jar ords.war user cczarski "EmpAdmin"
Enter a password for user cczarski: ********
Confirm password for user cczarski: ********
Dez 07, 2015 10:22:32 AM oracle.dbtools.standalone.ModifyUser execute
INFO: Created user: cczarski in file: D:\oracle\ordsconf\ords\credentials
C:\>

Um den Service nun (wiederum mit curl) aufrufen zu können, müssen Authentifizierungsdaten mitgegeben werden (HTTP Basic Authentication).

D:\> curl -u cczarski:******** -i http://localhost:8081/ords/restauth/emp/list/
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "AwR8DXNHtIkyk4PbmSr0PJnZbaCYETdkmg17Yi28Q3dKq4a6+De3o/GMEbQiv+TjY5z/Owyr3hnHfk2RPmd7Pg=="
Transfer-Encoding: chunked

{
    "items" : [ {
        "empno" : 7369,
        "ename" : "SMITH",
        "job" : "CLERK",
        "mgr" : 7902,
        "hiredate" : "1980-12-16T23:00:00Z",
        "sal" : 800,
        "comm" : null,
        "deptno" : 20

Wichtig bei diesem Setup ist, dass alles genau zusammenpasst: Die Rolle, die dem Nutzerkonto im Application Server zugewiesen ist, muss genau zur Rolle, die mit ORDS.CREATE_ROLE erzeugt wurde, passen. Auch Groß- und Kleinschreibung sind hier wichtig. Zu empfehlen ist auch, sich auf reine ASCII-Zeichen zu beschränken und das Leerzeichen nicht zu nutzen. Es mag zwar sein, dass ORDS damit keine Probleme hat - es kann aber sein, dass es Beschränkungen in den Java-Containern gibt, die dann dazu führen, dass die Rollendefinitionen nicht mehr zusammenpassen.

Mehr Sicherheit: OAuth-Verfahren

Der größte Nachteil am bis jetzt beschriebenen Verfahren ist, dass die Login-Credentials (Usernamen und Passwort) bei jedem Request mitgeschickt werden müssen. Technisch wird das Format {username}:{password} bei dieser Basic-Authentication ins Base64-Format konvertiert und als HTTP-Header Authorization mitgesendet. Das Verwenden von HTTPS sollte bei dieser Authentifizierungsvariante selbstverständlich sein - ohne SSL-Verschlüsselung kann jedermann die Login-Daten mitlesen. Aber auch mit Verschlüsselung gibt es bessere Verfahren: So wäre es doch sinnvoller, Usernamen und Passwort nur einmal (bei der intialen Anmeldung) zu senden und danach mit einem Token zu arbeiten. Das hat den entscheidenden Vorteil, dass auch die Client-Anwendung sich das Passwort nicht meht mehr merken muss. Im folgenden setzen wir also eine einfache OAuth-Authentifizierung für unseren REST-Service auf.

Ein OAuth-Client muss zuerst als solcher registriert werden. Das geht wie folgt:

/*
 * Client-Applikation erstellen
 */

begin 
  oauth.create_client(
    p_name =>            'OAuth EMP List Application',
    p_grant_type =>      'client_credentials',
    p_privilege_names => '',
    p_support_email =>   'carsten.czarski@oracle.com'
  );
end;
/
sho err

commit
/

/*
 * Rolle zuweisen
 */

begin
  oauth.grant_client_role(
    p_client_name => 'OAuth EMP List Application',
    p_role_name =>   'EmpAdmin'
  );
end;
/

commit 
/

Nun ist die Applikation registriert und ORDS hat eine Client-ID und ein Client-Secret erzeugt. Diese kann man sich als Usernamen und Passwort für eine Client-Anwendung vorstellen. Die folgende SQL-Abfrage zeigt sie an - für den nachfolgenden curl Aufruf werden wir sie brauchen.

SQL> select name, client_id, client_secret from user_ords_clients;

NAME                           CLIENT_ID                        CLIENT_SECRET
------------------------------ -------------------------------- --------------------------------
OAuth EMP List Application     oMQvSnxOUOTXWd3oKgZN9Q..         PyFS4qVFSe_hk08BzTlmtA..

1 Zeile wurde ausgewählt.

Auch die Zuordnung der Rolle zur Client-Anwendung kann man sich ansehen.

SQL> select * from user_ords_client_roles;

 CLIENT_ID CLIENT_NAME                       ROLE_ID ROLE_NAME
---------- ------------------------------ ---------- ---------------
     10508 OAuth EMP List Application          10489 EmpAdmin

1 Zeile wurde ausgewählt.

Nun wollen wir die OAuth-Authentifizierung testen. Dazu braucht es im ersten Schritt die oben angezeigten Client-ID und das Client-Secret (bei euch sind das natürlich dann andere Werte). Der erste REST-Call mit curl dient nun allein der Authentifizierung der Client-Anwendung - und wir bekommen demzufolge auch nur ein Access Token zurück. Mit diesem führt man danach den eigentlichen REST-Call durch. Zuerst also der Call zur Authentifizierung ...

C:\> curl -i 
             --user oMQvSnxOUOTXWd3oKgZN9Q..:PyFS4qVFSe_hk08BzTlmtA.. 
             --data "grant_type=client_credentials" 
             http://localhost:8081/ords/restauth/oauth/token
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked

{"access_token":"LfXJilIBdzj5JPRn4xb5QQ..","token_type":"bearer","expires_in":3600}

Der OAuth-Dienst von ORDS steht unter dem URL-Pattern oauth/token bereit. Client-ID und Client-Secret werden wie als HTTP Basic Authentication mitgegeben. In einem REST-Client im Browser sieht das in etwa wie folgt aus.

In der JSON-Antwort findet sich nun das Access-Token, dass bei fortfolgenden REST-Anfragen wie folgt als HTTP-Header Authorization mitgegeben wird.

C:\> curl -H"Authorization: Bearer nHpPl5J_0NgtFephW7n6gQ.." -i http://localhost:8081/ords/restauth/emp/list/
 
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "AwR8DXNHtIkyk4PbmSr0PJnZbaCYETdkmg17Yi28Q3dKq4a6+De3o/GMEbQiv+TjY5z/Owyr3hnHfk2RPmd7Pg=="
Transfer-Encoding: chunked

{
    "items" : [ {
        "empno" : 7369,
        "ename" : "SMITH",
        "job" : "CLERK",
        "mgr" : 7902,
        "hiredate" : "1980-12-16T23:00:00Z",
        "sal" : 800,
        "comm" : null,
        "deptno" : 20

Das Token ist 3600 Sekunden, also eine Stunde lang gültig. Solange müssen Client-ID und Client-Secret nicht mehr über die Leitung gesendet werden. Das reicht typischerweise für eine Anwendungssession aus.

Das OAuth-Protokoll geht aber noch weiter: Das Ziel ist es, dass eine externe Anwendung (losgelöst von ORDS) eine OAuth-Authentifizierung startet - woraufhin der Browser zur Anmeldemaske von ORDS geleitet wird. Nach erfolgreicher Anmeldung wird der Browser zur externen Anwendung zurückgeleitet und das OAuth-Token wird dabei mitgesendet. Die externe Anwendung kann mit diesem Token nun REST-Dienste im ORDS aufrufen. Die Anwendung muss sich keinerlei Usernamen und Passwörter merken und kennt allein die OAuth-Tokens. Der Endanwender gibt seine Usernamen und Passwörter nur in die Anmeldemaske des ORDS ein. Wie das aufgesetzt wird, wird dann in einem der nächsten Blog-Postings erläutert. Bis dahin viel Spaß beim Ausprobieren.

Dienstag, 10. November 2015

Node.js und die Oracle-Datenbank: Erste Schritte ... und ein etwas anderes Beispiel

Heute geht es mal nicht um REST und Oracle REST Data Services, dieses Posting stellt vielmehr die Entwicklerplattform Node.js in den Vordergrund. Ich werde euch den Oracle-Datenbanktreiber für Node.js vorstellen: node-oracledb. Der Treiber ist kostenlos verfügbar und wird auf Github und auf npmjs.org gehostet. Im Oracle Technet findet man im Node.js Developer Center nähere Informationen. In diesem Blog-Posting muss der Treiber aber nicht manuell heruntergeladen werden; das geht alles vollautomatisch.

Was Node.js ist und wie es funktioniert, möchte ich hier nicht mehr ausführlich beschreiben; dazu gibt es eine ganze Menge fertiger Artikel im Internet, die ich hier nicht nochmals wiederholen möchte. Ich nenne beispielhaft mal zwei - eine kurze Suche bringt noch viel mehr zum Vorschein.

In meinem anderen Blog SQL und PL/SQL in der Oracle-Datenbank hatte ich vor einiger Zeit schon Postings zu Node.js: Im ersten Posting gab es den "offiziellen" Treiber node-oracledb noch gar nicht, so dass ich auf einen der Open Source Community angewiesen war. Der ist heute nicht mehr nötig - der Treiber von Oracle ist ebenfalls Open Source und bietet wesentlich mehr Funktionen an. Das zweite Blog Posting habe ich kurz nach dem Beta-Release von node-oracledb geschrieben; es zeigt erstmals die Installation. Diese hat sich inzwischen etwas geändert - so dass dieses Blog-Posting hier den aktuellen Stand wiedergibt.

Node.js ist also Javascript – auf dem Server. Node.js ist in der Entwicklergemeinde mittlerweile sehr populär, was man vor allem an den zahlreichen Community-Erweiterungen erkennen kann. Für den Package Manager (NPM = Node Packaged Modules), mit dem die Erweiterungen der Node-Bibliothek verwaltet werden können, stehen mittlerweile fast 120.000 Pakete bereit.

Das asychrone Programmiermodell von Node.js werden wir bei den ersten Versuchen mit dem Datenbanktreiber node-oracledb noch kennenlernen. Alle blockierenden Aufrufe (Netzwerk, lokales Dateisystem etc.) werden mit Node asynchron ausgeführt – das Programm kann also weiter arbeiten. Man bemerkt deutlich das Ziel von Node.js: Die CPU soll möglichst ausgelastet werden – der Programmthread soll nur dann warten, wenn gerade wirklich nichts zu tun ist. Besonders beliebt ist Node für das Aufsetzen webbasierter Dienste, kommt diese Architektur deren Anforderungen doch besonders entgegen.

Im folgenden beschreibe ich, wie Ihr (ausgehend von einem Linux-System) eine Node.js-Umgebung so einrichtet, dass Ihr Node.js Programme für die Oracle-Datenbank schreiben könnt.

Installation von node.js selbst

Ladet euch Node.js von nodejs.org herunter. Zur Zeit unterstützt der Oracle-Treiber die Node.js Versionen 0.10 und 0.12. Sucht die richtige Datei für eure Plattform und installiert diese. Nach der Installation prüft Ihr, ob die Executables für Node und den Package Manager NPM funktionieren.

# ${NODE_HOME}/bin/node -v
v0.12.7

# ${NODE_HOME}/bin/npm -v
2.11.3

Der Package Manager NPM ist für das Nachladen benötigter Node.js-Pakete aus dem Internet zuständig. Wenn sich eure Umgebung hinter einer Firewall befindet, so müsst Ihr einen Proxy-Server setzen; das ist nur einmal erforderlich und geht wie folgt:

# npm config set proxy=http://{proxy-server}:{port}
# npm config set https-proxy=http://{proxy-server}:{port}

Legt euch dann ein "Arbeitsverzeichnis" an (mywork) - darin könnt Ihr die Internetverbindung direkt testen - ladet euch das Node-Paket express herunter, das ist ein sehr beliebter Webserver für Node.js.

# mkdir mywork
# cd mywork
# npm install express
express@4.13.3 node_modules/express
+-- escape-html@1.0.2
+-- merge-descriptors@1.0.0
:
+-- accepts@1.2.13 (negotiator@0.5.3, mime-types@2.1.7)
+-- send@0.13.0 (destroy@1.0.3, statuses@1.2.1, ms@0.7.1, http-errors@1.3.1, mime@1.3.4)

Euer Arbeitsverzeichnis enthält nun ein Unterverzeichnis node_modules; darin eins namens express, welches die Dateien des Pakets enthält.

Installation des Oracle Instant Clients

Der Datenbanktreiber node-oracledb benötigt zum Funktionieren Zugriff auf die Client-Bibliotheken der Oracle-Datenbank. Wenn eure Datenbankinstallation bereits auf dem Node.js Server vorhanden ist, müsst Ihr nichts weiter tun. Wenn nicht, muss ein Client heruntergeladen werden. Am besten ist der Oracle Instant Client geeignet; dessen Installation ist am einfachsten. Achtet darauf, dass Ihr zwei Pakete aus dem OTN herunterladet:

  • Instant Client Package - Basic: Das ist das Basispaket mit den Client-Bibliotheken.
  • Instant Client Package - SDK: Hierin sind die Header-Files für den C-Compiler enthalten; dieses Paket wird initial zum Einrichten des Datenbanktreibers gebraucht.

Packt beide ZIP-Dateien in ein Verzeichnis (${INSTANTCLIENT_HOME}) aus. Es sollte dann in etwa so aussehen (die Dateien des Pakets Basic sind blau, die des SDK rot markiert)

# cd ${INSTANTCLIENT_HOME}
$ find
./uidrvci
./libocci.so.12.1
./libociei.so
:
./xstreams.jar
./sdk
./sdk/include
./sdk/include/occiCommon.h
./sdk/include/occi.h
./sdk/include/occiData.h
:

Jetzt legt Ihr noch einen symbolischen Link namens libclntsh.so für die Datei libclntsh.so.12.1 an; natürlich kann man die Datei auch einfach kopieren. Dann sind die Vorbereitungen soweit fertig.

# cd ${INSTANTCLIENT_HOME}
# ln -s libclntsh.so.12.1 libclntsh.so

Installation des Datenbanktreibers "node-oracledb"

Wechselt nun nochmals in euer Arbeitsverzeichnis mywork. Setzt die Umgebungsvariablen LD_LIBRARY_PATH, OCI_LIB_DIR und OCI_INC_DIR wie folgt. LD_LIBRARY_PATH muss künftig auch zur Laufzeit immer gesetzt sein, die anderen beiden werden nur einmalig zum Build des Treibers benötigt.

# cd mywork
# export OCI_LIB_DIR = ${INSTANTCLIENT_HOME}
# export OCI_INC_DIR = ${INSTANTCLIENT_HOME}/sdk/include

# export LD_LIBRARY_PATH = ${INSTANTCLIENT_HOME}:$LD_LIBRARY_PATH

Wenn Ihr nicht mit dem Instant Client, sondern mit einer lokal installieren Oracle-Datenbank arbeitet, dann habt Ihr ein ORACLE_HOME und die Umgebungsvariablen werden etwas anders gesetzt.

# export OCI_LIB_DIR=${ORACLE_HOME}/lib
# export OCI_INC_DIR=${ORACLE_HOME}/rdbms/public

# export LD_LIBRARY_PATH = ${ORACLE_HOME}/lib:$LD_LIBRARY_PATH

Danach "installiert" Ihr den Treiber einfach mit npm install. Das Herunterladen, Auspacken und den "Build" des Treibers übernimmt NPM - Ihr solltet etwa folgende Ausgabe sehen.

# npm install oracledb
|
> oracledb@1.3.0 install /home/oracle/mywork/node_modules/oracledb
> node-gyp rebuild
:
  CXX(target) Release/obj.target/oracledb/src/dpi/src/dpiCommon.o
  SOLINK_MODULE(target) Release/obj.target/oracledb.node
  COPY Release/oracledb.node
make: Leaving directory `/home/oracle/mywork/node_modules/oracledb/build'
oracledb@1.3.0 node_modules/oracledb
+-- nan@1.9.0

Das Unterverzeichnis node_modules enthält nun einen weiteren Unterordner: oracledb. Jetzt kann's losgehen.

Das erste Node.js Programm mit der Oracle-Datenbank

Nun ist der Treiber installiert und das erste Node.js Programm zum Zugriff auf die Oracle-Datenbank kann geschrieben werden. Hier ist es auch schon, zunächst selektieren wir eine Zeile der EMP-Tabelle. Passt die Koordinaten eures Datenbankservers in Zeile 7 vorher an eure Umgebung an.

var oracledb = require('oracledb');

oracledb.getConnection(
  {
    user          : "scott",
    password      : "tiger",
    connectString : "dbserver:1521/orcl"
  },
  function(err, connection) {
    if (err) {console.error(err.message); return;}
    connection.execute(
      "SELECT * from EMP where ENAME = 'KING'",
      [],
      function(err, result) {
        if (err) {console.log('%s', err.message); return;}
        console.log(result.rows);
      }
    );
  });

console.log("Finished – really?");

Wenn Ihr das Programm laufen lasst, greift Ihr direkt mit Javascript auf eine Oracle-Datenbank zu - wer hätte das vor 10 Jahren gedacht ...?

# node emp.js
Finished – really?
[ [ 7839,
    'KING',
    'PRESIDENT',
    null,
    Tue Nov 17 1981 00:00:00 GMT+0100 (CET),
    5000,
    null,
    10 ] ]

Die Ausgabe zeigt deutlich die asynchrone Natur von Node.js. Zunächst baut das Programm eine Datenbankverbindung auf - da dies ein I/O gebundener Vorgang ist, wird ein Callback (als "anonymer" Block) mitgegeben, der dann aufgerufen wird, wenn die Verbindung steht. Der Programmthread arbeitet schon mal weiter mit der Anweisung console.log("Finished - really?"). Das Callback enthält dann das eigentliche Ausführen der SQL-Abfrage und die Ausgabe des Ergebnisses. Das Ausführen der SQL-Abfrage ist wiederum eine asynchrone Operation, die wiederum einen Callback übergeben bekommt. Man merkt schon, dass man mit Node.js etwas anders programmiert als mit "klassischen" Programmiersprachen.

"Screenshots" von Webseiten direkt in die Datenbank: Mit Node.js!

Zum Abschluß möchte ich ein etwas "exotischeres" Node.js Paket vorstellen: webshot. Dieses Node.js Paket ruft eine Webseite auf (ganz wie ein Browser), rendert die HTML-Seite und speichert diese als Bild ab. Damit kann man sich leicht "Vorschaubilder" für Webseiten erstellen - und genau das machen wir jetzt (es scheint, dass webshot keine Proxy-Server unterstützt; ihr müsst also Webseiten im Intranet nehmen). Natürlich speichern wir die Bilder in der Datenbank ab. Zuerst braucht es also eine Tabelle ... und in diese schreiben wir ein paar Zeilen für Webseiten, die wir "aufnehmen" wollen.

create table webshots (
  name varchar2(200)  not null, 
  url varchar2(200)   not null, 
  time_taken date, 
  image blob,
  constraint pk_webshots primary key (name)
);

insert into webshots values (
  'BLOG JSON, REST und Oracle12c','json-rest-oracledb.blogspot.com', null, null
);
insert into webshots values (
  'APEX Community', 'blogs.oracle.com/apexcommunity_deutsch', null, null
); 
insert into webshots values (
  'nodejs.org', 'nodejs.org', null, null
);
insert into webshots values (
  'BLOG SQL und PLSQL', 'sql-plsql-de.blogspot.com', null, null
);

commit;

Als nächstes braucht es das Node-Paket webshot. Ihr könnt es in gewohnter Manier mit npm installieren. Navigiert dazu in euer Arbeitsverzeichnis und führt dort npm install webshot aus.

# npm install webshot
> phantomjs@1.9.18 install /home/oracle/node/work/node_modules/webshot/node_modules/phantomjs

:

+-- tmp@0.0.28 (os-tmpdir@1.0.1)
+-- phantomjs@1.9.18 (which@1.0.9, progress@1.1.8, kew@0.4.0, adm-zip@0.4.4, request-progress@0.3.1, npmconf@2.1.1, request@2.42.0, fs-extra@0.23.1)
#

Danach kommt das Node-Programm selbst. Es soll sich mit der Datenbank verbinden, dann ein SELECT auf die Tabelle WEBSHOTS durchführen. Für jede Zeile soll ein Webshot anhand der Adresse in der Spalte URL gemacht und das Bild in IMAGE abgelegt werden. Der Code ist wie folgt - natürlich müsst Ihr die Koordinaten der Datenbank im Aufruf von oracledb.getConnection anpassen.

var webshot = require('webshot');
var oracledb = require('oracledb');

var options = {
  screenSize: { width: 640 , height: 480 }
 ,shotSize:   { width: 640 , height: 600 }
 ,userAgent:  "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:42.0) Gecko/20100101 Firefox/42.0"
}

function doWebshot(connection, gresult, ix) {
  var pName = gresult.rows[ix][0];
  var pUrl  = gresult.rows[ix][1];
  var pRows = gresult.rows.length;

  connection.execute(
    "UPDATE WEBSHOTS set TIME_TAKEN = SYSDATE, IMAGE = EMPTY_BLOB() where NAME = :name returning IMAGE into :lob",
    { name: pName, lob: {type: oracledb.BLOB, dir: oracledb.BIND_OUT} },
    { autoCommit: false },
    function(err, result) {
      if (err) { console.error(err.message); return; }
      if (result.rowsAffected != 1 || result.outBinds.lob.length != 1) { console.error('Error getting a LOB locator'); return; }

      var slob = result.outBinds.lob[0];
      slob.on('error', function(err) { console.error(err); });

      // Hier passiert es: Node.js macht ein Foto der Webseite
      console.log('Grabbing Webshot for ' + pName);
      var inStream = webshot(pUrl,options);
      inStream.on('error', function(err) { console.error(err); });

      // Die Funktion pipe() überträgt die Bytes in den BLOB-Locator der Datenbank.
      inStream.pipe(slob);

      // Wenn alle Bytes in den LOB geschrieben sind: COMMIT und nächste Zeile.
      slob.on('finish', function() {
        connection.commit(function(err) {
          if (err) {
            console.error(err.message);
          } else {
            console.log("Webshot " + (ix+1) + " of " + pRows + " for " + pName + " stored.");
            if (ix < pRows-1 ) {
              doWebshot(connection, gresult, ix + 1);
            }
          }
        });
      });
    }
  );
}

oracledb.getConnection(
  {
    user          : "scott",
    password      : "tiger",
    connectString : "dbserver:1521/orcl"
  },
  function(err, connection) {
    if (err) {console.error(err.message); return;}
    connection.execute(
      "SELECT NAME, URL from WEBSHOTS",
      [],
      function(err, result) {
        if (err) {console.log('%s', err.message); return;}
        console.log("SELECT successful: Found " + result.rows.length + " rows.");
        doWebshot(connection,result,0);
      }
    );
  });

Speichert diesen Code als Datei webshot.js ab. Nun könnt Ihr die Screenshots generieren lassen.

# node webshot.js
SELECT successful: Found 4 rows.
Grabbing Webshot for BLOG JSON, REST und Oracle12c
Webshot 1 of 4 for BLOG JSON, REST und Oracle12c stored.
Grabbing Webshot for APEX Community
Webshot 2 of 4 for APEX Community stored.
Grabbing Webshot for Nodejs.org
Webshot 3 of 4 for Nodejs.org stored.
Grabbing Webshot for BLOG SQL, PLSQL
Webshot 4 of 4 for BLOG SQL, PLSQL stored.
#

Zum Visualisieren habe ich mir eine kleine APEX-Anwendung gebaut - aber man kann natürlich alles hernehmen, was Bilder in einer Tabelle anzeigt.

Node.js eröffnet wirklich sehr interessante Möglichkeiten - das hier ist allenfalls der Anfang. Viel Spaß beim Ausprobieren und beim Entdecken der Möglichkeiten.

Montag, 19. Oktober 2015

Modern Application Development - ab Januar 2016

Im Januar 2016 starten Karin Patenge, Rainer Willems, Beda Hammerschmidt, Kai Donato, Dietmar Aust und ich - in Zusammenarbeit mit der DOAG eine Online-Videoserie zum Thema Modern Application Development - in der Praxis.

In acht Vidoes geht es um JSON, REST-Services, HTML5, Node.js und ähnliche, aktuell heiß diskutierte Entwicklerthemen. Alles wird im Bezug zum Unternehmenseinsatz betrachtet - die Integration in die IT-Landschaft oder mit der Oracle-Datenbank spielt also eine besondere Rolle. Die Teilnahme ist auch hier kostenlos - wenn Ihr auf aktuellem Stand bleiben möchtet, meldet euch gleich an (Klickt oben rechts auf Anmelden). Abgeschlossen wird die Reihe mit einer Online Q & A, bei der Ihr die Gelegenheit habt, mit den Experten zu diskutieren und eure Fragen zu stellen.

Schaut mal rein - sehen wir uns ...?

Dienstag, 29. September 2015

Dateien in Tabellen speichern: Komplette REST-API mit ORDS

Heute geht es doch nochmals um ORDS - das angekündigte Posting zu Node.js muss noch etwas warten. Ich möchte heute zeigen, wie Ihr mit der Oracle-Datenbank und ORDS einen REST-Service aufsetzt, der Dateien in einer Tabelle speichert und aus dieser abrufen kann. PUT und POST Requests sollen Dateien (Bilder, Dokumente) in eine Tabelle laden, GET-Requests sollen sie abrufen; DELETE logischerweise löschen. Dazu soll genau ein REST-Modul entstehen, welches diese vier Fälle (und noch einen mehr) komplett abdeckt.
Im letzten Blog-Posting wurde ja das neue rest-Kommando im neuen SQL Developer Command Line Interface (sqlcl vorgestellt - die Ausgabe dieses Tools ist die Vorlage für das Erzeugen unseres neuen REST-Service, denn ich möchte euch gerne die Skripte zeigen; die lassen sich doch wesentlich leichter nachvollziehen als das Klicken in einem Werkzeug. Wir werden in diesem Posting viel mit dem (derzeit undokumentierten) PL/SQL-Paket ORDS_SERVICES arbeiten.
Dieses Beispiel lässt sich übrigens auch ganz wunderbar im Oracle Database Cloud Service nachvollziehen; das ist sicherlich einer der einfachsten und schnellsten Wege, an einen laufenden Datenbank-Service im Internet zu kommen.
Zuerst braucht es, wie immer in einer Datenbank, eine Tabelle. Die legen wir auch genz einfach an. Dieses Blog-Posting geht davon aus, dass euer Datenbankschema DOCUMENTS heißt - passt die Skripte bei Bedarf einfach an.
create table my_documents(
  id           number(10)   generated always as identity
 ,mimetype     varchar2(200) 
 ,content      blob
 ,created      date         default sysdate not null
 ,last_updated date         default sysdate not null
 ,constraint  pk_mydocuments primary key (id)
)
/
Danach geht es an die Erstellung der REST-Services. Der erste Service soll zum Hochladen von Dokumenten dienen. Ihr könnt die REST-Services mit dem SQL Developer anlegen, in diesem Blog-Posting zeige ich aber den Skriptbasierten Ansatz; Ihr könnt also in eurem SQL-Tool mit Copy & Paste arbeiten. Zuerst sollte das Datenbankschema für REST-Zugriffe freigegeben werden (ORDS.ENABLE_SCHEMA). Führt diesen Call nicht als DBA; sondern als User DOCUMENTS aus. Ich gehe davon aus, dass euer ORDS bereits läuft - wenn nicht, könnt Ihr hier Details zum initiale Setup nachlesen.
BEGIN
  ORDS.ENABLE_SCHEMA(
    p_enabled             => TRUE,
    p_schema              => 'DOCUMENTS',
    p_url_mapping_type    => 'BASE_PATH',
    p_url_mapping_pattern => 'documents',
    p_auto_rest_auth      => FALSE
  );
END;
/

COMMIT
/
Als nächstes erzeugen wir den REST-Service. Das folgende Skript löscht zuerst ein etwaiges bereits definiertes REST-Modul docmodule; danach wird es neu angelegt. Ein Module ist als "Container" für mehrere zusammengehörige REST-Dienste zu verstehen. Direkt danach kommt die Definition der URL-Templates. Einem URL-Template können dann mit ORDS_SERVICES.ADD_HANDLER die REST-Dienste zugeordnet werden - und zwar genau ein Dienst pro HTTP-Methode (GET, PUT, POST, DELETE und andere).
declare
  l_moduleid   number;
  l_templateid number;
  l_handlerid  number;
begin
  -- evtl. vorhandenes REST-Modul löschen
  ords_services.delete_module('docmodule');

  -- REST Modul anlegen
  l_moduleid := ords_services.create_module(
    p_name       => 'docmodule'
   ,p_uri_prefix => 'doc/'
   ,p_status     => 'PUBLISHED'
  );

  -- 1. URL-Template
  l_templateid := ords_services.add_template(
    p_module_id    => l_moduleid
   ,p_uri_template => 'documents/'
  );

  -- POST-Handler für das URL Template. Der REQUEST führt
  -- PL/SQL-Code aus und speichert die per Request hochgeladene Datei (:body) in die
  -- Tabelle ab.
  l_handlerid := ords_services.add_handler(
    p_template_id  => l_templateid
   ,p_source_type  => ords.source_type_plsql
   ,p_source       => 'declare
                         l_id my_documents.id%type;
                       begin 
                        insert into my_documents(
                          mimetype, content
                        ) values (
                          :content_type, :body
                        ) returning id into l_id; 
                        owa_util.mime_header(''application/json'', true);
                        htp.p(''{"status": "created", "id": ''||l_id||''}'');
                       end;'
   ,p_method       => 'POST'
  );
end;
/

commit
/
Der entscheidende Code befindet sich im Aufruf von ORDS_SERVICES.ADD_HANDLER. Hier wird als Argument für P_SOURCE ein PL/SQL-Block übergeben. Der macht im Grunde nichts weiter als ein SQL INSERT in die oben erzeugte Tabelle MY_DOCUMENTS. Wenn Ihr hier PL/SQL-Code schreibt, stellt ORDS euch auch einige Umgebungsvariablen bereit: Zwei seht ihr hier: :body enthält den konkreten Request Body des POST-Requests und :content_type enthält den Mime-Type, den der REST-Client im HTTP-Header Content-Type mitgeschickt hat. Es gibt aber noch ein paar mehr - dieses Blog-Posting von Jon Dixon listet sie auf. Wenn der INSERT erfolgt ist, wird die neu erzeugte ID des Dokument in einem JSON-Format zurückgegeben. Probiert den neuen REST-Service mal aus - mit dem Chrome Advanced REST Client sieht das ganze so aus ...
... und mit dem Kommandozeilentool curl so (hier sind noch einige Statusmeldungen dabei). Ich persönlich habe eine Vorliebe für curl zum Testen von REST-Services entwickelt - damit ist man einfach nicht mehr den "Launen" der verschiedenen GUI-Werkzeuge ausgeliefert (und irgendeine Marotte haben sie alle).
D:\>curl -v -X POST "http://localhost:8081/ords/documents/doc/documents/" --data-binary "@ar.png" -H "Content-Type:image/png"
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8081 (#0)
> POST /ords/documents/doc/documents/ HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.44.0
> Accept: */*
> Content-Type:image/png
> Content-Length: 46098
> Expect: 100-continue
>
< HTTP/1.1 100 Continue
* We are completely uploaded and fine
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=UTF-8
< Transfer-Encoding: chunked
<
{"status": "created", "id": 2}
* Connection #0 to host localhost left intact
Schauen wir uns die Daten in der Tabelle an - das geschieht im SQL-Client.
SQL> select * from my_documents;

  ID MIMETYPE   CONTENT                        CREATED             LAST_UPDATED
---- ---------- ------------------------------ ------------------- -------------------
   3 image/png  89504E470D0A1A0A0000000D494844 29.09.2015 11:01:22 29.09.2015 11:01:22
                52000003850000021B0802000000D3
                A9AC70000000097048597300000EC4
                00000EC401952B0E1B000020004944
                4154789CEC9D796055C5D9FF9F67CE
                0D9B408424

   4 image/png  89504E471A01DFDA562FD82A0151AA 29.09.2015 11:01:58 29.09.2015 11:01:58
                C43D8A8822EE0BB1EFFB6B53B1AE68
                5B3535EDAB2501AD5A2D4A6D956A5B
                631537625D63DFAA80D1BEF5B56025
                B26917D942EE39F3FCFE983973E62C
                F7DE736F2E
Das ist doch ein sehr einfacher und angenehmer Weg, Dateien in eine Tabelle zu laden, oder? Und damit ist der erste REST-Service (Upload) auch schon fertig. Die nächsten drei Services liegen nahe. Für das URL-Pattern /doc/documents/{id} soll das Dokument mit der jeweiligen ID ...
  • mit der Methode GET abgerufen
  • mit der Methode PUT aktualisiert
  • mit der Methode DELETE gelöscht
... werden. Das Skript dafür sieht so aus (damit Ihr einfaches Copy & Paste machen könnt, hier nochmals das vollständige Skript. Der bereits vorhandene erste Teil ist ausgegraut.
declare
  l_moduleid   number;
  l_templateid number;
  l_handlerid  number;
begin
  -- evtl. vorhandenes REST-Modul löschen
  ords_services.delete_module('docmodule');

  -- REST Modul anlegen
  l_moduleid := ords_services.create_module(
    p_name       => 'docmodule'
   ,p_uri_prefix => 'doc/'
   ,p_status     => 'PUBLISHED'
  );

  -- 1. URL-Template
  l_templateid := ords_services.add_template(
    p_module_id    => l_moduleid
   ,p_uri_template => 'documents/'
  );

  -- POST-Handler für das URL Template. Der REQUEST führt
  -- PL/SQL-Code aus und speichert die per Request hochgeladene Datei (:body) in die
  -- Tabelle ab.
  l_handlerid := ords_services.add_handler(
    p_template_id  => l_templateid
   ,p_source_type  => ords.source_type_plsql
   ,p_source       => 'declare
                         l_id my_documents.id%type;
                       begin 
                        insert into my_documents(
                          mimetype, content
                        ) values (
                          :content_type, :body
                        ) returning id into l_id; 
                        owa_util.mime_header(''application/json'', true);
                        htp.p(''{"status": "created", "id": ''||l_id||''}'');
                       end;'
   ,p_method       => 'POST'
  );

  -- 2. URL-Template
  l_templateid := ords_services.add_template(
    p_module_id    => l_moduleid
   ,p_uri_template => 'documents/{id}'
  );

  -- GET-Handler für das URL Template. Das angefragte Dokument ({id}) wird
  -- per SQL Query abgerufen und ausgegeben. Dazu bietet ORDS einen fertigen
  -- "Source Type" MEDIA QUERY an.
  l_handlerid := ords_services.add_handler(
    p_template_id  => l_templateid
   ,p_source_type  => ords.source_type_media
   ,p_source       => 'select mimetype, content from my_documents where id = :id'
   ,p_method       => 'GET'
  );

  -- DELETE-Handler für das URL Template. Es wird (per PL/SQL-Block) eine 
  -- SQL DELETE-Anweisung ausgeführt.
  l_handlerid := ords_services.add_handler(
    p_template_id  => l_templateid
   ,p_source_type  => ords.source_type_plsql
   ,p_source       => 'begin 
                        delete from my_documents where id = :id; 
                        owa_util.mime_header(''application/json'', true);
                        htp.p(''{"status": "deleted"}'');
                       end;'
   ,p_method       => 'DELETE'
  );

  -- PUT-Handler für das URL Template. Es wird (per PL/SQL-Block) eine 
  -- SQL UPDATE-Anweisung ausgeführt.
  l_handlerid := ords_services.add_handler(
    p_template_id  => l_templateid
   ,p_source_type  => ords.source_type_plsql
   ,p_source       => 'begin 
                        update my_documents set content = :body, mimetype = :content_type, last_updated = sysdate where id = :id; 
                        owa_util.mime_header(''application/json'', true);
                        htp.p(''{"status": "updated", "id": ''||:id||''}'');
                       end;'
   ,p_method       => 'PUT'
  );
end;
/

commit
/
Testen wir die neuen REST-Dienste.
  • Beginnen wir mit GET (das Dokument soll in die Datei file.bin) gespeichert werden:
    D:\>curl -v -X GET http://localhost:8081/ords/documents/doc/documents/3 -o file.bin
      % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                     Dload  Upload   Total   Spent    Left  Speed
      0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 127.0.0.1...
    * Connected to localhost (127.0.0.1) port 8081 (#0)
    > GET /ords/documents/doc/documents/5 HTTP/1.1
    > Host: localhost:8081
    > User-Agent: curl/7.44.0
    > Accept: */*
    >
    < HTTP/1.1 200 OK
    < Content-Type: image/png
    < ETag: "ZuV0opwWReMeFm8NwmK8IPxuZfldcE+uB7n95A2Bwg6VE9z89b31m2VMNiKGWqTeD+IZ41hpPHAcZppbuOw/yA=="
    < Transfer-Encoding: chunked
    <
    { [16214 bytes data]
    100 46098    0 46098    0     0   261k      0 --:--:-- --:--:-- --:--:--  288k
    * Connection #0 to host localhost left intact
    
    An der Ausgabe (Content-Type) könnte der Client auch erkennen, was da konkret vom REST-Service ausgeliefert wird. In diesem Fall wäre es ein Bild (image/png).
  • Dann löschen wir ein Dokument ...
    D:\>curl -v -X DELETE http://localhost:8081/ords/documents/doc/documents/3
    *   Trying 127.0.0.1...
    * Connected to localhost (127.0.0.1) port 8081 (#0)
    > DELETE /ords/documents/doc/documents/3 HTTP/1.1
    > Host: localhost:8081
    > User-Agent: curl/7.44.0
    > Accept: */*
    >
    < HTTP/1.1 200 OK
    < Content-Type: application/json
    < Transfer-Encoding: chunked
    <
    {"status": "deleted"}
    * Connection #0 to host localhost left intact
    
  • Und schließlich noch ein Update ...
    D:\>curl -v -X PUT "http://localhost:8081/ords/documents/doc/documents/4" --data-binary "@index.html" -H "Content-Type:text/html"
    *   Trying 127.0.0.1...
    * Connected to localhost (127.0.0.1) port 8081 (#0)
    > PUT /ords/documents/doc/documents/4 HTTP/1.1
    > Host: localhost:8081
    > User-Agent: curl/7.44.0
    > Accept: */*
    > Content-Type:text/html
    > Content-Length: 381
    >
    * upload completely sent off: 381 out of 381 bytes
    < HTTP/1.1 200 OK
    < Content-Type: text/html; charset=UTF-8
    < Transfer-Encoding: chunked
    <
    {"status": "updated", "id": 4}
    * Connection #0 to host localhost left intact
    
Fehlt zum Abschluß nur noch eine Übersicht über die bereits vorhandenen Dokumente - hierzu bietet sich wiederum ein GET-Request auf die Collection selbst an - also das URL-Pattern /doc/documents/. Wenn eine ID angehägt wird, soll das konkrete Dokument ausgeliefert werden (das ist bereits fertig), wenn keine ID angehängt wird, soll eine Übersicht - im JSON-Format mit Links zu den einzelnen Dokumenten - ausgegeben werden. Hier wiederum das komplette Skript - die Neuerungen sind farblich hervorgehoben.
declare
  l_moduleid   number;
  l_templateid number;
  l_handlerid  number;
begin
  -- evtl. vorhandenes REST-Modul löschen
  ords_services.delete_module('docmodule');

  -- REST Modul anlegen
  l_moduleid := ords_services.create_module(
    p_name       => 'docmodule'
   ,p_uri_prefix => 'doc/'
   ,p_status     => 'PUBLISHED'
  );

  -- 1. URL-Template
  l_templateid := ords_services.add_template(
    p_module_id    => l_moduleid
   ,p_uri_template => 'documents/'
  );

  -- POST-Handler für das URL Template. Der REQUEST führt
  -- PL/SQL-Code aus und speichert die per Request hochgeladene Datei (:body) in die
  -- Tabelle ab.
  l_handlerid := ords_services.add_handler(
    p_template_id  => l_templateid
   ,p_source_type  => ords.source_type_plsql
   ,p_source       => 'declare
                         l_id my_documents.id%type;
                       begin 
                        insert into my_documents(
                          mimetype, content
                        ) values (
                          :content_type, :body
                        ) returning id into l_id; 
                        owa_util.mime_header(''application/json'', true);
                        htp.p(''{"status": "created", "id": ''||l_id||''}'');
                       end;'
   ,p_method       => 'POST'
  );

  -- GET-Handler für das URL Template "documents/" (ohne ID). Es wird (Source Type "feed")
  -- eine Übersicht mit allen Dokumenten ausgegeben.
  l_handlerid := ords_services.add_handler(
    p_template_id  => l_templateid
   ,p_source_type  => ords.source_type_feed
   ,p_source       => 'select 
                         id, 
                         mimetype, 
                         dbms_lob.getlength(content) as doc_size, 
                         created, 
                         last_updated 
                       from my_documents'
   ,p_method       => 'GET'
  );

  -- 2. URL-Template
  l_templateid := ords_services.add_template(
    p_module_id    => l_moduleid
   ,p_uri_template => 'documents/{id}'
  );

  -- GET-Handler für das URL Template. Das angefragte Dokument ({id}) wird
  -- per SQL Query abgerufen und ausgegeben. Dazu bietet ORDS einen fertigen
  -- "Source Type" MEDIA QUERY an.
  l_handlerid := ords_services.add_handler(
    p_template_id  => l_templateid
   ,p_source_type  => ords.source_type_media
   ,p_source       => 'select mimetype, content from my_documents where id = :id'
   ,p_method       => 'GET'
  );

  -- DELETE-Handler für das URL Template. Es wird (per PL/SQL-Block) eine 
  -- SQL DELETE-Anweisung ausgeführt.
  l_handlerid := ords_services.add_handler(
    p_template_id  => l_templateid
   ,p_source_type  => ords.source_type_plsql
   ,p_source       => 'begin 
                        delete from my_documents where id = :id; 
                        owa_util.mime_header(''application/json'', true);
                        htp.p(''{"status": "deleted"}'');
                       end;'
   ,p_method       => 'DELETE'
  );

  -- PUT-Handler für das URL Template. Es wird (per PL/SQL-Block) eine 
  -- SQL UPDATE-Anweisung ausgeführt.
  l_handlerid := ords_services.add_handler(
    p_template_id  => l_templateid
   ,p_source_type  => ords.source_type_plsql
   ,p_source       => 'begin 
                        update my_documents set content = :body, mimetype = :content_type, last_updated = sysdate where id = :id; 
                        owa_util.mime_header(''application/json'', true);
                        htp.p(''{"status": "updated", "id": ''||:id||''}'');
                       end;'
   ,p_method       => 'PUT'
  );
end;
/

commit
/
Probieren wir auch diesen Service aus ... erst Mal mit curl ...
D:\>curl -X GET -v http://localhost:8081/ords/documents/doc/documents/
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8081 (#0)
> GET /ords/documents/doc/documents/ HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.44.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< ETag: "Fr7HbBdEKCjVbKL+qMgCZu5WTPRr7dRMvrsdVzUskvYWkqM/OCEVrCeqYEbwUZ7Ki4u3deYNXSxIVJhIidX5Uw=="
< Transfer-Encoding: chunked
<
{"items":[{"uri":{"$ref":"http://localhost:8081/ords/documents/doc/documents/3"},"id":3,"mimetype":"image/png","doc_size
":46098,"created":"2015-09-29T09:01:22Z","last_updated":"2015-09-29T09:01:22Z"},{"uri":{"$ref":"http://localhost:8081/or
ds/documents/doc/documents/4"},"id":4,"mimetype":"text/html","doc_size":381,"created":"2015-09-29T09:01:58Z","last_updat
ed":"2015-09-29T09:01:58Z"}],"first":{"$ref":"http://localhost:8081/ords/documents/doc/documents/"}}
* Connection #0 to host localhost left intact
Dieser einfache GET-Request kann auch im Browser getestet werden ...
Auf diese Art und Weise könnt Ihr nun noch weitere Dienste hinzufügen und so einen komplette REST-Schnittstelle für die Tabelle schaffen - in der Praxis braucht es sicherlich noch ein paar Tabellen mehr. Die REST-Dienste können dann von beliebigen Anwendungen genutzt werden; ob Node.js, PHP, Java oder .NET: Alle greifen über die einheitliche REST-Schnittstelle zu; der konkrete Datenbanktreiber wird bedeutungslos und die Tabellenzugriffe werden so einfach wie noch nie. Viel Spaß damit.

Mittwoch, 23. September 2015

Oracle Rest Data Services (ORDS) jetzt auch im SQLcl

Heute gibt es nur ein sehr kurzes Blog-Posting (wieder von Rainer Willems), was aber gut zu den bereits vorhandenen Postings zu Oracle REST Data Services passt. In den nächsten Tagen kommt dann nochmals was umfangreicheres - zu Node.js - versprochen ;-)
Mit der aktuellen Version des neuen command line Interfaces SQLcl  (Release Candidate 4.2.0.15.254.0747) gibt es nun auch eine direkte Unterstützung des ORDS und der REST Data Services. Eine kleine, aber sehr nützliche Hilfestellung.
SQL> help rest

REST ------ REST allows to export ORDS 3.X services. REST export - All modules REST export <module_name> - Export a specific module REST export <module_prefix> - Export a specific module related to a given prefix REST modules - List the available modules REST privileges - List the existing privileges REST schemas - List the avaiable schemas
So bekommt man einen schnellen Überblick über die vorhandenen Einstellungen und eine sehr schnelle Export-Möglichkeit der Module.REST modules zeigt schön die vorhandenen Module (vorher set sqlformat ansiconsole absetzten, dann sieht es auch hübsch aus).


Mit REST EXPORT <module_name> bekommt man ein ein kleines PL/SQL-Skript generiert, mit dem das genannte Modul per Kommandozeile angelegt werden kann.


Damit erhält man also auch gleich ein kleines anschauliches Beispiel zum Entwickeln von Services mit dem Package ORDS_METADATA geliefert. In den vorhergehenden Posts wurde das Package ORDS mit der Methode CREATE_SERVICE zum Anlegen solcher Services verwendet. Mit diesem kann man aber aktuell nur einen Handler pro Modul anlegen.

Dienstag, 1. September 2015

REST-Services - völlig ohne Schema - mit "SODA": So geht's.

Ich freue mich, dass heutige Blog-Posting von Rainer Willems vorstellen zu können. Rainer zeigt, wie man SODA (Simple Oracle Document Access) einrichtet und damit REST-Services in der Oracle-Datenbank - völlig ohne Schema bereitstellen kann. Jedes beliebige JSON wird entgegengenommen. Ihr könnt Rainer auf Twitter folgen: @josifabr.

Als Abwechslung zur skriptbasierten Vorgehensweise, die ich in den letzten Blog-Postings verwendet habe, seht Ihr hier die Vorgehensweise mit dem SQL Developer. Viel Spaß beim Lesen und Ausprobieren.



Heute wollen wir uns mit SODA beschäftigen. Hierbei handelt es sich nicht um eine dem aktuellen Wetter angepasste Erfrischung, sondern um Simple Oracle Document Access. Carsten hat im Blog schon gezeigt, wie einfach eine bestehende Tabelle mit ORDS REST-fähig gemacht werden kann oder wie ein solcher Service mit PL/SQL aufgebaut werden kann.

Dies erfüllt aber noch nicht die Anforderungen von Entwicklern, vollständig flexibel - also ohne vorgegebenes Schema - arbeiten so können. NoSQL-Datenbanken preisen dies als Vorteil an. Auch Oracle wartet hier mit einer NoSQL-Datenbank auf. Habe ich aber ohnehin Oracle 12c im Einsatz, wäre es doch viel einfacher, diese eine (unter vielen, vielen anderen) Anforderung mit der gleichen Datenbank abzudecken. Und genau hier kommt SODA ins Spiel. SODA bietet die Möglichkeit die Datenbank als flexiblen schemaless Document-Store zu verwenden: Oracle DB als NoSQL JSON document store

Wir betrachten betrachten im Folgenden nun SODA for REST, ebenfalls verfügbar ist SODA for Java.

Voraussetzung auf Datenbankseite ist das Release 12.1.0.2 und der Patch 20885778 Ist dieser installiert und der ORDS wie beschrieben installiert, kann es losgehen; oder besser gesagt, sind wir eigentlich schon fertig.

Der Vollständigkeit halber hier die Beschreibung der Installation des ORDS mit dem SQL Developer. Unter Tools-REST data Services findet man den Punkt Install

Wir wählen entweder den in der aktuellen SQL Developer Version mitkommenden ORDS oder einen anderen, ggf. aktuelleren (Im momentan aktuellen SQL Developer 4.1.0.19 befindet sich auch der momentan aktuellste ORDS). Dazu wählen wir eine Lokation für die Konfigurationsfiles:


Wir verbinden uns mit der Datenbank und legen einen Public User an, welcher vergleicbar mit dem APEX_PUBLIC_USER ist, nur eben für die REST-Services.


Dies muss als sys geschehen, da in der Datenbank das Metadaten-Repository für die Services angelegt wird (Schema ORDS_METADATA)


Wir legen die Tablespaces fest.


und ignorieren in den kommenden beiden Steps die Einstellungen zum PL/SQL Gateway bzw. APEX.



Wir möchten die Services zum Testen Standalone laufen lassen und definieren den HTTP Port.



Achtung. Wer hier in Konflikt mit einem bereits verwendeten Port kommt und die Installation dann ein weiteres mal versuchen möchte, muss erst die Konfigurationsdatei im Filesystem löschen, da sonst weiterhin die beim ersten Versuch angegebene Port-Nummer verwendet wird.

Für Entwicklungszwecke definieren wir noch einen Admin- und einen Entwicklungsuser (keine Datenbankuser).




Ist der ORDS dann gestartet, sollten wir auch die dementsprechende Anzeige im Log bekommen.



Läuft der ORDS dann, erkennt man das auch an einem roten Quadrat in der Menuleiste des SQL Developer. Clickt man dieses an, wird der ORDS beendet ...


... und die Installation kann unter Tools - REST Data Services - Run wieder gestartet werden (wobei man wieder ein paar Fragen beantworten muss)





Jetzt braucht man nur noch das gewünschte Datenbankschema (das was nachher herauskommt ist trotzdem "schema-less"!) "REST-fähig" machen. Das kann man einfach über das Kontextmenu in den Verbindungen machen.


Der Schemaalias wird nachher in den URLs der REST Services verwendet.



Letztendlich wird hier die Prozedur ORDS.ENABLE_SCHEMA ausgeführt:
DECLARE
  PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN

    ORDS.ENABLE_SCHEMA(p_enabled => TRUE,
                       p_schema => 'SCOTT',
                       p_url_mapping_type => 'BASE_PATH',
                       p_url_mapping_pattern => 'scott',
                       p_auto_rest_auth => FALSE);
    
    commit;
END;

FERTIG !!!! Die Datenbank kann nun via REST als JSON Document Store verwendet werden. Zum Testen deaktivieren wir nur noch die Security (als User scott in diesem Fall hier). Aber bitte nur zum Testen:
begin
     ords.delete_privilege_mapping('oracle.soda.privilege.developer','/soda/*');
end;
 /

Ich verwende übrigens gerne den Postman, eine Extension für den Chrome-Browser, um REST-Zugriffe zu testen.

Mittels eines GET auf http://mymachine:port/myords/myschema/soda/latest/ kann man sich nun anschauen, welche Collections in unserem Schema vorhanden sind:



Es sind natürlich noch keine da. Eine neue Collection läßt sich mittels eines PUT unter Angabe des gewünschten Collection-Namens erzeugen



Diese erscheint direkt als Datenbank-Tabelle im dementsprechenden Schema.


Neben der ID und dem JSON Dokument als solchem werden noch die Version (von SODA), das Erstelldatum und die letzte Modifikation in der Tabelle gepflegt.



Frage ich nun nochmal alle Collections ab, erhalte ich meine gerade angelegte Collection:
{
  "items": [
    {
      "name": "mycollection",
      "properties": {
        "schemaName": "SCOTT",
        "tableName": "mycollection",
        "keyColumn": {
          "name": "ID",
          "sqlType": "VARCHAR2",
          "maxLength": 255,
          "assignmentMethod": "UUID"
        },
        "contentColumn": {
          "name": "JSON_DOCUMENT",
          "sqlType": "BLOB",
          "compress": "NONE",
          "cache": true,
          "encrypt": "NONE",
          "validation": "STANDARD"
        },
        "versionColumn": {
          "name": "VERSION",
          "type": "String",
          "method": "SHA256"
        },
        "lastModifiedColumn": {
          "name": "LAST_MODIFIED"
        },
        "creationTimeColumn": {
          "name": "CREATED_ON"
        },
        "readOnly": false
      },
      "links": [
        {
          "rel": "canonical",
          "href": "http://localhost:8080/ords/scott/soda/latest/mycollection"
        }
      ]
    }
  ],
  "more": false
}

Die Dokumente werden hier letztendlich in einem BLOB abgelegt. Dieses BLOB wird mittels eines Check-Constraints mit der Bedingung
      "JSON_DOCUMENT" is json format json
überwacht, damit dort auch nur JSON Dokumente abgelegt werden können. Spätestens hier wird klar, weshalb wir für SODA 12.1.0.2 benötigen, da die JSON-Funktionalität hier erst eingeführt wurde.

Mit einem einfachen POST kann man nun ein JSON Dokument in die Collection schieben ...


... und bekommt in der Antwort auch die ID zurückgeliefert, die dieses Dokument bekommen hat.


Fragt man nun die Collection ab (also ein GET auf http://localhost:8080/ords/scott/soda/latest/mycollection), bekommt man eine Aufstellung der Daten:
{
  "items": [
    {
      "id": "CBD1D6D35A66419390F4DB73F0B842E7",
      "etag": "5B4B2052A0927083D1C835CAD4DC59BA4AA6D6C8F99264775EEABE9E6F9BCE49",
      "lastModified": "2015-07-01T15:58:37.925000Z",
      "created": "2015-07-01T15:58:37.925000Z",
      "links": [
        {
          "rel": "self",
          "href": "http://localhost:8080/ords/scott/soda/latest/mycollection/CBD1D6D35A66419390F4DB73F0B842E7"
        }
      ],
      "value": {
        "first": "Guenther",
        "last": "Stuerner",
        "address": {
          "Street": "Liebknechtstrasse",
          "City": "Stuttgart",
          "PLZ": "70565"
        }
      }
    },
    {
      "id": "E4D885A4B3F04FBC8457482E1DA0C01E",
      "etag": "511BAD96CC7C4037E8B20E5E65EBC29E7A034A273684AC2EA9029157C4EF1812",
      "lastModified": "2015-07-01T15:58:20.779000Z",
      "created": "2015-07-01T15:58:20.779000Z",
      "links": [
        {
          "rel": "self",
          "href": "http://localhost:8080/ords/scott/soda/latest/mycollection/E4D885A4B3F04FBC8457482E1DA0C01E"
        }
      ],
      "value": {
        "first": "Rainer",
        "last": "Willems",
        "address": {
          "Street": "Robert-Bosch-Strasse",
          "City": "Dreieich",
          "PLZ": "63303"
        }
      }
    },
    {
      "id": "27200D52F15C4A8F82868C353112E350",
      "etag": "51948BD2C13AE6A0962304FCC325743D1A5F976D687D0A03BFB732D2436CF316",
      "lastModified": "2015-07-01T15:58:29.077000Z",
      "created": "2015-07-01T15:58:29.077000Z",
      "links": [
        {
          "rel": "self",
          "href": "http://localhost:8080/ords/scott/soda/latest/mycollection/27200D52F15C4A8F82868C353112E350"
        }
      ],
      "value": {
        "first": "Paul",
        "last": "Wehner",
        "address": {
          "Street": "Robert-Bosch-Strasse",
          "City": "Dreieich",
          "PLZ": "63303"
        }
      }
    }
  ],
  "hasMore": false,
  "count": 3,
  "offset": 0,
  "limit": 100,
  "totalResults": 3,
  "links": []
}
Hierbei lassen sich natürlich Parameter mitgeben, um die Ausgabe zu Filtern (darüber werden wir noch berichten). Für die Abfrage eines einzelnen JSON-Dokumentes hänge ich einfach die ID an das GET an.



Folgende Befehle sind nun also möglich
PUT    - http://localhost:8080/ords/scott/soda/latest/mycollection     Anlegen einer Collection
GET    - http://localhost:8080/ords/scott/soda/latest/                 Anzeige aller Collections
GET    - http://localhost:8080/ords/scott/soda/latest/mycollection     Anzeige des Inhaltes einer Collection
GET    - http://localhost:8080/ords/scott/soda/latest/mycollection/id  Anzeige eines JSON-Dokumentes
POST   - http://localhost:8080/ords/scott/soda/latest/mycollection     Übergabe und Anlage eines Dokumentes
DELETE - http://localhost:8080/ords/scott/soda/latest/mycollection/id  Löschen eines Dokumentes
  ... besser nicht verwechseln mit
DELETE - http://localhost:8080/ords/scott/soda/latest/mycollection     ... denn dann ist die Collection weg

Weitere Möglichkeiten wie Bulk-Insert und Delete sowie verfügbare Actions sind in der Dokumentation ausführlich beschrieben.
Der riesige Vorteil, den wir nun erhalten, findet sich in der Datenbank. Da diese per SQL mit JSON-Dokumenten umgehen kann, sind der Suche in diesen Dokumenten oder der Verknüpfung dieser mit relationalen Inhalten keine Grenzen gesetzt. Und das mit ein paar einfachen Schritten. Wer will da noch eine zusätzliche Datenbank installieren, verwalten, sichern, ....

Noch ein kleiner Hinweis, falls die Restservices wieder entfernt werden sollen. Da die Metaobjekte aus der Datenbank wieder entfernt werden muss man sich mit SYSDBA Privilegien anmelden. Der Zusatz "as sysdba" wird aber aktuell (SQL Developer 4.1.0.19) im Gegensatz zur Installation vom Tool nicht automatisch angehängt, sondern muss manuel eingetragen werden.


Donnerstag, 6. August 2015

REST-Services: Viele Datenbanken - eine ORDS-Instanz. So geht's.

In diesem Blog-Posting geht es um ein Setup-Thema für die Oracle Rest Data Services (ORDS). Im ersten Blog-Posting ('Auto-REST' für Tabellen) habt Ihr erfahren, wie man ORDS herunterlädt und mit wenigen Handgriffen installiert - dabei wird auch die Verbindung zur Datenbank eingerichtet. Standardmäßig ergibt sich also pro Datenbank eine ORDS-Instanz. Bei mehreren Datenbanken also mehrere ORDS-Instanzen. Möchte man aber über einen HTTP-Endpoint auf alle REST-Services zugreifen, so ergeben sich zwei Möglichkeiten:

  • Man setzt einen Webserver (bspw. Apache) als HTTP-Endpoint auf - Weiterleitungsregeln sorgen dafür, dass bestimmte URL-Schemata auf bestimmte ORDS-Instanzen abgebildet werden.
  • Man setzt nur eine ORDS-Instanz auf und konfiguriert diese für mehrere Datenbanken.

Zentralen Webserver als Endpoint für ORDS-Instanzen einrichten

Wie die erste Variante genau konfiguriert wird, hängt von vielen Details ab. So ist die Frage bedeutsam, ob ORDS im Standalone-Modus läuft oder in einem Java-Server wie Weblogic, Glassfish oder Tomcat deployed wird. Hier ist nur der Standalone-Modus beschrieben, für das Deployment von ORDS in einem Java-Container wird es eigene Blog-Postings geben. Angenommen, es liegen zwei ORDS-Instanzen auf dbserver1 und dbserver2 vor - beide sind bereits mitsamt Datenbankverbindung konfiguriert und jeder - für sich - funktioniert. Die folgenden Schritte müssen natürlich für beide Instanzen gemacht werden.

Navigiert nun zu den Konfigurationsdateien eurer ORDS-Instanzen - und dort zur Datei standalone.properties.

{ORDS-Config-Directory}
`-- ords
    |-- conf
    |   |-- apex_pu.xml
    |-- defaults.xml
    `-- standalone
        `-- standalone.properties

Ändert die Property standalone.context.path in der ersten ORDS-Instanz von /ords auf /restdb1 und in der zweiten Instanz auf /restdb2. Für die erste Instanz sieht das dann so aus.

#Thu Aug 06 10:30:54 CEST 2015
jetty.port=8081
standalone.context.path=/restdb1
standalone.doc.root=/home/oracle/ords/config/restdb1/standalone/doc_root
standalone.static.context.path=/i
standalone.static.do.not.prompt=true

Benennt dann das Verzeichnis {ORDS-Config-Directory}/ords nach {ORDS-Config-Directory}/restdb1 (zweite ORDS-Instanz analog) um. Schließlich benennt Ihr die Java-Datei ords.war (aus welcher der ORDS heraus gestartet wird) nach restdb1.war (zweite ORDS-Instanz analog) um. Die Verzeichnisstruktur sollte dann für die ORDS-Instanz auf dem dbserver1 so aussehen.

.
|-- {ORDS-Config-Directory}
|   `-- restdb1
|       |-- conf
|       |   `-- apex_pu.xml
|       |-- defaults.xml
|       `-- standalone
|           `-- standalone.properties
|-- derby.log

:

|-- readme.html
`-- restdb1.war

Nun könnt Ihr die ORDS-Instanzen starten - nun muss aber das WAR-File restdb1.war bzw. restdb2.war mit "java -jar restdb1.war" gestartet werden. Anstelle des URL-Pfads /ords/ erreicht Ihr eure REST-Services nun unter /restdb1/ bzw. /restdb2/.

http://dbserver1.mydomain.de:8080/restdb1/{...}
http://dbserver2.mydomain.de:8080/restdb2/{...}

Nun geht es an die Konfiguration des Apache Webserver - auch hier gehen wir davon aus, dass dieser bereits installiert und konfiguriert ist. Die Direktiven für das mod_proxy könnten in etwa wie folgt aussehen.

ProxyPreserveHost On

ProxyPass        /restdb1/ http://dbserver1.mydomain.de:8080/restdb1/
ProxyPassReverse /restdb1/ http://dbserver1.mydomain.de:8080/restdb1/

ProxyPass        /restdb2/ http://dbserver2.mydomain.de:8080/restdb2/
ProxyPassReverse /restdb2/ http://dbserver2.mydomain.de:8080/restdb2/

Von nun an werden alle REST-Requests mit dem URL-Pfad /restdb1/ am die ORDS-Instanz auf dem dbserver1 geleitet, die mit dem Pfad /restdb2 folgerichtig an die andere ORDS-Instanz auf dbserver2. Alle REST-Requests werden nur noch an den Apache-Webserver gerichtet, die konkreten ORDS-Instanzen sind nicht mehr sichtbar; der Client hat den Eindruck, als gäbe es nur einen einzigen ORDS-Server.

Der beschriebene Ansatz ist gut machbar, solange man pro Server nur eine Datenbank hat - der Apache-Webserver dient dann sehr schön als zentraler Einstiegspunkt.

ORDS-Instanz für mehrere Datenbanken einrichten

Hat man dagegen mehrere Datenbankinstanzen auf einem Server, so bräuchte man, passend dazu, genauso viele ORDS-Instanzen. Jede ORDS-Instanz ist ein eigener Java-Prozess - optimale Ressourcennutzung sieht anders aus.

Eine andere Variante ist daher es, von vorneherein nur eine ORDS-Instanz aufzusetzen und dann darin zwei oder mehrere Datenbanken zu konfigurieren - wie das geht, ist im folgenden beschrieben. Ausgangspunkt ist eine vorhandene ORDS-Instanz auf dbserver1, die bereits für eine Datenbank konfiguriert ist (siehe erstes Blog-Posting ('Auto-REST' für Tabellen).

Stoppt zunächst diese ORDS-Instanz und konfiguriert die zweite Datenbank danach wie folgt:

$ java -jar ords.war setup --database dbserver2

Das Kommando setup --database richtet eine neue Datenbankverbindung ein - Ihr müsst auf jeden Fall den Namen des Connection Pool an den Aufruf anhängen (hier: dbserver2). Dann werdet Ihr nochmals durch den Dialog zum Setup der Datenbankverbindung geführt.

Enter the name of the database server [dbserver1.mydomain.de]:dbserver2.mydomain.de
Enter the database listen port [1521]:1521
Enter the database service name [orcl]:db-servicename.db.domain
Enter 1 if you want to verify/install Oracle REST Data Services schema or 2 to skip this step [1]:1
Enter the database password for ORDS_PUBLIC_USER:******
Confirm password:******
Enter 1 if you want to use PL/SQL Gateway or 2 to skip this step [1]:2
Enter 1 to specify passwords for Application Express RESTful Services database users (APEX_LISTENER, APEX_REST_PUBLIC_USER) or 2 to skip this step [1]:2
Aug 06, 2015 11:40:07 AM oracle.dbtools.common.config.file.ConfigurationFilesBase update
INFO: Updated configurations: dbserver2_pu
Aug 06, 2015 11:40:07 AM oracle.dbtools.rt.config.setup.SchemaSetup install
INFO: Oracle REST Data Services schema version 3.0.1.177.18.02

Nun ist der Connection Pool für eure zweite Datenbank eingerichtet. Dennoch solltet Ihr die XML-Konfigurationsdateien nochmals prüfen. In der Datei ${ORDS_CONFIG_DIR}/ords/defaults.xml speichert ORDS die Einstellungen, die für alle Datenbankverbindungen gültig sind - im Verzeichnis ${ORDS_CONFIG_DIR}/ords/conf befindet sich sich darüber hinaus eine XML-Datei für jede eingerichtete Datenbank - sie hält die Einstellungen, die nur für diese Datenbankverbindung gültig sind.

{ORDS-Config-Directory}
|-- ords
|   |-- conf
|   |   |-- apex_pu.xml
|   |   `-- dbserver2_pu.xml
|   |-- defaults.xml
|   `-- standalone
|       `-- standalone.properties

Die Datei apex_pu.xml enthält die Konfiguration der Datenbank, die bei der ersten Installation der ORDS-Instanz eingerichtet wurde - dieser Connection-Pool trägt immer den Namen apex. Die Namen weiterer Dateien richten sich nach dem Namen, der bei Einrichtung für den Connection-Pool verwendet wurde. Schaut nun in die neue Datei (dbserver2_pu.xml) hinein. Der Inhalt sollte wie folgt aussehen.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>Saved on Thu Aug 06 11:40:06 CEST 2015</comment>
<entry key="db.hostname">dbserver2.mydomain.de</entry>
<entry key="db.servicename">db-servicename.db.domain</entry>
<entry key="db.username">ORDS_PUBLIC_USER</entry>
<entry key="db.password">@052804911E20317F33FA63EBF21DFC1C76</entry>
</properties>

Wenn etwas fehlt oder nicht richtig eingestellt ist, könnt Ihr es hier korrigieren. Das Passwort für die Datenbankverbindung ist verschlüsselt gespeichert. Wenn Ihr es ändern möchtet, tragt es im Klartext ein, angeführt von einem Ausrufezeichen ("!oracle"). Beim nächsten Start wird ORDS es dann verschlüsseln.

Wenn Ihr den ORDS dann startet, werdet Ihr zunächst feststellen, dass Ihr nun zwar zwei Datenbank-Connection-Pools habt, aber trotzdem keinen einzigen REST-Service mehr erreichen könnt - er wird immer mit einer HTTP-404 Fehlermeldung (Not Found) antworten. Grund ist, dass das automatische Mapping der URL, auf einen REST-Service in einem Datenbankschema, bei mehreren Datenbankverbindungen nicht mehr verfügbar ist. Solange nur eine Datenbankverbindung vorhanden war, konnte eine URL /ords/scott/rest-emp automatisch auf das REST-Modul rest-emp im Datenbankschema SCOTT abgebildet werden - bei zwei oder mehreren Datenbanken geht das nicht mehr.

Also müssen die URL-Mappings manuell gepflegt werden. Dazu dient das ORDS-Kommando map-url. Das Attribut --type legt fest, wie das URL-Matching erfolgen soll; base-path ist das einfachste und am häufigsten verwendete. Daneben steht auch noch das mächtigere regex bereit, mit dem auch komplexere URL-Mappings einrichten kann. Wichtig ist das Attribut --schema-name: Es legt fest, in welchem Datenbankschema die REST-Services zu finden sind. Die letzten beiden Parameter bezeichnen das URL-Pattern selbst und den Datenbank-Connection-Pool. Die folgenden beiden Aufrufe, die übrigens auch im laufenden Betrieb abgesetzt werden können, richten zwei URL-Mappings ein.

$ java -jar ords.war map-url --type base-path --schema-name scott /db1/scott apex
$ java -jar ords.war map-url --type base-path --schema-name scott /db2/scott dbserver2

Der URL-Präfix /ords/db1/scott wird demnach auf den Datenbankuser SCOTT und den Default-Connection-Pool apex abgebildet. Der Pfad /ords/db2/scott analog dazu auf das Schema SCOTT im Connection Pool dbserver2. Ansonsten funktionieren die REST-Services wie gehabt. Ein GET Request auf /ords/db2/scott/emp spricht also ..

  • ... im Connection-Pool dbserver2 ...
  • ... im Schema SCOTT ...
  • ... mit der Methode GET ...
  • ... das REST-Modul emp an.

Wenn Ihr nun in einer der Datenbanken mit ORDS.ENABLE_SCHEMA ein neues Schema für REST-Services freischaltet, könnt Ihr es nicht sofort mit REST-Requests ansprechen - zuerst muss mit java -jar ords.war map-url ein URL-Mapping für dieses Schema eingerichtet werden. Im Gegenzug hat man nur noch einen einzigen ORDS-Server, der aber REST-Services in vielen Datenbanken bereitstellt.

Probiert es aus - es braucht ein wenig Übung, um mit den verschiedenen Setup-Varianten vertraut zu werden; im Gegenzug bekommt man REST-Endpoints für eine oder viele Datenbanken - nach Maß! Viel Spaß damit.

Donnerstag, 23. Juli 2015

ORDS und Javascript im Einsatz: D3.js-Charts für Tabellendaten

Die ersten beiden Blog-Postings ('Auto-REST' für Tabellen und 'REST-Enabling' für 'custom' SQL und PL/SQL 0Auto-Rest' für Tabellen) haben einige wesentliche Grundlagen gelegt - nun wisst Ihr, wie Ihr mit Oracle REST Data Services (ORDS) sehr einfach REST-Services für Tabellen, Views, SQL-Queries oder PL/SQL-Objekte erzeugen könnt. Nun ist es Zeit, das einmal praktisch einzusetzen. In diesem Blog Posting wollen wir einen so generieren REST-Service nutzen, um einen D3.js Chart mit Daten zu versorgen.
D3.js ist eine sehr populäre, Diagrammtechnologie, die allein auf Standards wie HTML5, CSS und Javascript basiert. D3.js ist aber keine Chart-Engine, sondern vielmehr ein Javascript-Programmierframework, welches gängige Aufgaben, die beim Programmieren eines Diagramms entstehen, übernimmt bzw. leichter macht. Tatsächlich programmieren muss man den Chart aber immer noch. Der Autor, Charles Bostock hat neben dem Framework an sich auch eine Reihe von Beispielen ins Internet gestellt, anhand dieser kann man gut nachvollziehen, wie D3.js Charts funktionieren und wie man das Framework nutzt.
Eben solche Charts hätten wir gerne für unsere Tabellendaten. Ein Bubble Chart auf die Tabelle EMP sollte dann so aussehen.
Schauen wir uns den Javascript-Code und die Daten des Bubblechart-Beispiels einmal näher an. Die Daten liegen im JSON-Format vor und sehen wie folgt aus.
Im Javascript-Code ist vor allem das markierte Fragment interessant - der Aufruf von d3.json lädt die JSON-Daten aus der angegebenen Datei - anstelle eines einfachen Dateinamens kann auch eine HTTP-URL angegeben werden - und dann könnte man die URL eines ORDS Rest Service angeben. So könnte man den Chart mit Daten als der Oracle-Datenbank versorgen.
Bevor wir aber jetzt loslegen, einen REST-Service bauen, der die im Beispiel angegebene hierarchische Struktur generiert, schauen wir noch etwas genauer in den Javascript-Code hinein - der enthält auch noch diese Javascript-Funktion hier ...
Offensichtlich werden die hierarchischen JSON-Daten mit dieser Funktion "flachgeklopft" - und wenn das so ist, dann brauchen wir im REST-Service nicht erst großartig eine Hierarchie zu erzeugen; die flache Ausgabe, die wir ohnehin bekommen, reicht völlig aus. Zuerst also die Skripte für unseren REST Service - dass die Tabelle EMP in eurem Datenbankschema vorhanden und ORDS installiert ist, setzte ich jetzt mal voraus. Analog zur Vorgehensweise im letzten Blog-Posting erzeugen wir den Service wie folgt.
/*
 * Etwaige vorhandene Service-Definition löschen 
 */

declare
  l_int_module_id user_ords_modules.id%TYPE;
begin
  select id into l_int_module_id 
  from user_ords_modules
  where name = 'd3bubble';

  ords_services.delete_module(p_id => l_int_module_id);
end;
/
sho err

/*
 * Neuen Service erzeugen
 */

begin
  ords.create_service(
    p_module_name => 'd3bubble',
    p_base_path =>   '/d3bubble/',
    p_pattern =>     'getdata',
    p_method =>      'GET',
    p_source_type => ords.source_type_query,
    p_source =>      q'#select ename as name, job as packageName, sal as value from emp#'
  );
end;
/
sho err

commit
/
Danach testen ...
Als nächstes gehen wir in den Javascript-Code des Charts und nehmen dort ein paar Änderungen vor.
  • Der Zugriff von d3.json auf die Datei flare.json wird durch eine URL auf unseren neuen REST-Service ersetzt.
    :
    d3.json("/ords/scott/d3bubble/getdata"function(error, root) {
    :
  • Der Aufruf der Javascript-Funktion classes, welche die Hierarchie in ein flaches Format wandelt, wird eliminiert.
  • JSON-Attributnamen müssen an der einen oder anderen Stelle an das vom REST-Service ausgelieferte JSON angepasst werden
Wenn das alles gemacht wurde, kommt der folgende Javascript-Code heraus. Denkt daran, die URL zum REST-Service in Zeile 28 ggfs. an eure Umgebung anzupassen.
<!DOCTYPE html>
<html>
  <head>
    <style> text { font: 10px sans-serif; } </style>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
  </head>
  <body>
    <script>
var diameter = 600,
    format = d3.format(",d"),
    color = d3.scale.category20c();

var bubble = d3.layout.pack()
    .sort(null)
    .size([diameter, diameter])
    .padding(1.5);

var svg = d3.select("body").append("svg")
    .attr("width", diameter)
    .attr("height", diameter)
    .attr("class", "bubble");

d3.json("/ords/scott/d3bubble/getdata", function(error, root) {
  if (error) throw error;

  var node = svg.selectAll(".node")
    .data(bubble.nodes({"children":root.items})
    .filter(function(d) { return !d.children; }))
    .enter().append("g")
    .attr("class", "node")
    .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });

  node.append("title")
      .text(function(d) { return d.name + ": " + format(d.value); });

  node.append("circle")
      .attr("r", function(d) { return d.r; })
      .style("fill", function(d) { return color(d.packagename); });
  node.append("text")
      .attr("dy", ".3em")
      .style("text-anchor", "middle")
      .text(function(d) { return d.name.substring(0, d.r / 3); });
});

d3.select(self.frameElement).style("height", diameter + "px");

    </script>
  </body>
</html>
Speichert diesen HTML- und Javascript-Code in einer Datei ab (d3bubble.html). Diese Datei müsste als nächstes ebenfalls über den ORDS erreichbar sein. Dazu nutzen wir die Tatsache aus, dass ORDS auch als Webserver für APEX dienen kann und für diesen Zweck auch statische Dateien ausliefern kann. Legt also eine Ordner für statische Dateien (bspw. D:\oracle\ords-3.0.1\files an und stoppt den ORDS. Startet ihn dann wie folgt neu.
java -jar ords.war standalone --apex-images "D:\oracle\ords-3.0.1\files"
Nun könnt Ihr eure HTML-Datei ganz einfach mit dem URL http://{ords-host}:{port}/i/d3bubble.html aufrufen. Der HTML-Code wird in den Browser geladen, das Javascript startet und lädt die Tabellendaten per REST-Request nach. Fertig.
Nach gleichem Schema lässt sich auch das D3-Tutorial für einen Bar Chart mit ORDS kombinieren. Wir brauchen nicht einmal einen neuen REST-Service - der vorhandene tut es auch. Nur muss der Javascript-Code ein wenig angepasst werden.
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <style>
    .bar { fill: steelblue; }
    .bar:hover { fill: brown; }
    
    .axis { font: 10px sans-serif; }
    .axis path, .axis line { fill: none; stroke: #000; shape-rendering: crispEdges; }
    .x.axis path { display: none; }
    </style>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
  <head>
  <body>
    <script>

var margin = {top: 20, right: 20, bottom: 30, left: 40},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var x = d3.scale.ordinal()
    .rangeRoundBands([0, width], .1);

var y = d3.scale.linear()
    .range([height, 0]);

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left")
    .ticks(10, "");

var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

d3.json("/ords/scott/d3bubble/getdata", function(error, data) {
  if (error) throw error;
  x.domain(data.items.map(function(d) { return d.name; }));
  y.domain([0, d3.max(data.items, function(d) { return d.value; })]);

  svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);

  svg.append("g")
      .attr("class", "y axis")
      .call(yAxis)
    .append("text")
      .attr("transform", "rotate(-90)")
      .attr("y", 6)
      .attr("dy", ".71em")
      .style("text-anchor", "end")
      .text("Salary");

  svg.selectAll(".bar")
      .data(data.items)
    .enter().append("rect")
      .attr("class", "bar")
      .attr("x", function(d) { return x(d.name); })
      .attr("width", x.rangeBand())
      .attr("y", function(d) { return y(d.value); })
      .attr("height", function(d) { return height - y(d.value); });
});

function type(d) {
  d.value = +d.value;
  return d;
}

    </script>
  </body>
</html>
Den Javascript-Code packt Ihr in eine Datei namens d3bar.html und legt diese ins gleiche Verzeichnis wie schon zuvor d3bubble.html. Ein Aufruf von http://{ords-host}:{port}/i/d3bar.html sollte dann zu folgendem Bild führen.
Viel Spaß damit.