9. Januar 2012

Login als User "A" - mit dem Password von "B": Proxy Authentication!

Log in as user "A" - with the password of "B": Proxy Authentication
Es ist in der Tat möglich: Ich kann mich an der Oracle-Datenbank als User "A" mit dem Passwort des Benutzers "B" anmelden - und das ist keine Sicherheitslücke. Ich möchte dieses Blog-Posting dem Thema Proxy Authentication widmen. Das kann man ganz besonders in einer dreischichtigen Webarchitektur gebrauchen. Denn dort werden Datenbankverbindungen nahezu immer statisch im Application Server konfiguriert ( data-sources.xml). Wenn man in einer solchem Umgebung mit den Nutzerkonten der Datenbank arbeiten möchte, hat man ein Problem: Man kann sie nicht alle in der data-sources.xml einrichten; erstmal sind es oft zu viele und zweitens kann man die data-sources.xml nicht so einfach im laufenden Betrieb ändern. Die Lösung ist Proxy Authentication: Dabei wird die Datenbankverbidnung im Application Server fest mit einem technischen User und einem Passwort eingerichtet; der tatsächliche Datenbankuser, nach dem sich auch die Privilegien richten, wird zur Laufzeit festgelegt. Und das ganze funktioniert wie folgt: Zuerst brauchen wir einen technischen User TECHUSER (mit Passwort TECHUSER) und zwei "echte" User (REALUSER1 und REALUSER2).
create user techuser identified by techuser
/

grant connect to techuser
/

reate user realuser1 identified by realuser1
/

grant connect, resource to realuser1
/

reate user realuser2 identified by realuser2
/

grant connect, resource to realuser2
/
Und damit die User in einer Anwendung unterscheidbar werden, bekommen Sie nun eine Kopie der EMP-Tabelle, aber mit unterschiedlichen Inhalten ...
craete table realuser1.emp as select * from scott.emp where deptno = 10
/

craete table realuser2.emp as select * from scott.emp where deptno = 20
/
Und jetzt geht es los: Zuerst muss man der Datenbank sagen, dass TECHUSER die Erlaubnis bekommt, sich mit seinem eigenen Passwort als REALUSER1 oder REALUSER2 "auszugeben". Man kann auch sagen: TECHUSER wird der Proxy User für die Clients REALUSER1 und REALUSER2.
alter user realuser1 grant connect through techuser
/

alter user realuser2 grant connect through techuser
/
Der Setup lässt sich in der Dictionary View PROXY_USERS auch überprüfen:
SQL> select * from proxy_users;

PROXY           CLIENT          AUT FLAGS
--------------- --------------- --- -----------------------------------
TECHUSER        REALUSER1       NO  PROXY MAY ACTIVATE ALL CLIENT ROLES
TECHUSER        REALUSER2       NO  PROXY MAY ACTIVATE ALL CLIENT ROLES
Mit SQL*Plus kann man das jetzt schon ausprobieren:
D:\>sqlplus.exe techuser[realuser1]/techuser

SQL*Plus: Release 11.1.0.6.0 - Production on Do Dez 22 16:17:30 2011

Copyright (c) 1982, 2007, Oracle.  All rights reserved.


Verbunden mit:
Oracle Database 11g Enterprise Edition Release 11.2.0.2.0 - Production
With the Partitioning, Oracle Label Security, OLAP, Data Mining
and Real Application Testing options

SQL> select user from dual;

USER
------------------------------
REALUSER1
Man sieht sehr schön, dass Username und Passwort des TECHUSER zum Login verwendet werden; in eckigen Klammern wird aber festgelegt, für wen die Datenbankverbindung eigentlich aufgemacht werden soll.
So weit, so gut, so einfach. Aber für eine dreischichtige Applikation nutzt das bis jetzt noch gar nichts. Schließlich kann man nicht einfach den "echten" User in eckige Klammern in die Definition der Datenbankverbindung eintragen, denn diese ist ja statisch. Der User, für den der Connect gelten soll, soll sich aber beliebig ändern können - also dynamisch sein. Also müssen wir in der Lage sein, den Usernamen, den man bei SQL*Plus in eckige Klammern setzt, per Java-Code zu setzen ... Schauen wir uns zunächst ein kleines Java-Testprogramm an: Es gibt zuerst den Namen des angemeldeten Users aus ( select user from dual) und dann die Inhalte der EMP Tabelle. Achtet darauf, dass Username und Passwort des TECHUSER hart kodiert sind - und das wird so bleiben!
import java.sql.*;
import oracle.jdbc.*;
import java.io.*;
import java.util.*;

public class proxyConnect {
  public static void main(String args[]) throws Exception {
    DriverManager.registerDriver(new oracle.jdbc.OracleDriver());
    Connection con = DriverManager.getConnection("jdbc:oracle:thin:@sccloud030:1521/orcl","techuser","techuser");
    
/*
 * Code zum Setzen des Client-Usernamens HIER !!!
 */

    Statement stmt = null;
    ResultSet rs1 = null;
    stmt = con.createStatement();
    rs1 = stmt.executeQuery("select user from dual");
    while (rs1.next()) {
      System.out.println("Database userid: " + rs1.getString(1));
    }  
    rs1.close();
    stmt.close();

    stmt = con.createStatement();
    rs1 = stmt.executeQuery("select * from emp");
    while (rs1.next()) {
      System.out.println("EMPNO: " + rs1.getString(1) + " ["+rs1.getString(2)+"] - DEPTNO: "+rs1.getString("DEPTNO"));
    }  
    rs1.close();
    stmt.close();
    con.close();
  }
} 
Beim ersten Test sagt uns das Programm nur, dass wir als TECHUSER verbunden sind und dass es keine EMP-Tabelle gibt ...
D:\> java.exe proxyConnect realuser1
Database userid: TECHUSER
Exception in thread "main" java.sql.SQLSyntaxErrorException: 
ORA-00942: Tabelle oder View nicht vorhanden
        at oracle.jdbc.driver.SQLStateMapping.newSQLException(SQLStateMapping.java:91)
        at oracle.jdbc.driver.DatabaseError.newSQLException(DatabaseError.java:112)
        at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:173)
        :
Dann bauen wir jetzt den Code ein, der den "richtigen" User setzt. Tauscht die Zeilen ...
/*
 * Code zum Setzen des Client-Usernamens HIER !!!
 */
... durch diese hier aus: Als Usernamen nehmen wir den ersten Parameter der Kommandozeile.
    Properties props = new Properties();
    props.put("PROXY_USER_NAME", args[0]);
    ((OracleConnection)con).openProxySession(OracleConnection.PROXYTYPE_USER_NAME, props);  
Neu kompilieren und wieder ausführen. Und .. voilá:
D:\> java proxyConnect realuser1
Database userid: REALUSER1
EMPNO: 7782 [CLARK] - DEPTNO: 10
EMPNO: 7839 [KING] - DEPTNO: 10
EMPNO: 7934 [MILLER] - DEPTNO: 10

D:\> java proxyConnect realuser2
Database userid: REALUSER2
EMPNO: 7369 [SMITH] - DEPTNO: 20
EMPNO: 7566 [JONES] - DEPTNO: 20
EMPNO: 7788 [SCOTT] - DEPTNO: 20
EMPNO: 7876 [ADAMS] - DEPTNO: 20
EMPNO: 7902 [FORD] - DEPTNO: 20
Nur durch die Angabe des Namens wird nun festgelegt, als welcher User die Datenbanksession laufen soll. Damit kann ein Java-Programm (bspw. in einem Application Server) mit einer statischen Datasource-Definition dennoch dynamisch den Datenbankuser wechseln. Denn eine bestehende Proxy-Verbindung kann geschlossen werden, um danach auf der gleichen "physikalischen" Datenbankverbindung eine neue Proxy-Verbindung für einen anderen User zu öffnen. Im folgenden Code habe ich das mal illustriert - zur besseren Übersicht habe ich die Datenbankaktionen entfernt und gegen den Pseudocall auf erledigeDatenbankaktionen() ausgetauscht.
  public static void main(String args[]) throws Exception {
    // Physikalische Verbindung öffnen als TECHUSER
    DriverManager.registerDriver(new oracle.jdbc.OracleDriver());
    Connection con = DriverManager.getConnection("jdbc:oracle:thin:@sccloud030:1521/orcl","techuser","techuser");
    
    // Proxy-Verbindung für User REALUSER1 öffnen, etwas tun und schließen
    Properties props = new Properties();
    props.put("PROXY_USER_NAME", "realuser1");
    ((OracleConnection)con).openProxySession(OracleConnection.PROXYTYPE_USER_NAME, props);  
    erledigeDatenbankaktionen(con);
    ((OracleConnection)con).close(OracleConnection.PROXY_SESSION); 

    // Die physikalische Verbindung ist immer noch offen ...
    // Proxy-Verbindung für User REALUSER2 öffnen, etwas tun und schließen
    props.put("PROXY_USER_NAME", "realuser2");
    ((OracleConnection)con).openProxySession(OracleConnection.PROXYTYPE_USER_NAME, props);  
    erledigeDatenbankaktionen(con);
    ((OracleConnection)con).close(OracleConnection.PROXY_SESSION); 

    // Physikalische Verbindung schließen
    con.close();
  }
