9. Externe UDF und Stored Procedures

Werbung
9. Externe UDF und Stored Procedures
Datenbankprogrammierung
Roadmap
intern
intern/extern
SQL UDF Trigger
ext. UDF
SQL SP XQUERY
Embedded SQL SQLJ
ext. SP
extern
JDBC ODBC
OLE .NET LINQ
Kommunikationslast
Datenbankabhängigkeit
2
I. Externe UDFs
Externe UDFs
EXTERNE NUTZERDEFINIERTE FUNKTIONEN
§ in höherer Programmiersprache geschrieben
(z.B. C, Java, C#)
§ Warum extern?
- nahezu beliebige Logik implementierbar
(kein schreibender Datenbankzugriff)
- Zugriff auf bestehende externe
Softwarekomponenten möglich
§ Arten
- Externe Skalarfunktion (external scalar)
- Externe Tabellenfunktion (external table)
GRUNDLEGENDER ABLAUF
1. Erstellen der Funktion
2. Kompilierung
3. optional: Erzeugen einer Library
4. Speichern des Binärcodes im Datenbankserver
à sqllib/function oder
sqllib/function/unfenced
5. Registrierung mittels CREATE FUNCTION
4
Externe Skalarfunktionen
Externe Skalarfunktionen - Syntax
SYNTAX
CREATE FUNCTION <NAME> ([<PAR-NAME>] <PAR-TYPE>, …)
RETURNS <TYPE> [CAST FROM <TYPE>]
neu [SPECIFIC <UNIQUE-NAME>]
EXTERNAL NAME <EXTERNAL-NAME>
neu
sprachabhängig
LANGUAGE C|JAVA|CLR|OLE
PARAMETER STYLE DB2GENERAL|JAVA|SQL
[[NOT] DETERMINISTIC]
[[NOT] FENCED]
[RETURNS NULL ON NULL INPUT | CALLED ON NULL INPUT]
[NO SQL | CONTAINS SQL | READS SQL DATA]
[[NO] EXTERNAL ACTION]
neu [NO SCRATCHPAD | SCRATCHPAD <LENGTH>]
neu [[NO] FINAL CALL]
neu [ALLOW PARALLEL | DISALLOW PARALLEL]
neu [[NO] DBINFO]
neu
neu
6
Externe Skalarfunktionen - Syntax
OPTIONEN
§ siehe “5. Nutzerdefinierte Funktionen & Stored
Procedures”
§ CAST FROM: automatische Typkonvertierung
-
z.B.: RETURNS DATE CAST FROM CHAR(10)
§ EXTERNAL NAME: Zugriffspfad auf externe UDF
-
z.B. für Java: '[<jarid>:]<package>.<subpackage>.<class-id>!<methodid>'
§ LANGUAGE: verwendete Programmiersprache
§ PARAMETER STYLE: Art der
Parameterübergabe
§ FENCED: DBMS-externe Ausführung
§ RETURNS NULL ON NULL INPUT: Aufruf nur,
falls alle Argumente nicht-null
§ NO SQL: Transaktionsverhalten: Funktion führt
keinerlei SQL-Statement aus
§ SCRATCHPAD: Zwischenspeicherung von
Daten
§ FINAL CALL: Abschließender Aufruf (ähnlich
Destruktor)
§ ALLOW PARALLEL: mehrere, parallel
verwendete Instanzen der UDF erlauben
- Standardwert falls weder NOT
DETERMINISTIC, EXTERNAL ACTION,
SCRATCHPAD, oder FINAL CALL
angegeben
§ DBINFO: Informationen über die Datenbank an
UDF weitergeben
http://publib.boulder.ibm.c om/infocenter/db2luw/v 9r7/topic/c om.ibm.db2.l uw.sql.ref.doc/doc/r0004461.html
7
Fenced vs. Unfenced
FENCED VS. UNFENCED
§ FENCED-Modus (Standard)
- UDF läuft in eigenem Prozess
- kein Zugriff auf DB2-Interna
- sicher, aber langsam
§ NOT FENCED-Modus
- in DB2-Engine ausgeführt à schneller
- für bereits erprobte, vertrauenswürdige UDFs
§ Wechsel des Ausführungsmodus
- ALTER FUNCTION <external-functoin> NOT FENCED
- erfordert SYSADM-Privileg oder DBADM- bzw. CREATE_NOT_FENCED_ROUTINE-Rechte
- bestimmt Ablageverzeichnis: sqllib/function oder sqllib/function/unfenced
8
Externe Skalarfunktionen
BEISPIEL
§ CREATE FUNCTION HELLOWORLD()
RETURNS VARCHAR(15)
EXTERNAL NAME 'UDFJavaStyleExample!helloWorld‘
LANGUAGE JAVA
PARAMETER STYLE JAVA
DETERMINISTIC
NO SQL
NO EXTERNAL ACTION;
§ VALUES(HELLOWORLD())
1
-----------Hello World
9
Externe Tabellenfunktionen
Externe Tabellenfunktionen - Syntax
SYNTAX
CREATE FUNCTION <NAME> ([<PAR-NAME>] <PAR-TYPE>, …)
neu RETURNS TABLE(<COL-NAME> <COL-TYPE>, …)
[SPECIFIC <UNIQUE-NAME>]
EXTERNAL NAME <EXTERNAL-NAME>
sprachabhängig
neu LANGUAGE C|JAVA|CLR|OLE
PARAMETER STYLE DB2GENERAL|SQL
[NOT] DETERMINISTIC]
[[NOT] FENCED]
[RETURNS NULL ON NULL INPUT | CALLED ON NULL INPUT]
[NO SQL | CONTAINS SQL | READS SQL DATA]
[[NO] EXTERNAL ACTION]
[NO SCRATCHPAD | SCRATCHPAD <LENGTH>]
[[NO] FINAL CALL]
neu [DISALLOW PARALLEL | ALLOW PARALLEL EXECUTE ON ALL DATABASE PARTITIONS RESULT TABLE DISTRIBUTED]
[[NO] DBINFO]
11
Externe Tabellenfunktionen - Syntax
OPTIONEN
§ siehe externe Skalarfunktionen
§ ALLOW PARALLEL EXECUTE ON ALL DATABASE PARTITIONS
RESULT TABLE DISTRIBUTED
- für partitionierte Datenbanken
- Funktion wird auf jeder Partition ausgeführt, Ergebnisse vereinigt
§ DISALLOW PARALLEL
- Funktion wird nur auf aktueller Partition ausgeführt
- sollte bei nicht-partitionierter Datenbank angegeben werden
HTTP://PUBLIB.BOULD ER.IB M. COM/I NFOCE NTE R/ DB2 L UW /V 9R 7/TOPIC /CO M.IBM .DB 2. LUW .SQL .REF .DO C/ DOC /R 0004462. HTML
12
Externe Tabellenfunktionen - Beispiel
BEISPIEL
§ CREATE FUNCTION ALL_EMPLOYEES()
RETURNS TABLE(FULLNAME VARCHAR(128))
EXTERNAL NAME 'UDFDb2generalStyleExample!allEmployees‘
LANGUAGE JAVA
PARAMETER STYLE DB2GENERAL
DETERMINISTIC
READS SQL DATA
NO SCRATCHPAD
NO EXTERNAL ACTION
DISALLOW PARALLEL;
§ SELECT * FROM TABLE(ALL_EMPLOYEES()) X
FULLNAME
--------------------CHRISTINE HAAS
MICHAEL THOMPSON
…
13
Nutzerdefinierte Funktionen in Java
Nutzerdefinierte Funktionen in Java
NUTZERDEFINIERTE FUNKTIONEN IN JAVA
§ Ablage als Methode einer Klasse
- Klasse und Methode müssen public sein
- je nach Parameterart zusätzlich static
§ Unterstützte Parameterarten
- JAVA: nur einfache UDFs
- DB2GENERAL: beliebige UDFs möglich
§ FENCED und UNFENCED Modus
§ Zugriff auf Standard-I/O-Ströme nicht möglich (System.out, …)
AUFBAU DES EXTERNEN NAMENS (EXTERNAL NAME)
§ Angabe der Klasse und Methode, optional auch der Bibliothek
§ [<jar-id>:]<package>.<subpackage>.<class-id>!<method-id>
§ Ablage der kompilierten Klasse/Bibliothek in sqllib/function
15
Parameterart JAVA
PARAMETERART JAVA
§ nur externe Skalarfunktionen
§ kein SCRATCHPAD, FINAL CALL, DBINFO
§ Aufbau
- Klasse als public definiert
- Methode als public static definiert
- Parameter als Java-Typen, automatisches Marshalling
- Rückgabewert der Methode wird an Datenbanksystem weitergeleitet
§ Zugriff auf Datenbank
- nur lesend
- mittels Standardverbindung:
DriverManager.getConnection("jdbc:default:connection")
- Fehler werden an das DBMS weitergereicht
à Definition mit throws SQLException
16
Parameterart JAVA - Beispiele
PUBLIC
}
PUBLIC
}
PUBLIC
}
STATIC
RETURN
STRING HELLOWORLD() {
"HELLO WORLD";
STATIC INT MOD 10(INT X )
RETURN X % 10;
{
VALUES(HELLO_WORLD());
1
--------------Hello World
VALUES(MOD10(7), MOD10(17));
1
2
----------- ----------7
7
STATIC INT NO EMPLOYEES () THROWS SQLEXCEPTION {
CONNECTION CON =
DRIVERMANAGER.GETCONNECTION("JDBC:DEFAULT:CONNECTION");
STATEMENT STMT = CON.CREATESTATEMENT();
RESULTSET RS = STMT.EXECUTEQUERY(
"SELECT COUNT(*) FROM EMPLOYEE");
RS .NEXT ();
INT NO EMPLOYEES = RS .GET INT (1);
RS .CLOSE ();
STMT .CLOSE ();
CON .CLOSE ();
RETURN NO EMPLOYEES ;
VALUES(NO_EMPLOYEES());
1
----------42
17
Parameterart DB2GENERAL
PARAMETERART DB2GENERAL
§ externe Skalar- und Tabellenfunktionen
§ Aufbau
- Klassen erben von COM.ibm.db2.app.UDF
- Klasse und Methode als public definiert (Methoden nicht static)
- Fehler werden an das DBMS weitergereicht
à Definition mit throws Exception
- Ein- und Ausgabeparameter als Java-Typen in Argumentliste im Methodenkopf, automatisches
Marshalling
18
UDF-Klasse
WICHTIGE METHODEN DER UDF-KLASSE
§ Connection getConnection()
- Liefert Connection-Objekt für aufrufende
Datenbank
§ boolean isNull(int index)
- Überprüfung eines Eingabearguments auf
NULL-Wert
§ void set(int index, *)
- Setzen von Ausgabeparametern (1-basiert)
§ boolean needToSet(int index)
- Erkennung nicht-verwendeter
Ausgabeparametern bei „projection push
down“
(nur bei Tabellenfunktionen)
§ setSQLstate(String)/
setSQLmessage(String)
- Rückgabe von Fehlermeldungen
§ byte[] getScratchpad() /
setScratchpad(byte[])
- Auslesen/Setzen des Scratchpad (später mehr)
§ int getCallType()
- Aufruftyp ermitteln
• Skalarfunktionen:
SQLUDF_FIRST_CALL,
SQLUDF_NORMAL_CALL
• Tabellenfunktionen: später…
§ * getDB*()
- Informationen über aufrufende Datenbank
(Angabe von DBINFO erforderlich)
http://publib.boulder.ibm.c om/infocenter/db2luw/v 9r7/topic/c om.ibm.db2.l uw.apdv.r outi nes.doc /doc/r0008987.html
19
Parameterart DB2GENERAL - Beispiele
PUBLIC
}
PUBLIC
}
PUBLIC
VOID HELLO WORLD (STRING RESULT ) THROWS
SET (1, "HELLO WORLD ");
EXCEPTION {
VOID MOD 10(INT X , INT RESULT ) THROWS
SET (2, X % 10);
EXCEPTION {
VOID NO EMPLOYEES (INT RESULT ) THROWS EXCEPTION
CONNECTION CON = GETCONNECTION();
STATEMENT STMT = CON.CREATESTATEMENT();
RESULTSET RS = STMT.EXECUTEQUERY(
{
"SELECT COUNT(*) FROM EMPLOYEE");
}
RS .NEXT ();
SET (1, RS .GET INT (1));
RS .CLOSE ();
STMT .CLOSE ();
20
Externe Tabellenfunktionen in Java
EXTERNE TABELLENFUNKTIONEN IN JAVA
§ Parameterart DB2GENERAL
§ Iterator-Konzept
- Menge der Ergebnistupel wird iterativ zurückgeliefert (fetch)
- ähnlich Cursor
- Aufruftyp (abfragen mit: int getCallType())
• SQLUDF_TF_OPEN
à Öffnen
• SQLUDF_TF_FETCH à Tupel zurückliefern
• SQLUDF_TF_CLOSE à Schließen
• bei Angabe von FINAL CALL zusätzlich SQLUDF_TF_FIRST, SQLUDF_TF_FINAL
§ Attribute der Tabelle
- zusätzliche Parameter am Ende der Parameterliste der Methodendeklaration
- setzen mittels set(int index, *)
21
Beispiel mit Instanzvariablen
PRIVATE S TATEMENT
PRIVATE R ESULTS ET
STMTE MPLOYEES;
RSE MPLOYEES;
PUBLIC VOID ALLE MPLOYEES(S TRING NAME) THROWS
SWITCH ( GETC ALLT YPE()) {
CASE SQLUDF_TF_OPEN:
CASE
CASE
}
}
Ausgabeparameter nach
Schema der Ergebnistabelle
E XCEPTION {
C ONNECTION CON = GETC ONNECTION();
STMTE MPLOYEES = CON. CREATES TATEMENT();
RSE MPLOYEES = STMTE MPLOYEES. EXECUTEQ UERY(
"SELECT FIRSTNME, LASTNAME FROM EMPLOYEE");
BREAK;
SQLUDF_TF_FETCH:
IF (! RSE MPLOYEES. NEXT()) {
//2000 – E NDE DER E RGEBNISMENGE
SETSQL STATE("02000");
} ELSE {
SET(1, RSE MPLOYEES. GETS TRING("FIRSTNME") + " "
+ RSE MPLOYEES. GETS TRING("LASTNAME"));
}
BREAK;
SQLUDF_TF_CLOSE:
RSE MPLOYEES. CLOSE();
STMTE MPLOYEES. CLOSE();
BREAK;
SELECT * FROM TABLE(ALL_EMPLOYEES()) X
FULLNAME
--------------------CHRISTINE HAAS
MICHAEL THOMPSON
…
22
Externe Tabellenfunktionen in Java
SPEICHERUNG VON ZWISCHENERGEBNISSEN
§ 2 Varianten
- mittels Instanzvariablen der Klasse
- mittels Scratchpad
SCRATCHPAD
§
§
§
§
§
Option SCRATCHPAD in der CREATE FUNCTION-Anweisung
Zwischenspeicher mit fest vorgegebener Länge (in Byte)
Abfragen mittels getScratchpad()
Setzen mittels setScratchpad(byte[]), gleiche Länge!
für Java-UDFs nicht notwendig
23
Beispiel mit Scratchpad
PUBLIC VOID SEQ(INT FROM, INT TO, INT CUR) THROWS EXCEPTION {
// VARIABLES TO READ FROM SCRATCHPAD AREA
BYTE[] SCRATCHPAD = GETSCRATCHPAD();
BYTEARRAYINPUTSTREAM BYTEARRAYIN = NEW BYTEARRAYINPUTSTREAM(SCRATCHPAD);
DATAINPUTSTREAM DATAIN = NEW DATAINPUTSTREAM(BYTEARRAYIN);
// VARIABLES TO WRITE INTO SCRATCHPAD AREA
BYTE[] BUFFER;
BYTEARRAYOUTPUTSTREAM BYTEARRAYOUT = NEW BYTEARRAYOUTPUTSTREAM(SCRATCHPAD.LENGTH);
DATAOUTPUTSTREAM DATAOUT = NEW DATAOUTPUTSTREAM(BYTEARRAYOUT);
SWITCH (GETCALLTYPE()) {
CASE SQLUDF_TF_OPEN:
CUR = FROM;
DATAOUT.WRITEINT(CUR);
BUFFER = BYTEARRAYOUT.TOBYTEARRAY();
SYSTEM.ARRAYCOPY(BUFFER, 0, SCRATCHPAD, 0, BUFFER.LENGTH);
SETSCRATCHPAD(SCRATCHPAD);
BREAK;
CASE SQLUDF_TF_FETCH:
CUR = DATAIN.READINT();
IF (CUR > TO) {
SETSQLSTATE("02000");
} ELSE {
SET(3, CUR);
CUR++;
DATAOUT.WRITEINT(CUR);
BUFFER = BYTEARRAYOUT.TOBYTEARRAY();
SYSTEM.ARRAYCOPY(BUFFER, 0, SCRATCHPAD, 0,
}
BUFFER.LENGTH);
SETSCRATCHPAD(SCRATCHPAD);
BREAK;
CASE SQLUDF_TF_CLOSE:
BREAK;
}
In Java typischerweise Umsetzung
mittels Instanzvariablen!
SELECT * FROM TABLE(SEQ(5, 6)) X
CNT
----------5
6
}
24
Externe Stored Procedures
http://publib.boulder.ibm.com/infocenter/db2luw/v9r7/topic/com.ibm.db2.luw.sql.ref.doc/doc/r0008328.html
Externe Stored Procedures
EXTERNE STORED PROCEDURES
§ Definition und Implementierung sehr ähnlich externen UDFs
GRUNDLEGENDER ABLAUF
1. Erstellen der Stored Procedure
2. Kompilierung
3. optional: Erzeugen einer Library
4. Speichern des Binärcodes im Datenbankserver
– sqllib/function oder sqllib/function/unfenced
5. Registrierung mittels CREATE PROCEDURE
26
Syntax
SYNTAX
CREATE PROCEDURE <NAME> (IN|OUT|INOUT <PAR-NAME> <PAR-TYPE>, … )
[SPECIFIC <UNIQUE-NAME>]
EXTERNAL NAME <EXTERNAL-NAME>
neu
sprachabhängig
LANGUAGE C|JAVA|OLE
PARAMETER STYLE JAVA|DB2GENERAL|DB2SQL|GENERAL|GENERAL WITH NULLS
[[NOT] DETERMINISTIC]
[DYNAMIC RESULT SETS <NUMBER>]
[NO SQL | CONTAINS SQL |READS SQL DATA | MODIFIES SQL DATA]
[CALLED ON NULL INPUT]
[[NO] DBINFO]
[[NOT] FENCED]
neu
[PROGRAM TYPE SUB | MAIN]
27
Syntax
OPTIONEN
§ Siehe Externe Funktionen
§ PROGRAM TYPE SUB
- Parameter werden als eigenständige Argumente übergeben
§ PROGRAM TYPE MAIN
- Parameterübergabe wie in Main-Funktion (argc und argv[])
- Aufruf beliebiger Programme möglich
- nur in C
28
Stored Procedures (Java)
Stored Procedures in Java
STORED PROCEDURES IN JAVA
§ Siehe externe UDFs
§ Methoden besitzen keinen Rückgabewert
PARAMETERART JAVA
§ OUT und INOUT-Parameter als einelementige Arrays definiert
§ andere Parameter als einfacher Java-Datentyp
public static void <methode>(
<Datentyp>[] <parametername>, …){
<parametername>[0] = <value>;
}
30
Parameterart Java
BEISPIEL
public static void mod10 (
int x,
int[] result)
throws Exception {
result[0] = x % 10;
CALL mod10(25,?)
Value of output parameters
-------------------------Parameter Name : RESULT
Parameter Value : 5
Return Status = 0
}
public static void getNoEmployees(String job, int[] num)
throws Exception {
Connection con =
DriverManager.getConnection("jdbc:default:connection");
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("SELECT count(*) "
+ "FROM employee WHERE job='" + job + "'");
rs.next();
CALL getNoEmployees('DESIGNER',?)
Value of output parameters
num[0] = rs.getInt(1);
-------------------------rs.close();
Parameter Name : NUM
stmt.close();
Parameter Value : 10
Return Status = 0
31
Parameterart JAVA
BEISPIEL
CREATE PROCEDURE mod10 (IN x int, OUT res int)
LANGUAGE Java
PARAMETER STYLE JAVA
EXTERNAL NAME 'SPJavaStyleExample.mod10'
CREATE PROCEDURE getNoEmployees ( IN job CHAR(15),
OUT num int)
LANGUAGE Java
PARAMETER STYLE JAVA
EXTERNAL NAME 'SPJavaStyleExample.getNoEmployees'
32
Parameterart DB2GENERAL
PARAMETERART DB2GENERAL
§ Aufbau
- Klassen erben von COM.ibm.db2.app.StoredProc
- Klasse und Methode als public definiert
WICHTIGE METHODEN DER STOREDPROC-KLASSE
§ Connection getConnection()
- Liefert Connection-Objekt für aufrufende Datenbank
§ boolean isNull(int index)
- Überprüfung eines Eingabearguments auf NULL-Wert
§ void set(int index, *)
- Setzen von Ausgabeparametern (1-basiert)
HTTP://PUBLIB.BOULD ER.IB M. COM/I NFOCE NTE R/ DB2 L UW /V 9R 7/TOPIC /CO M.IBM .DB 2. LUW .AP DV. ROUTI NES. DO C/ DOC/ R 0008986.HT ML
33
Parameterart DB2GENERAL
BEISPIEL
public void getNoEmployees(String job, int num) throws Exception {
Connection con = getConnection();
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT count(*) FROM employee WHERE job='" + job + "'");
rs.next();
CALL getNoEmployees('DESIGNER',?)
set(2, rs.getInt(1));
rs.close();
Value of output parameters
stmt.close();
-------------------------Parameter Name : NUM
con.close();
Parameter Value : 10
}
Return Status = 0
34
Rückgabe von Ergebnismengen
RÜCKGABE VON ERGEBNISMENGEN
§ Verwendung eines ResultSet-Array als zusätzlicher Parameter
§ Statement ausführen und ResultSet übergeben
ResultSet rs[0] = stmt.executeQuery();
§ Statement offen lassen, Verbindung schließen
§ DYNAMIC RESULT SETS <number> (maximal 32767)
35
Rückgabe von Ergebnismengen
BEISPIEL
public static void getEmployees(String job, ResultSet[] rs)
throws SQLException {
Connection con = DriverManager.getConnection("jdbc:default:connection");
String query = "SELECT firstnme, lastname “ + "FROM employee " + "WHERE job=?";
PreparedStatement stmt = con.prepareStatement(query);
stmt.setString(1, job);
rs[0] = stmt.executeQuery();
con.close();
}
Statement nicht schließen
CREATE PROCEDURE GETEMPLOYEES
( IN job VARCHAR(15) )
DYNAMIC RESULT SETS 1
LANGUAGE JAVA
EXTERNAL NAME 'MyClass.getEmployees'
PARAMETER STYLE JAVA
CALL getEmployees('DESIGNER')
FIRSTNME
-----------BRUCE
ELIZABETH
MASATOSHI
…
LASTNAME
--------------ADAMSON
PIANKA
YOSHIMURA
…
36
Entwicklung von java-basierten UDFs/SPs
AKTUALISIERUNG DER KLASSEN
§ DB2 lädt bereits geladene Klassen nicht neu
à Instanzparameter KEEPFENCED
http://publib.boulder.ibm.c om/infocenter/db2luw/v 9r7/topic/c om.ibm.db2.l u
w.admin.config.doc/doc/r 0000274.html
- YES: Weiterverwendung bereits geladener
Klassen (Produktionssystem)
- NO: bei jedem UDF/SP-Aufruf Klasse neu
laden (Entwicklungssystem)
KOMPILIERUNG
§ Bibliotheken
- UDFs/SPs mit DB2GENERAL-Parameterart
benötigen DB2-Library
- befindet sich unter
~/sqllib/java/db2java.zip
- aber: bereits im Standardklassenpfad
§ Kompilierung mittels javac <filename>
§ Kopieren nach:
~/sqllib/function/<PackageOrdnerstruktur>
ODER
§ Buildskript: /home/dbprog/build/build-rtnjava.sh <filename>
§ Kopiert die Binärdateien direkt in
~/sqllib/function
37
Zusammenfassung
EXTERNE NUTZERDEFINIERTE FUNKTIONEN
§ Skalar- und Tabellenfunktionen in Java
§ Zwischenergebnisse speichern über Instanzvariablen oder Scratchpad
EXTERNE STORED PROCEDURES
§ Rückgabe von Ergebnismengen
§ Stored Procedures in Java
QUELLTEXT DER BEISPIELE
§ Java: /home/dbprog/examples/9/UDF*.*
/home/dbprog/examples/9/SP*.*
38
Herunterladen