Beim Umstieg von Client/Server- zu Webanwendungen ist ein solches Vorgehen hochinteressant.
Wichtig ist, dass REALUSER1 bzw. REALUSER2 immer noch das CREATE SESSION-Privileg brauchen; auch darf der Account nicht gelockt sein. Es ist aber durchaus möglich, direkte Connects zu unterbinden - dazu muss man "nur" das Passwort auf einen nicht ermittelbaren Wert setzen ...
SQL> alter user realuser1 identified by values 'Hallo'
Nun wird der Text "Hallo" als Hashwert in die Oracle-Passworttabelle geschrieben - ein Connect als REALUSER1 ist nun theoretisch möglich: wenn man das Passwort herausbekommt, dessen Hashwert "Hallo" ergibt. Defacto jedoch kann man sich nur noch über den TECHUSER als REALUSER1 anmelden. Aber auch ein Parallelbetrieb mit direkten und Proxy-Verbindungen ist natürlich problemlos machbar.
Mehr zur Proxy Authentication in der Dokumentation: Dazu sind zwei Links wichtig:
Yes - it is possible: You can connect to the Oracle database as say: User "A" with the password of User "B" - and that is not a security vulnerability. In this blog posting I'd like to elaborate a bit on proxy authentication - this feature is particular useful in three-tier-applications. Database connections are typically part of the static application server configuration ("data-sources.xml"). And when such a java environments want to use the user accounts in the database there is a problem: We cannot add each individual user to the datasource configuration. The first arguments is, that there might be just too many users - the second one is that changes to data-sources.xml are often not possible without downtime. So we need a connection using one single technical user's credentials and the ability to set the desired username at runtime. And that is all what proxy authentication is about. We'll start with creating the technical user (TECHUSER) and two "real" user accounts (REALUSER1 and REALUSER2).
create user techuser identified by techuser
/

grant connect to techuser
/

reate user realuser1 identified by realuser1
/

grant connect, resource to realuser1
/

reate user realuser2 identified by realuser2
/

grant connect, resource to realuser2
/
REALUSER1 and REALUSER2 now get a copy of the EMP table with different contents: we want to differentiate between the two later on.
craete table realuser1.emp as select * from scott.emp where deptno = 10
/

craete table realuser2.emp as select * from scott.emp where deptno = 20
/
Now we declare TECHUSER as the proxy user for REALUSER1 and REALUSER2. This is kind of a GRANT statement. The privilege to act as (the clients) REALUSER1 or REALUSER2 is granted to TECHUSER.
alter user realuser1 grant connect through techuser
/

alter user realuser2 grant connect through techuser
/
You might review this in the dictionary view PROXY_USERS:
SQL> select * from proxy_users;

PROXY           CLIENT          AUT FLAGS
--------------- --------------- --- -----------------------------------
TECHUSER        REALUSER1       NO  PROXY MAY ACTIVATE ALL CLIENT ROLES
TECHUSER        REALUSER2       NO  PROXY MAY ACTIVATE ALL CLIENT ROLES
A first test with SQL*Plus:
D:\>sqlplus.exe techuser[realuser1]/techuser

SQL*Plus: Release 11.1.0.6.0 - Production on Do Dez 22 16:17:30 2011

Copyright (c) 1982, 2007, Oracle.  All rights reserved.


Connected to:
Oracle Database 11g Enterprise Edition Release 11.2.0.2.0 - Production
With the Partitioning, Oracle Label Security, OLAP, Data Mining
and Real Application Testing options

SQL> select user from dual;

USER
------------------------------
REALUSER1
TECHUSER used his own credentials to log in. But the user as which the connection should be established was provided within the brackets. In SQL*Plus it is that easy: We logged in as TECHUSER/techuser and we now are REALUSER1.
So far - so good. But for real world environments this is virtually useless - no end user connects with SQL*Plus. And we cannot add the brackets to the static datasource definition in the application server: we need to set the client user dynamically with some code - and the following example will show how to do this. First I have a little testing program in java language. It first connects to the database with hardcoded TECHUSER credentials, then it looks up "as who" it is connected and finally it shows the contents of the EMP table. Again: The username and password arguments in the call to DriverManager.getConnection are hard coded and this will not change!
import java.sql.*;
import oracle.jdbc.*;
import java.io.*;
import java.util.*;

public class proxyConnect {
  public static void main(String args[]) throws Exception {
    DriverManager.registerDriver(new oracle.jdbc.OracleDriver());
    Connection con = DriverManager.getConnection("jdbc:oracle:thin:@sccloud030:1521/orcl","techuser","techuser");
    
/*
 * The java code to set the client username goes here !!!
 */

    Statement stmt = null;
    ResultSet rs1 = null;
    stmt = con.createStatement();
    rs1 = stmt.executeQuery("select user from dual");
    while (rs1.next()) {
      System.out.println("Database userid: " + rs1.getString(1));
    }  
    rs1.close();
    stmt.close();

    stmt = con.createStatement();
    rs1 = stmt.executeQuery("select * from emp");
    while (rs1.next()) {
      System.out.println("EMPNO: " + rs1.getString(1) + " ["+rs1.getString(2)+"] - DEPTNO: "+rs1.getString("DEPTNO"));
    }  
    rs1.close();
    stmt.close();
    con.close();
  }
} 
This program so far does not know anything about proxy connections - the results are therefore straightforward: We are connected as TECHUSER and there is no EMP table.
D:\> java.exe proxyConnect realuser1
Database userid: TECHUSER
Exception in thread "main" java.sql.SQLSyntaxErrorException: 
ORA-00942: table or view does not exist.
        at oracle.jdbc.driver.SQLStateMapping.newSQLException(SQLStateMapping.java:91)
        at oracle.jdbc.driver.DatabaseError.newSQLException(DatabaseError.java:112)
        at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:173)
        :
Now we'll add the "magic" lines of code to set the client username. Exchange those three lines ...
/*
 * The java code to set the client username goes here !!!
 */
... with these - we'll take the first command line argument as the username.
    Properties props = new Properties();
    props.put("PROXY_USER_NAME", args[0]);
    ((OracleConnection)con).openProxySession(OracleConnection.PROXYTYPE_USER_NAME, props);  
Recompile and test again - and ... it works.
D:\> java proxyConnect realuser1
Database userid: REALUSER1
EMPNO: 7782 [CLARK] - DEPTNO: 10
EMPNO: 7839 [KING] - DEPTNO: 10
EMPNO: 7934 [MILLER] - DEPTNO: 10

D:\> java proxyConnect realuser2
Database userid: REALUSER2
EMPNO: 7369 [SMITH] - DEPTNO: 20
EMPNO: 7566 [JONES] - DEPTNO: 20
EMPNO: 7788 [SCOTT] - DEPTNO: 20
EMPNO: 7876 [ADAMS] - DEPTNO: 20
EMPNO: 7902 [FORD] - DEPTNO: 20
Only by giving the username on the command line we connected as REALUSER1 or REALUSER2 - the password always was TECHUSER's one. With this approach a java program can obtain a database connection from its application server, set the client username and connect to the database as another "real" user. And this proxy connection can be changed while the main "physical" connection remains open. The following code shows this - for clarity I have removed the actual database actions and replaced them with the pseudo method doDatabaseActions().
  public static void main(String args[]) throws Exception {
    // open the physical connection as TECHUSER
    DriverManager.registerDriver(new oracle.jdbc.OracleDriver());
    Connection con = DriverManager.getConnection("jdbc:oracle:thin:@sccloud030:1521/orcl","techuser","techuser");
    
    // open the proxy connection for REALUSER1, do something and close
    Properties props = new Properties();
    props.put("PROXY_USER_NAME", "realuser1");
    ((OracleConnection)con).openProxySession(OracleConnection.PROXYTYPE_USER_NAME, props);  
    doDatabaseActions(con);
    ((OracleConnection)con).close(OracleConnection.PROXY_SESSION); 

    // the physical connection is still open ...
    // now open the proxy connection for REALUSER2, do something and close
    props.put("PROXY_USER_NAME", "realuser2");
    ((OracleConnection)con).openProxySession(OracleConnection.PROXYTYPE_USER_NAME, props);  
    doDatabaseActions(con);
    ((OracleConnection)con).close(OracleConnection.PROXY_SESSION); 

    // finally close the physical connection
    con.close();
  }
When client/server applications are moving to a web environment, this technology gets highly interesting - because the application can keep its existing user, role and security model and move on to the web technology where all database connections are done with one technical user.
REALUSER1 or REALUSER2 still need their CREATE SESSION privilege - the accounts also must exist in the database and must not be locked. But (if needed) you actually can "forbid" direct connections by setting the password to an "impossible" value ...
SQL> alter user realuser1 identified by values 'Hello'
The REALUSER1 account is still open and connects are possible in theory. But since the IDENTIFIED BY VALUES command directly wrote "Hello" into the password table, one would have to find out one of the password - which evaluate to "Hello" during the hashing process - I'll say it that way: at least very difficult. Connections as REALUSER1 now are only possible as connections through TECHUSER. But it is also possible to have both direct and proxy connections in parallel.
More about proxy authentication is in the documentation:

22. Dezember 2011

Zum Ende von 2011: Oracle 4.1.4 ...

At the end of 2011: Oracle 4.1.4
Das letzte Blog Posting in 2011 soll nichts technisches mehr sein - die letzten Tage im Jahr kann man ja ein wenig zur Ruhe kommen. Daher hier nur ein paar Screenshots, die ich gemacht habe, als ich eine alte Oracle 4.1.4 nochmals installiert habe. Kein XML, kein PL/SQL, kein Oracle TEXT, kein APEX ... aber EMP ... und DEPT!
Frohe Weihnachten und ein gutes neues Jahr 2012!
My last blog posting in 2011 will not contain technical stuff. The last few days in a year are a good chance to relax a bit - and so I just would like to post some screenshots which I have taken while installing the Oracle 4.1.4 files which I found a couple of weeks ago. No XML, no PL/SQL, no Oracle TEXT, no APEX ... but EMP ... and DEPT.
Merry Christmas and a happy new year 2012!

7. Dezember 2011

Anteile in Prozent in einem Bericht kalkulieren - nur mit SQL!

How to calculate "ratio to report" in SQL
Heute morgen erreichte mich eine SQL-Frage - und das Problem taucht sicherlich öfter auf, daher veröffentliche ich meine Antwort hier für alle. Ausgangspunkt ist eine Tabelle.
|art   |anzahl     |kosten       |anteil    |
|------|-----------|-------------------------
| A    |        55 |       16,95 |     null |
| B    |        55 |        5,45 |     null |
| C    |        55 |        3,20 |     null |
| D    |        55 |        1,95 |     null |
Die rechte Spalte anteil soll nun den prozentualen (hier:) Kostenanteil der Zeile im Verhältnis zur Summe über alle Zeilen enthalten. Damit ich die Tabelle nicht neu anlegen muss, übertrage ich das Beispiel auf die uns allen bekannte Tabelle EMP: Wir wollen also zu jeder das Gehalt als prozentualen Anteil an der Gesamt-Gehaltssumme ausgedrückt sehen. Fangen wir mit dem "klassischen" Ansatz an ...
with summe as (
  select sum(sal) sal from emp
)
select 
  e.ename, 
  e.sal,
  e.sal / s.sal * 100 anteil
from emp e, summe s
/ 

ENAME            SAL  ANTEIL
---------- --------- -------
SMITH           1280    2,76
ALLEN           2560    5,51
WARD            2000    4,31
JONES           4760   10,25
MARTIN          2000    4,31
Diese Lösung dürfte auf nahezu allen Datenbanksystemen funktionieren - mit der WITH-Klausel wird zunächst die Gehaltsumme ermittelt und dann per Join in die eigentliche Abfrage integriert. Wenn man den Gehaltsanteil pro Abteilung (DEPTNO) sehen möchte, müsste man die Abfrage in der WITH-Klausel mit einem GROUP BY versehen und eine Joinbedingung in die Hauptabfrage einbauen ...
with summe as (
  select deptno, sum(sal) sal from emp
  group by deptno
)
select 
  e.ename, 
  e.deptno,
  e.sal,
  e.sal / s.sal * 100 anteil_dept
from emp e join  summe s on (e.deptno = s.deptno)
/

ENAME          DEPTNO       SAL ANTEIL_DEPT
---------- ---------- --------- -----------
SMITH              20      1280        7,36
ALLEN              30      2560       17,02
WARD               30      2000       13,30
JONES              20      4760       27,36
MARTIN             30      2000       13,30
BLAKE              30      4560       30,32
So weit - so gut. Aber es geht auch wesentlich schöner: Setzt man analytische Funktionen ein, so kann man sich die Inline-View sparen - das macht dann alles die Datenbank ... so könnte man die analytische Variante von SUM wie folgt verwenden.
select 
  e.ename, 
  e.deptno,
  e.sal,
  (e.sal / sum(sal) over ()) * 100 anteil
from emp e
/
ENAME          DEPTNO       SAL  ANTEIL
---------- ---------- --------- -------
SMITH              20      1280    2,76
ALLEN              30      2560    5,51
WARD               30      2000    4,31
Hier wird SUM ohne GROUP BY verwendet - einfach weil es die analytische Variante ist. Das sog. Query Window, welches angibt, über welche Zeilen das Aggregat gebildet werden soll, wird in der OVER -Klausel festgelegt. Und wenn die Klammern leer sind, heißt das soviel wie "über alles". Aber auch die Variante mit den Anteil pro Abteilung ist machbar.
select 
  e.ename, 
  e.deptno,
  e.sal,
  (e.sal / sum(sal) over (partition by deptno)) * 100 anteil
from emp e
/

ENAME          DEPTNO       SAL  ANTEIL
---------- ---------- --------- -------
CLARK              10      3920   28,00
KING               10      8000   57,14
MILLER             10      2080   14,86
JONES              20      4760   27,36
FORD               20      4800   27,59
Jetzt steht etwas in der OVER()-Klausel drin. Mit PARTITION BY wird festgelegt, dass das Aggregat (die Summe) nach Abteilungen berechnet werden soll. Das Schlüsselwort PARTITION BY ist übrigens nicht zu verwechseln mit der Tabellenpartitionierung und im Gegensatz zu dieser erfordern die analytischen Funktionen keine separaten Lizenzen. Aber es geht sogar noch einfacher: Denn für diese Aufgabe gibt es eine spezielle analytische Funktion: RATIO_TO_REPORT.
select 
  e.ename, 
  e.deptno,
  e.sal,
  ratio_to_report(e.sal) over () * 100 anteil
from emp e
/

ENAME          DEPTNO        SAL  ANTEIL
---------- ---------- ---------- -------
SMITH              20       1280    2,76
ALLEN              30       2560    5,51
WARD               30       2000    4,31
JONES              20       4760   10,25
MARTIN             30       2000    4,31
Und natürlich geht auch hier die PARTITION BY-Klausel ...
select 
  e.ename, 
  e.deptno,
  e.sal,
  ratio_to_report(e.sal) over (partition by deptno) * 100 anteil
from emp e
/
Analytische Funktionen lohnen auf jeden Fall eine genauere Betrachtung. Jeder, der mit SQL und der Oracle-Datenbanken arbeitet, sollte sie kennen. Neben den hier beschriebenen Dingen lassen sich damit auch gleitende Durchschnitte, fortlaufende Summen, Rankings und andere Dinge, die sonst recht kompliziertes SQL mit Subselects erfordern, mit einfachen Funktionsaufrufen erledigen. Zum Nachlesen ist hier noch ein Link zur Dokumentation.
This morning I got a SQL question - nothing difficult - but I think, this kind of requirement is more frequent, and therefore I decided to publish the answer for everyone. The question is based on table data (as always) ...
|art   |count      |cost         |cost_share |
|------|-----------|--------------------------
| A    |        55 |       16,95 |      null |
| B    |        55 |        5,45 |      null |
| C    |        55 |        3,20 |      null |
| D    |        55 |        1,95 |      null |
The right column cost_share should contain the row's share of total cost - expressed in percent. So the cost column needs to be divided by SUM(COST) over all rows. I'm lazy: so instead of creating this table I decided to solve the problem for the SAL column in the well known EMP table. Let's start with "classic" SQL.
with total as (
  select sum(sal) sal from emp
)
select 
  e.ename, 
  e.sal,
  e.sal / s.sal * 100 share
from emp e, total s
/ 

ENAME            SAL   SHARE
---------- --------- -------
SMITH           1280    2,76
ALLEN           2560    5,51
WARD            2000    4,31
JONES           4760   10,25
MARTIN          2000    4,31
This query should work on almost every database system. First we compute the total salary amount with an inline view using the WITH clause. This inline view will be joined in the main query. It returns only one row, so we don't need any join criteria and we can access the total salary amount just like a table column. And this can be extended - if we'd like to see the salary share expressed as "percentage of the department total", we add a GROUP BY to the inline view and extend the join in the main query as follows ...
with total as (
  select deptno, sum(sal) sal from emp
  group by deptno
)
select 
  e.ename, 
  e.deptno,
  e.sal,
  e.sal / s.sal * 100 share_dept
from emp e join  total s on (e.deptno = s.deptno)
/

ENAME          DEPTNO       SAL  SHARE_DEPT
---------- ---------- --------- -----------
SMITH              20      1280        7,36
ALLEN              30      2560       17,02
WARD               30      2000       13,30
JONES              20      4760       27,36
MARTIN             30      2000       13,30
BLAKE              30      4560       30,32
So far, so good. This works, but there are much more elegant approaches. Now I'll drop my inline view and use the analytic variant of SUM() for the problem.
select 
  e.ename, 
  e.deptno,
  e.sal,
  (e.sal / sum(sal) over ()) * 100 share
from emp e
/
ENAME          DEPTNO       SAL   SHARE
---------- ---------- --------- -------
SMITH              20      1280    2,76
ALLEN              30      2560    5,51
WARD               30      2000    4,31
This query uses SUM without GROUP BY, because it's the analytic SUM function. The query window , which defines the rows to be aggregated, is specified in the OVER() clause. The aggregate is then calculated to each row of the query result set. An empty OVER clause means as much as "over all rows". But we can also do the calculation on the department level.
select 
  e.ename, 
  e.deptno,
  e.sal,
  (e.sal / sum(sal) over (partition by deptno)) * 100 share
from emp e
/

ENAME          DEPTNO       SAL   SHARE
---------- ---------- --------- -------
CLARK              10      3920   28,00
KING               10      8000   57,14
MILLER             10      2080   14,86
JONES              20      4760   27,36
FORD               20      4800   27,59
Now we have the keywords PARTITION BY inside the OVER() clause. So the aggregate is being computed for each department. Don't confuse this PARTITION BY clause with table partitioning. Opposed to the latter, PARTITION BY within an analytic function has nothing to do with table storage and does not require an additional license. But we can solve the original problem even more elegant. Why? Because there is a special function for this purpose: RATIO_TO_REPORT.
select 
  e.ename, 
  e.deptno,
  e.sal,
  ratio_to_report(e.sal) over () * 100 share
from emp e
/

ENAME          DEPTNO        SAL   SHARE
---------- ---------- ---------- -------
SMITH              20       1280    2,76
ALLEN              30       2560    5,51
WARD               30       2000    4,31
JONES              20       4760   10,25
MARTIN             30       2000    4,31
And this (of course) also works with PARTITION BY ...
select 
  e.ename, 
  e.deptno,
  e.sal,
  ratio_to_report(e.sal) over (partition by deptno) * 100 share
from emp e
/
Analytic functions are absolutely worth the learing effort. Beyond the problem described here, analytic functions provide an easy query syntax for things like moving averages, rankings or other aggregates with flexible query windows. Solving this with "classic" SQL is possible (of course), but this most often gets cumbersome and difficult to read. You'll find more information in the documentation.

21. November 2011

DROP TABLE .. Sind Sie sicher?

"Are you sure?" for DROP TABLE
UPDATE: Auf einem internen "Produktionsserver" (sofern man davon sprechen kann) habe ich das Package natürlich gleich installiert. Und heute morgen dann gemerkt, dass ein TRUNCATE / DROP auch mal der Normalfall sein kann. Als ich mich im APEX-Workspace einloggen wollte, kam die Fehlermeldung, dass ein DROP / TRUNCATE auf die Tabelle WWV_FLOW_USER_ACCESS_LOG2$ nicht möglich ist. APEX macht das von sich aus: Da hilft es nix: Der Trigger muss erweitert werden! Ich habe den Code (unten) angepasst und eine Ausnahme für APEX eingebaut; es kann durchaus sein, dass noch ein paar hinzukommen. Wenn euch da etwas vor mir auffällt, einfach in den Kommentar schreiben.
Jüngst bin ich nochmals über das Thema Database Event Triggers mit PL/SQL gestolpert - man kann ja (schon seit einiger Zeit) auch Trigger auf Datenbank-Events anlegen - ein klassisches Beispiel wäre das "Verbieten" einer DROP-Operation auf Produktionssystemen - einfach zur Sicherheit. Der Trigger dazu ist einfach und schnell gebaut ...
create or replace trigger tr_drop_is_prohibited
before drop on database
begin
  raise_application_error(-20000, 'DROP IS NOT ALLOWED ON THIS SYSTEM');
end;
/
Einfach, aber effektvoll. Man kann nun auf dieser Datenbank nichts mehr droppen (außer dem Trigger selbst, natürlich). Aber für den praktischen Einsatz etwas zu "grobstollig". Erstens ist ein TRUNCATE Table mittlerweile schlimmer als ein DROP TABLE, welches man mit dem Recycle Bin (FLASHBACK TABLE ... TO BEFORE DROP) rückgängig machen kann, zum anderen muss es ja manchmal doch sein, dass man etwas droppen will ... und dann muss man erst den Trigger deaktivieren und danach wieder aktivieren.
Nach diesem Gedanken kam ich auf die Idee, eine Art "Sind Sie Sicher?"-Mechanismus für DROP- und TRUNCATE-Operationen zu bauen. In der Datenbank funktioniert er nur andersherum als bei Microsoft Windows: Man muss die Drop-Operation zuerst deklarieren und dann ausführen. Auf einem Produktionssystem ist sowas vielleicht doch ganz hilfreich und erspart unter Umständen das eine oder andere versehentlich gedroppte Object. Hier ist nun der Code (muss als SYS eingespielt werden) - wir beginnen mit einem Package, mit dem die DROP-Operation vorher deklariert wird:
create or replace package PREDROP authid current_user is
  g_forsession boolean := false;
  g_nextobject boolean := false;
  g_objectname varchar2(4000) := '';
  g_objecttype varchar2(4000) := '';
  g_objectuser varchar2(4000) := '';

  procedure obj(
    p_objectname in varchar2,
    p_objectuser in varchar2 default user,
    p_objecttype in varchar2 default 'TABLE'
  );
  procedure obj;
  procedure sess(
    p_enable in boolean default true
  );
  procedure clear; 
  procedure info;
end PREDROP;
/
sho err

create or replace package body PREDROP is
  procedure sess (
    p_enable in boolean default true
  ) is 
  begin
     g_forsession := p_enable;
  end sess;

  procedure obj is
  begin
    g_forsession := false;
    g_nextobject := true;

    g_objectname := null;
    g_objectuser := null;
    g_objecttype := null;
  end obj;

  procedure obj (
    p_objectname in varchar2,
    p_objectuser in varchar2 default user,
    p_objecttype in varchar2 default 'TABLE'
  ) is
  begin
    g_forsession := false;
    g_nextobject := true;
    g_objectname := p_objectname;
    g_objecttype := p_objecttype;
    g_objectuser := p_objectuser;
  end obj;

  procedure clear is
  begin
    g_forsession := false;
    g_nextobject := false;
    g_objectname := null;
    g_objecttype := null;
    g_objectuser := null;
  end clear;

  procedure info is
  begin
    dbms_output.put_line('********************************************');
    dbms_output.put_line('* Enable Session Flag: ' ||(case when g_forsession then 'TRUE' else 'FALSE' end));
    dbms_output.put_line('* Enable Object Flag:  ' ||(case when g_nextobject then 'TRUE' else 'FALSE' end));
    dbms_output.put_line('* Object name:         ' ||g_objectname);
    dbms_output.put_line('* Object type:         ' ||g_objecttype);
    dbms_output.put_line('* Object owner:        ' ||g_objectuser);
    dbms_output.put_line('********************************************');
    dbms_output.put_line('');
  end info;
end PREDROP;
/
sho err

grant execute on PREDROP to public
/

create public synonym PREDROP for sys.PREDROP
/
Execute-Privileg und Public Synonym nicht vergessen und dann geht es auch schon weiter. Als nächstes kommt der Database Event Trigger, der ins Package hineinguckt, ob das Objekt, was da gerade bearbeitet werden soll, vorher deklariert wurde - wenn ja, geht die Operation durch, wenn nicht, wird ein Fehler ausgelöst ...
create or replace trigger tr_drop_protection 
before drop or truncate on database
declare
  l_raiseerror     boolean := true;

  l_namematch      boolean := false;
  l_typematch      boolean := false;
  l_usermatch      boolean := false;
  l_match          boolean := false;
  l_always_allowed boolean := false;

  /* UPDATE: 
   * Some code need to be allowed in general!
   */ 
  function always_allowed(p_owner in varchar2, p_object in varchar2) return boolean is
    l_always boolean := false;
  begin
    if p_owner like 'APEX_%' and p_object like 'WWV_FLOW_%LOG%' then
      l_always := true;
      goto ende;
    end if;
    <<ende>>
    return l_always;
  end always_allowed;

  function match(p_string1 in varchar2, p_string2 in varchar2) return boolean is
    l_match boolean := false;
  begin
    if p_string1 = p_string2 or p_string2 is null then 
      l_match := true;
    else 
      l_match := false;
    end if;
    return l_match;
  end match;
begin
    l_namematch := match(ora_dict_obj_name, PREDROP.g_objectname);
    l_typematch := match(ora_dict_obj_type, PREDROP.g_objecttype);
    l_usermatch := match(ora_dict_obj_owner, PREDROP.g_objectuser);
    l_match := l_namematch and l_usermatch and l_typematch;
    l_always_allowed := always_allowed(ora_dict_obj_owner, ora_dict_obj_name);

    if ((PREDROP.g_nextobject or PREDROP.g_forsession) and l_match) or l_always_allowed  then
      l_raiseerror := false;
    else
      l_raiseerror := true;
    end if;

    if not PREDROP.g_forsession and not l_raiseerror then 
      PREDROP.clear;
    end if;

    if l_raiseerror then
      raise_application_error(-20000, 'Enable DROP/TRUNCATE of '||ora_dict_obj_type|| ' "'||ora_dict_obj_name||'" with the '||upper('PREDROP')||' package.');
    end if;
end;
/
Und das war's auch schon. Von jetzt an funktioniert DROP und TRUNCATE etwas anders ...
SQL> drop table emp3;
drop table emp3
*
FEHLER in Zeile 1:
ORA-00604: Fehler auf rekursiver SQL-Ebene 1
ORA-20000: Enable DROP/TRUNCATE of TABLE "EMP3" with the PREDROP package.
ORA-06512: in Zeile 36

SQL> exec predrop.obj('EMP3');

PL/SQL-Prozedur erfolgreich abgeschlossen.

SQL> drop table emp3;

Tabelle wurde gelöscht.

SQL>
Für Truncate gilt das gleiche. Lässt man den Objektnamen auf SQL NULL, gilt die Freigabe für die nächste Tabelle, egal, wie sie heißt. Setzt man auch den Object Type auf SQL NULL, gilt die Freigabe für alle Objekte im Schema. Und der Parameter P_OBJECTUSER gilt analog. Ruft man dann noch die Prozedur PREDROP.SESS auf, gilt die Freigabe nicht nur für das nächste Objekt, sondern für die ganze Session. Auch auf Produktionsungebungen läuft ja mal ein Skript mit mehreren gewollten DROP-Operationen - solche Vorgänge sollten nicht behindert werden; das Package soll helfen und nicht im Weg sehen. Man könnte das jetzt natürlich auch noch mit regulären Ausdrücken erweitern und so die völlige Flexibilität bekommen - aber das machen wir ein anderes Mal ...
Was denkt Ihr ...?
UPDATE: Just yesterday I installed this package on my own "production" instance - and as I wanted to login into APEX today I encountered my own message Cannot DROP/TRUNCATE table "WWV_FLOW_USER_ACCESS_LOG2$" - so APEX does some regular DROP / TRUNCATE operations which must not be trapped by the Trigger - So I extended the trigger code a bit (further extensions might be necessary in the future) - just have a look into the new trigger code. If you also encounter DROP or TRUNCATE operations which need to ge generally allowed in the trigger, feel free to post me a message or a comment.
Recently I again encountered the database event triggers in PL/SQL - using these you can place a trigger not only on table or view DML, but also on DDL commands or database events like STARTUP, SHUTDOWN oder LOGON. One thirst thought would be prohibiting the DROP operation on a production instance - the trigger code for this is rather simple ...
create or replace trigger tr_drop_is_prohibited
before drop on database
begin
  raise_application_error(-20000, 'DROP IS NOT ALLOWED ON THIS SYSTEM');
end;
/
Simple and powerful. On this instance no object can be dropped any more (except the trigger itself, of course). But this can also lead to a lot of problems, of course: Even on production systems, sometimes there must be dropped something. Then the trigger needs to be disabled before and enabled after the operation. And then there might be objects which are frequently dropped and re-created and other objects which not. And what about the TRUNCATE operation. This one cannot be undone with FLASHBACK TABLE TO BEFORE DROP - so accidentially use of TRUNCATE is much more dangerous than the DROP operation ...
So my next thought was to try a bit more sophisticated approach - I'd like to see the "Are you sure?" feature which we all know from Windows also in the database. And although we cannot catch the DROP operation to ask the "are you sure" question, we can implement a similar approach: The idea is to declare the object to be dropped beforehand. So we have a package which holds the declared object and a trigger which looks into the package and raises an error message if the object has not been declared. First we create the package.
create or replace package PREDROP authid current_user is
  g_forsession boolean := false;
  g_nextobject boolean := false;
  g_objectname varchar2(4000) := '';
  g_objecttype varchar2(4000) := '';
  g_objectuser varchar2(4000) := '';

  procedure obj(
    p_objectname in varchar2,
    p_objectuser in varchar2 default user,
    p_objecttype in varchar2 default 'TABLE'
  );
  procedure obj;
  procedure sess(
    p_enable in boolean default true
  );
  procedure clear; 
  procedure info;
end PREDROP;
/
sho err

create or replace package body PREDROP is
  procedure sess (
    p_enable in boolean default true
  ) is 
  begin
     g_forsession := p_enable;
  end sess;

  procedure obj is
  begin
    g_forsession := false;
    g_nextobject := true;

    g_objectname := null;
    g_objectuser := null;
    g_objecttype := null;
  end obj;

  procedure obj (
    p_objectname in varchar2,
    p_objectuser in varchar2 default user,
    p_objecttype in varchar2 default 'TABLE'
  ) is
  begin
    g_forsession := false;
    g_nextobject := true;
    g_objectname := p_objectname;
    g_objecttype := p_objecttype;
    g_objectuser := p_objectuser;
  end obj;

  procedure clear is
  begin
    g_forsession := false;
    g_nextobject := false;
    g_objectname := null;
    g_objecttype := null;
    g_objectuser := null;
  end clear;

  procedure info is
  begin
    dbms_output.put_line('********************************************');
    dbms_output.put_line('* Enable Session Flag: ' ||(case when g_forsession then 'TRUE' else 'FALSE' end));
    dbms_output.put_line('* Enable Object Flag:  ' ||(case when g_nextobject then 'TRUE' else 'FALSE' end));
    dbms_output.put_line('* Object name:         ' ||g_objectname);
    dbms_output.put_line('* Object type:         ' ||g_objecttype);
    dbms_output.put_line('* Object owner:        ' ||g_objectuser);
    dbms_output.put_line('********************************************');
    dbms_output.put_line('');
  end info;
end PREDROP;
/
sho err

grant execute on PREDROP to public
/

create public synonym PREDROP for sys.PREDROP
/
Don't forget to grant EXECUTE Privileges and to create a public synonym in order to make the package accessible for everyone. Creating the Trigger is the next step. As said before, it simply checks whether the object to be dropped ( ora_dict_obj_name, ora_dict_obj_type, ora_dict_obj_owner) has been declared with the PRECODE package. If not, the trigger raises an error, otherwise it does nothing and the operation will succeed.
create or replace trigger tr_drop_protection 
before drop or truncate on database
declare
  l_raiseerror     boolean := true;

  l_namematch      boolean := false;
  l_typematch      boolean := false;
  l_usermatch      boolean := false;
  l_match          boolean := false;
  l_always_allowed boolean := false;

  /* UPDATE: 
   * Some code need to be allowed in general!
   */
  function always_allowed(p_owner in varchar2, p_object in varchar2) return boolean is
    l_always boolean := false;
  begin
    if p_owner like 'APEX_%' and p_object like 'WWV_FLOW_%LOG%' then
      l_always := true;
      goto ende;
    end if;
    <<ende>>
    return l_always;
  end always_allowed;

  function match(p_string1 in varchar2, p_string2 in varchar2) return boolean is
    l_match boolean := false;
  begin
    if p_string1 = p_string2 or p_string2 is null then 
      l_match := true;
    else 
      l_match := false;
    end if;
    return l_match;
  end match;
begin
    l_namematch := match(ora_dict_obj_name, PREDROP.g_objectname);
    l_typematch := match(ora_dict_obj_type, PREDROP.g_objecttype);
    l_usermatch := match(ora_dict_obj_owner, PREDROP.g_objectuser);
    l_match := l_namematch and l_usermatch and l_typematch;
    l_always_allowed := always_allowed(ora_dict_obj_owner, ora_dict_obj_name);

    if ((PREDROP.g_nextobject or PREDROP.g_forsession) and l_match) or l_always_allowed  then
      l_raiseerror := false;
    else
      l_raiseerror := true;
    end if;

    if not PREDROP.g_forsession and not l_raiseerror then 
      PREDROP.clear;
    end if;

    if l_raiseerror then
      raise_application_error(-20000, 'Enable DROP/TRUNCATE of '||ora_dict_obj_type|| ' "'||ora_dict_obj_name||'" with the '||upper('PREDROP')||' package.');
    end if;
end;
/
That's it. From now on, DROP and TRUNCATE operations need to be declared beforehand ...
SQL> drop table emp3;
drop table emp3
*
ERROR in line 1:
ORA-00604: Error at recursive SQL level 1
ORA-20000: Enable DROP/TRUNCATE of TABLE "EMP3" with the PREDROP package.
ORA-06512: line 36

SQL> exec predrop.obj('EMP3');

PL/SQL-Procedure successfully completed.

SQL> drop table emp3;

Table dropped.

SQL>
This implementation of the PREDROP package already provides some flexibility. Setting the object name, type or owner to SQL NULL matches everything. Normally a declaration is valid until it has been matched by a DROP operation - after calling PREDROP.SESS it will be valid until the end of the session - this is useful when runnung SQL scripts. Using regular expressions or LIKE syntax would make the package even more flexible ... but this is a story for another blog posting ...
Tell me what you think!

21. Oktober 2011

Ab morgen: DOAG2011

Bis zur DOAG2011 ist es ja nicht mehr solange hin - ich werde an allen drei Tagen dort sein und freue mich schon darauf, das eine oder andere Gesicht aus der Oracle-Community wiederzusehen ... Wie in den letzten Jahren habe ich auch dieses Jahr wieder ein paar Vorträge - hier sind sie ...
  • Demo-Kino: Transformers 4.1: Von Standard nach Cool
    15.11.2011 10:00 - 10:45 Uhr - Foyer Tokio

    Mit nur wenig Aufwand kann man einer APEX-Applikation moderne Elemente hinzufügen. Eine eher langweilige Geschäftsanwendung wird im Film transformiert - Elemente wie "Google Suggest", Karten, Volltextsuche und moderne Layoutelemente kommen hinzu. Die neue Anwendung holt den Nutzer dort ab, wo er steht, mit Anwendungsfeatures, die er aus dem Internet kennt ...

  • Das Navi in der Datenbank: Oracle11g has NAVTEQ on Board
    16.11.2011 10:00 - 10:45 Uhr - Raum Tokio

    Wussten Sie schon, dass Sie eine postalische Adresse mit der Oracle-Datenbank mit einer einfachen SQL-Abfrage in Koordinaten, also Längen- und Breitengrade, umwandeln können. Mit der Geocoding-Engine ist das überhaupt kein Problem - und durch die enge Integration mit der Datenbank kann dies Teil der normalen Geschäftslogik werden.
    Basis dafür sind NAVTEQ-Daten - Oracle has NAVTEQ on Board. Der Vortrag zeigt, was das NAVTEQ ODF Dataset (Oracle Data Format) beinhaltet, wie man es installiert und wie man damit nicht nur Geocoding, sondern auch Routing oder andere Operationen durchführen kann (diesen Vortrag mache ich gemeinsam mit Till Kreiler von NAVTEQ).

  • SQL oder NoSQL? Das ist hier die Frage! Die "Oracle NoSQL Datenbank"
    16.11.2011 14:00 - 14:45 Uhr - Raum Tokio

    Dieses Thema finde ich ziemlich spannend - ein wenig habe ich schon mit der neuen Oracle NoSQL Database herumgespielt - und ich muss sagen: Das ist wirklich ein neuer Ansatz - und auf die Reaktionen und Diskussionen bin ich auch schon sehr gespannt ...

    Bekannte Webseiten wie Twitter, Facebook oder URL-Verkürzer wie tinyurl.com setzen NoSQL-Datenbanken ein - und auf der OOW wurde jüngst die Oracle NoSQL Database angekündigt. Der Vortrag stellt die Oracle NoSQL Database vor, geht auf Unterschiede zwischen NoSQL-Datenbanken und den "klassischen" RDBMS ein und stellt mögliche Einsatzszenarien vor. NoSQL-Datenbanken werden genutzt, um extrem große Datenmengen extrem vieler User zu speichern. Im Gegensatz zu unserer "klassischen" Oracle-Datenbank spielen das Datenmodell, Transaktionskonzepte und Datenintegrität keine Rolle - das einzige, was zählt, ist die wirklich ständige Verfügbarkeit, die mit massiver Parallelisierung erreicht wird. Eine NoSQL-Datenbank richtet sich an Entwickler, denn es gibt keine Abfragesprache und keinen Query-Optimizer. Zugriffe erfolgen über eine Programmierschnittstelle (API) - alle Intelligenz steckt in der Anwendung.

  • apex.meinefirma.de: APEX Hosting im eigenen Unternehmen
    17.11.2011 13:00 - 13:45 Uhr - Raum Istabul

    Nahezu überall ändern sich Prozesse und Anforderungen immer schneller: gebraucht wird also eine Plattform, welche die Entwicklung neuer Anwendungen genauso schnell ermöglicht. Der öffentliche Demoserver "apex.oracle.com" zeigt, wie es geht: Entwickler können Ihre Workspaces selbstständig beantragen und verwalten, ohne manuelles des DBA Eingreifen sofort aktiv werden; neue Anwendungen stehen sofort bereit. Der Vortrag zeigt, wie ein Server "apex.meinefirma.de" aufgesetzt und betrieben werden kann, welche Hardware man braucht, wie Ressourcenkonflikte gelöst werden und worauf geachtet werden sollte. Das ist Cloud Computing "in Action".

Auf der ODTUG Kscope-Konferenz, an der ich im Juni teilgenommen hatte, wurde Twitter ganz massiv genutzt - Tweets über die einzelnen Vorträge und neue Ideen kamen quasi im Sekundentakt. Ich bin gespannt, wie das auf der DOAG2011 läuft. Meine Teamkollegen und ich werden euch über den Twitter Account @OracleBUDB und natürlich mit dem Hashtag #doag2011 auf dem Laufenden halten ... also dranbleiben ...
This is a posting about the DOAg2011 conference and therefore in german only.

4. Oktober 2011

Von Java nach SQL: Java-Objekte und die Datenbank-JVM

From Java to SQL: Java objects and the database JVM
In der Vergangenheit hatte ich ja schon einige Blog-Postings zum Thema "Java in der Datenbank"; das letzte Posting zum Thema Twitter-Postings mit der Datenbank, die diversen Postings zum Thema Dateisystem-Zugriffe haben alle eines gemein: Sie verwenden die in der Datenbank enthaltene Java-Engine. Wenn es aber darum geht, Java-Funktionen aus PL/SQL oder SQL heraus zu nutzen, ist immer auch eine Parameter-Mapping gefragt. Und genau dem möchte ich mich heute widmen. Dabei geht es mir aber nicht um das Abbilden eines VARCHAR2, NUMBER oder DATE auf Java-Datentypen - das ist sehr einfach, wie man hier sehen kann ...
create or replace and compile java source named simple_test as

public class SimpleTest {
  public static String getString() {
    return "Hallo Welt";
  }
  public static java.sql.Timestamp getDate() {
    return new java.sql.Timestamp(new java.util.Date().getTime());
  }
  public static int getNumber() {
    return 4711;
  }
} 
/
sho err

create or replace package simple_test_plsql is
  function get_string return varchar2;
  function get_date return date;
  function get_number return number;
end simple_test_plsql;
/
sho err

create or replace package body simple_test_plsql is
  function get_string return varchar2 as 
  language java name 'SimpleTest.getString() return java.lang.String';
  function get_date return date as
  language java name 'SimpleTest.getDate() return java.sql.Timestamp';
  function get_number return number as
  language java name 'SimpleTest.getNumber() return int';
end simple_test_plsql;
/
sho err

select 
  simple_test_plsql.get_string,
  simple_test_plsql.get_date,
  simple_test_plsql.get_number
from dual
/

GET_STRING           GET_DATE            GET_NUMBER
-------------------- ------------------- ----------
Hallo Welt           21.09.2011 09:19:40       4711
Den Experten ist sicher schon aufgefallen, dass die Java-Methoden alle "static" sind; das ist auch logisch so, denn PL/SQL ist ja keine objektorientierte Sprache. Mit "statischen" Java-Methoden wird Java wie eine prozedurale Sprache genutzt - es entspricht eher dem Konzept von PL/SQL, daher können nur statische Java-Methoden auf PL/SQL-Pakete, Prozeduren oder Funktionen abgebildet werden.
Das einzige, wo man ein wenig aufpassen muss, sind DATE-Mappings - diese können auf die Klassen java.sql.Date und java.sql.Timestamp abgebildet werden. In Java bedeutet das aber etwas anderes als in SQL. Arbeitet man beim Mapping mit java.sql.Date, dann werden nur Tag, Monat und Jahr an die SQL-Ebene zurückgegeben - also das Datum - möchte man die Uhrzeit haben, sollte man mit java.sql.Timestamp arbeiten. Aber abgesehen davon ist das Mapping solcher Datentypen ja wirklich einfach - und wenn man Java-Bibliotheken in der Datenbank verwendet, sollte man sich am besten stets eine eigene "Schicht" mit Java-Methoden schreiben, die einfach auf SQL und PL/SQL abzubilden sind - wo die Methodensignaturen also am besten nur solche einfachen Datentypen nutzen und keine komplexen Objekte.
Aber jetzt geht's ans Eingemachte: Angenommen, wir haben eine Java-Bibliothek (und als Beispiel nehmen wir mal java.io.File), die ein komplexes Objekt repräsentiert. Und wir möchten nun eben nicht für jedes Attribut einen eigenen Call bauen, sondern alle relevanten Attribute mit einem einzigen Call abholen. Uns prinzipiell gibt es in der Datenbank ja auch Objekttypen bzw. User Defined Types, mit denen man sowas wie ein "File" modellieren kann. Also fangen wir mal damit an.
create type file_t as object(
  file_path      varchar2(4000),
  file_name      varchar2(4000),
  file_size      number,
  last_modified  date,
  is_dir         char(1),
  is_writeable   char(1),
  is_readable    char(1),
  file_exists    char(1)
)
/
Allerdings kann man nun keine direkte Verbindung zwischen dem SQL-Typen FILE_T und der Java-Klasse java.io.File herstellen - beide haben ja überhaupt nichts miteinander zu tun - ein Mapping ist aber mit Hilfe von einer "Java-Brückenklasse" möglich: Mit oracle.sql.STRUCT kann man Objekttypen aus Java heraus erstellen und an SQL bzw. PL/SQL zurückgeben. Was wir also brauchen, ist eine (statische) Java-Methode, die mit Hilfe von java.io.File die Information zu einer Datei holt, damit ein STRUCT für den FILE_T erzeugt und diese kann dann in die SQL-Ebene zurückgibt.
create or replace and compile java source named java_file as
import java.math.*;
import java.util.*;
import java.io.*;

import java.sql.*;

import oracle.sql.*;
import oracle.jdbc.*;

public class JavaFile {
  public static STRUCT getFile(String pFile) throws Exception {
    Connection       con    = DriverManager.getConnection("jdbc:default:connection:");
    StructDescriptor sDescr = StructDescriptor.createDescriptor("FILE_T", con);
    Object[]         o      = new Object[8];

    File f = new File(pFile);
    if (f.exists()) {
     o[0] = f.getPath();
     o[1] = f.getName();
     o[2] = new BigDecimal(f.length());
     o[3] = new java.sql.Timestamp(f.lastModified());
     o[4] = (f.isDirectory()?"Y":"N");
     o[5] = (f.canWrite()?"Y":"N");
     o[6] = (f.canRead()?"Y":"N");
     o[7] = "Y";
     return new STRUCT(sDescr, con, o);
    } else {
     return null;
    } 
  }
}
/
sho err

create or replace function get_file_atts (p_file in varchar2) return file_t
is language java name 'JavaFile.getFile(java.lang.String) return oracle.sql.STRUCT';
/
sho err
Hier muss der Java-Code mit der SQL-Ebene zusammenspielen - deshalb wird zuerst eine JDBC-"Verbindung" aufgebaut - das ist aber nichts weiter als eine Art "Pointer" auf die Datenbanksitzung, denn das Java läuft ja bereits in der Datenbank. Es wird ein StructDescriptor-Objekt erzeugt, welches auf den vorher erzeugten Typen FILE_T zeigt. Alle Attribute des FILE_T werden auf Java-Seite in einem Array der Klasse Object[] abgelegt. Hier muss man als Entwickler aufpassen, dass die verwendeten Java-Typen auf die Datentypen des SQL-Typen passen (siehe einfache Mappings oben). Mit diesem Array, dem StructDescriptor-Objekt und dem Connection-Objekt wird dann ganz zum Schluß ein STRUCT-Objekt generiert, welches genau auf den Typen FILE_T passt. Natürlich kann man auch komplexere Dinge bauen und ein "STRUCT in ein STRUCT" schachteln, man muss nur aufpassen, dass alles zur Definition der Objekttypen in SQL passt.
Zum Abschluß kommt wieder die PL/SQL-Definition der Funktion - in PL/SQL wird als IN Parameter ein VARCHAR2 und als Rückgabewert ein FILE_T deklariert. Folgerichtig passt das auf ein java.lang.String als Eingabe- und ein oracle.sql.STRUCT als Rückgabewert. Alle Objekttypen werden auf Java-Seite als oracle.sql.STRUCT abgebildet - die Verknüpfung mit dem konkreten Objekttypen erledigt der StructDescriptor ...
Alles klar? Dann können wir testen ...
SQL> select get_file_atts('/') from dual;
select get_file_atts('/') from dual
                               *
FEHLER in Zeile 1:
ORA-29532: Java-Aufruf durch nicht abgefangene Java-Exception beendet:
java.security.AccessControlException: the Permission (java.io.FilePermission /
read) has not been granted to SCOTT. The PL/SQL to grant this is
dbms_java.grant_permission( 'SCOTT', 'SYS:java.io.FilePermission', '/', 'read'
)
Ach ja: Zum Dateisystemzugriff braucht es Privilegien - die muss der DBA einräumen. Wenn Ihr vor diesen Meldungen Ruhe haben wollt, gebt eurem Datenbankschema das JAVASYSPRIV-Privileg, dann har er alle Rechte, die man haben kann (für Produktion nicht zu empfehlen). Alternativ könnt Ihr einfach den in der Fehlermeldung dargestellten Aufruf ausführen - der räumt genau das fehlende Privileg ein. Wenn Ihr das Privileg habt, probiert es nochmal ...
SQL> select get_file_atts('/tmp') from dual;

GET_FILE_ATTS('/TMP')(FILE_PATH, FILE_NAME, FILE_SIZE, LAST_MODIFIED, IS_DIR, IS
--------------------------------------------------------------------------------
FILE_T('/tmp', 'tmp', 126976, '21.09.2011 10:25:22', 'Y', 'Y', 'Y', 'Y')

1 Zeile wurde ausgewählt.
So weit so gut. Wir haben es also geschafft, ein strukturiertes Objekt von Java nach PL/SQL zu übertragen. Analog dazu kann man nun für alle Objekte vorgehen:
  • SQL-Objekttypen erzeugen
  • Java Methode erzeugen, die das eigentliche Java-Objekt auf eine STRUCT-Instanz abbildet, dabei mit dem StructDescriptor und dem Object[]-Array arbeiten
  • STRUCT-Instanz aus Java zurückgeben und in SQL übernehmen
  • PL/SQL Wrapper für die neue Java Stored Procedure erstellen
Bleibt die nächste (und im Datenbankumfeld spannende) Aufgabe: Ich möchte ein Directory-Listing abbilden, also eine ganze Reihe von strukturierten Objekten zurückgeben. Im reinen PL/SQL geht haben wir hierfür die Table Functions - und ein ähnliches Konzept nutzen wir auch in Java. Zunächst brauchen wir, wie bei der PL/SQL Table Function, einen Objekttypen, der die Dateiliste repräsentiert - das ist einfach:
create type file_ct as table of file_t
/
Und wieder gilt es, aus Java heraus eine Instanz dieses Typs FILE_CT zu erzeugen. Für Varray- oder Table Types gibt es jedoch eine andere Java-"Brückenklasse": oracle.sql.ARRAY. Der Umgang damit ist aber ganz ähnlich wie bei der Klasse oracle.sql.STRUCT. Es wird ein ArrayDescriptor-Objekt benötigt, der die Verbindung zum konkreten SQL-Typen herstellt, und mit diesem, dem Connection-Objekt und einem Standard-Java-Array wird die Instanz vom Typ oracle.sql.ARRAY erzeugt, die genau auf den FILE_CT passt. Das ganze als Code ...
public class JavaFile {
  public static ARRAY getFileList(String pFile) throws Exception {
    Connection       con    = DriverManager.getConnection("jdbc:default:connection:");
    StructDescriptor sDescr = StructDescriptor.createDescriptor("FILE_T", con);
    ArrayDescriptor  aDescr = ArrayDescriptor.createDescriptor("FILE_CT", con);
    Object[]         o      = new Object[8];

    /* Array containing java File objects */
    File[]   f = new File(pFile).listFiles();

    /* Array containing SQL STRUCT objects */
    STRUCT[] a = new STRUCT[f.length];

    /* now loop through the File array and create a STRUCT instance for each file */
    for (int i=0;i<f.length;i++) {
     o[0] = f[i].getPath();
     o[1] = f[i].getName();
     o[2] = new BigDecimal(f[i].length());
     o[3] = new java.sql.Timestamp(f[i].lastModified());
     o[4] = (f[i].isDirectory()?"Y":"N");
     o[5] = (f[i].canWrite()?"Y":"N");
     o[6] = (f[i].canRead()?"Y":"N");
     o[7] = "Y"
     a[i] = new STRUCT(sDescr, con, o);
    } 
    /* Create and return the ARRAY object which maps to the SQL type */
    return new ARRAY(aDescr, con, a);
  }
}
/
sho err

create or replace function get_file_list (p_file in varchar2) return file_ct
is language java name 'JavaFile.getFileList(java.lang.String) return oracle.sql.ARRAY';
/
sho err
Und das ist im Grunde genommen nur eine Erweiterung obigen Codes. Durch die Liste der File-Objekte, die von java.io.File.listFiles() zurückgegeben wird, laufen wir in einer Schleife durch, erzeugen für jedes File-Objekt eine STRUCT-Instanz und packen auch diese in ein Array. Das wird dann mit dem ArrayDescriptor auf den SQL-Typen FILE_CT abgebildet und als oracle.sql.ARRAY-Objekt zurückgegeben. Fertig - Test.
SQL> select file_name, last_modified, file_size from table(get_file_list('/'))

FILE_NAME                 LAST_MODIFIED        FILE_SIZE
------------------------- ------------------- ----------
wget-log                  27.04.2011 16:24:02        496
boot                      02.02.2011 13:22:58       1024
misc                      25.07.2011 09:29:09          0
stage                     04.04.2011 11:27:57          7
lib                       15.07.2011 11:06:27       4096
etc                       25.07.2011 09:29:07       4096
root                      15.07.2011 11:14:01       4096
bin                       02.02.2011 13:28:45       4096
:                         :                            :
Eigentlich ganz einfach, oder? Mit dem hier vorgestellten lässt sich nun jede beliebige Java-Bibliothek in der Datenbank nutzen - die einzige Voraussetzung ist, dass sie keinen Native-Code verwendet - nur 100%-Java-Bibliotheken laufen in der Datenbank. Zum Ansprechen aus SQL und PL/SQL überlegt man sich dann eine vernünftige Schnittstelle, erzeugt die passenden Java-Klassen, die entweder einfache Datentypen (String, Date, numerische) oder komplexe Datentypen als STRUCT oder ARRAY zurückgeben. Diese werden dann auf SQL-Ebene durch PL/SQL-Packages und Objekttypen repräsentiert. Die Code-Packages zum Dateisystem-Zugriff, zum Umgang mit einem POP3- oder IMAP-Mailserver oder zum Ein- und Auspacken von ZIP-Archiven arbeiten alle genau so - und wie gesagt: Jede andere Java-Bibliothek lässt sich genauso einbinden. Damit gibt es keine Grenzen in der Oracle-Datenbank.
In the past I had quite a number of postinhs in which I made use of the Java engine within the Oracle Database. Examples are the previous posting about automated tweets from PL/SQL (which was about using "twitter4j" in the database) or the postings about File- and operating system access. Today I'd like to elaborate a bit about a fundamental thing which one has to accomplish when using Java in the database: The parameter mapping. This posting is about how to map input or return paramaters in a java method to SQL and PL/SQL types in a package. And the focus will not be on the simple mapping of VARCHAR2, NUMBER or DATE datatypes ... these are rather simple, as we can see with this code example ...
create or replace and compile java source named simple_test as

public class SimpleTest {
  public static String getString() {
    return "Hello World";
  }
  public static java.sql.Timestamp getDate() {
    return new java.sql.Timestamp(new java.util.Date().getTime());
  }
  public static int getNumber() {
    return 4711;
  }
} 
/
sho err

create or replace package simple_test_plsql is
  function get_string return varchar2;
  function get_date return date;
  function get_number return number;
end simple_test_plsql;
/
sho err

create or replace package body simple_test_plsql is
  function get_string return varchar2 as 
  language java name 'SimpleTest.getString() return java.lang.String';
  function get_date return date as
  language java name 'SimpleTest.getDate() return java.sql.Timestamp';
  function get_number return number as
  language java name 'SimpleTest.getNumber() return int';
end simple_test_plsql;
/
sho err

select 
  simple_test_plsql.get_string,
  simple_test_plsql.get_date,
  simple_test_plsql.get_number
from dual
/

GET_STRING           GET_DATE            GET_NUMBER
-------------------- ------------------- ----------
Hello World          21.09.2011 09:19:40       4711
The "experts" might have notices that all java methods are "static" - that's evident. PL/SQL is a procedural, not an object-oriented language - so when we want to integrate PL/SQL with Java in the database we need to use the "procedural part" of Java which are static class methods. If you want to map java methods to procedures and functions in a PL/SQL package you must use static methods for that.
When mapping DATE values from Java to PL/SQL a bit attention is needed. Basically a date can be represented in java using java.sql.Date or java.sql.Timestamp. When those are being mapped to DATE in SQL or PL/SQL, the java.sql.Date class only maps Day, Month and Year - the time in the DATE would be set to midnight. For having the time component also you need to use java.sql.Timestamp. Apart from this the mapping of simple scalar datatypes from Java to SQL and PL/SQL is quite simple. So the first rule is to use simple types whenever possible. Complex objects should only be used when they're really needed ... because, as we will see, they require additional coding ...
And now we'll handle these: Let's assume we have a Java library doing some stuff and for this example we use the java.io.File class (which is part of standard Java). We'd like to access file attributes with SQL functions and we don't want to have a single call for each attribute. We need a SQL function which collects all attributes in one call - so we need a datatype containing all these attributes at the SQL side. We have Object types (or User-Defined-Types) for that purpose so the first thing we want to do is to create an object type representing a file.
create type file_t as object(
  file_path      varchar2(4000),
  file_name      varchar2(4000),
  file_size      number,
  last_modified  date,
  is_dir         char(1),
  is_writeable   char(1),
  is_readable    char(1),
  file_exists    char(1)
)
/
So far we have a Java representation for a file: java.io.File - and we have a SQL representation for a file: Our new FILE_T. But there is absolutly no connection between those. Of course, we cannot map the java.io.File class to our FILE_T type - the database has no clue how to map the attributes. We need to build a "bridge" between java.io.File and FILE_T. And this bridge is a special java class: oracle.sql.STRUCT. So we now need to implement a static Java method (we can only use static methods) which uses java.io.File to collect file attributes and builds a oracle.sql.STRUCT instance which can be mapped to FILE_T. This code goes here ...
create or replace and compile java source named java_file as
import java.math.*;
import java.util.*;
import java.io.*;

import java.sql.*;

import oracle.sql.*;
import oracle.jdbc.*;

public class JavaFile {
  public static STRUCT getFile(String pFile) throws Exception {
    Connection       con    = DriverManager.getConnection("jdbc:default:connection:");
    StructDescriptor sDescr = StructDescriptor.createDescriptor("FILE_T", con);
    Object[]         o      = new Object[8];

    File f = new File(pFile);
    if (f.exists()) {
     o[0] = f.getPath();
     o[1] = f.getName();
     o[2] = new BigDecimal(f.length());
     o[3] = new java.sql.Timestamp(f.lastModified());
     o[4] = (f.isDirectory()?"Y":"N");
     o[5] = (f.canWrite()?"Y":"N");
     o[6] = (f.canRead()?"Y":"N");
     o[7] = "Y";
     return new STRUCT(sDescr, con, o);
    } else {
     return null;
    } 
  }
}
/
sho err

create or replace function get_file_atts (p_file in varchar2) return file_t
is language java name 'JavaFile.getFile(java.lang.String) return oracle.sql.STRUCT';
/
sho err
The Java engine within the database needs to interact with the SQL layer - we need a JDBC database connection for that. This connection is not more than kind of a pointer to the actual database session in which the java code runs in. The StructDescriptor class is a utility which helps us to create a STRUCT object exactly matching FILE_T. Note the usage of the connection object and "FILE_T" when the StructDescriptor instance is being created. After that we collect all relevant file attributes in an Array of the fundamental Java class Object[]. The developer needs to take care about the order within that array: Our object type has eight simple, scalar attributes. The Java types in the Object array (String, int, java.sql.Date) must match the attributes of the object type (VARCHAR, NUMBER, DATE). With this array, the StructDescriptor instance, and the database connection the STRUCT instance is being created and returned in the last step of the program. This STRUCT instance exactly matches the FILE_T definition in the SQL layer.
The final step as (as always) the PL/SQL Wrapper for the static method. We create a function which takes a VARCHAR2 (containing the file path) and returns a FILE_T. These are being mapped to java.lang.String and oracle.sql.STRUCT . All object types are being encoded as oracle.sql.STRUCT - the StructDescriptor objects cares for the mapping to the correct SQL type.
Got it ...? Then try it!
SQL> select get_file_atts('/') from dual;
select get_file_atts('/') from dual
                               *
FEHLER in Zeile 1:
ORA-29532: Java-Aufruf durch nicht abgefangene Java-Exception beendet:
java.security.AccessControlException: the Permission (java.io.FilePermission /
read) has not been granted to SCOTT. The PL/SQL to grant this is
dbms_java.grant_permission( 'SCOTT', 'SYS:java.io.FilePermission', '/', 'read'
)
Ah, yes: You need Java privileges in order to access the file system with a Java stored porcedure. In the developing stage you might grant yourself the JAVASYSPRIV privilege - you will be a "Java superuser" then - on production systems this is not recommended you might grant individual privileges there. The practical bit is that Oracle not only throws the error message - it also gives the complete PL/SQL call to grant the required permission. So when you have the privilege, try again ...
SQL> select get_file_atts('/tmp') from dual;

GET_FILE_ATTS('/TMP')(FILE_PATH, FILE_NAME, FILE_SIZE, LAST_MODIFIED, IS_DIR, IS
--------------------------------------------------------------------------------
FILE_T('/tmp', 'tmp', 126976, '21.09.2011 10:25:22', 'Y', 'Y', 'Y', 'Y')

1 row selected.
So far - so good. We managed to create a structured object in java and pass it to PL/SQL. You can use this approach for any kind of structured object - and yes: You can nest one oracle.sql.STRUCT within another oracle.sql.STRUCT - just as you can nest object types in each other. The basic steps are:
  • Create the SQL object types
  • Create the static Java method which creates the STRUCT instance matching the object type using the Object[] array and the StructDescriptor class.
  • Return the STRUCT instance from java to SQL and PL/SQL
  • Create the PL/SQL wrapper for your Java stored procedure
Then we'll move on to the next (and more interesting) level: We want to pass not only a structured object but a list of structured objects from Java to PL/SQL. In the PL/SQL world we have Table Functions for that purpose - and as with these we now first create another object type representing our list of files ...
create type file_ct as table of file_t
/
And again: We now need to create an object within Java which maps to this FILE_CT type. But for Varray or table types there is another "Bridging class": oracle.sql.ARRAY. It's used the same way as oracle.sql.STRUCT. We first create an ArrayDescriptor instance using the JDBC connection and the type name. This helper object, the JDBC connection and a plain Java array are being used to create the actual instance of oracle.sql.ARRAY. And the code goes here ...
public class JavaFile {
  public static ARRAY getFileList(String pFile) throws Exception {
    Connection       con    = DriverManager.getConnection("jdbc:default:connection:");
    StructDescriptor sDescr = StructDescriptor.createDescriptor("FILE_T", con);
    ArrayDescriptor  aDescr = ArrayDescriptor.createDescriptor("FILE_CT", con);
    Object[]         o      = new Object[8];

    /* Array containing java File objects */
    File[]   f = new File(pFile).listFiles();

    /* Array containing SQL STRUCT objects */
    STRUCT[] a = new STRUCT[f.length];

    /* now loop through the File array and create a STRUCT instance for each file */
    for (int i=0;i<f.length;i++) {
     o[0] = f[i].getPath();
     o[1] = f[i].getName();
     o[2] = new BigDecimal(f[i].length());
     o[3] = new java.sql.Timestamp(f[i].lastModified());
     o[4] = (f[i].isDirectory()?"Y":"N");
     o[5] = (f[i].canWrite()?"Y":"N");
     o[6] = (f[i].canRead()?"Y":"N");
     o[7] = "Y"
     a[i] = new STRUCT(sDescr, con, o);
    } 
    /* Create and return the ARRAY object which maps to the SQL type */
    return new ARRAY(aDescr, con, a);
  }
}
/
sho err

create or replace function get_file_list (p_file in varchar2) return file_ct
is language java name 'JavaFile.getFileList(java.lang.String) return oracle.sql.ARRAY';
/
sho err
As you see: This code is basically just a small extension of the code above. We loop through the File[] array returned by java.io.File.listFiles(), create an oracle.sql.STRUCT instance for each java.io.File object and place it into a Java array (STRUCT[]). This java array is then being used to create the oracle.sql.ARRAY instance, which is finally passed back to PL/SQL. On the PL/SQL side we create the wrapper function and that's it. Try ...
SQL> select file_name, last_modified, file_size from table(get_file_list('/'))

FILE_NAME                 LAST_MODIFIED        FILE_SIZE
------------------------- ------------------- ----------
wget-log                  27.04.2011 16:24:02        496
boot                      02.02.2011 13:22:58       1024
misc                      25.07.2011 09:29:09          0
stage                     04.04.2011 11:27:57          7
lib                       15.07.2011 11:06:27       4096
etc                       25.07.2011 09:29:07       4096
root                      15.07.2011 11:14:01       4096
bin                       02.02.2011 13:28:45       4096
:                         :                            :
It's quite simpe, isn't it? Using this approach you can pass any data structure between Java and PL/SQL - you can use any Java library you want. The only prerequisite is that your Java library does not depend on native code - it must be pure Java. First you might design a simple interface consisting of static Java methods which can be easily mapped to PL/SQL calls. Use simple, scalar types whenever possible and use object types and arrays only when you really need them. Then you need to implement the static java methods (using STRUCT and ARRAY, if necessary), create the SQL object types and PL/SQL wrapper packages and you are done. The code packages for filesystem access, zum accessing POP3 or IMAP4 mail servers with PL/SQL or topack and unpack ZIP archives all work that way. And as said: You can use integrate any Java library doing interesting stuff into the Oracle database. There are no limits.

Beliebte Postings