Erstellen von Java-UDFs

Unter diesem Thema wird das Erstellen und Installieren einer Java-UDF (benutzerdefinierte Funktion) erklärt.

Unter diesem Thema:

Schreiben von Java-Code

Schreiben von Java-Klassen und -Methoden

Schreiben Sie eine Klasse, die den unten stehenden Spezifikationen entspricht:

  • Definieren Sie die Klasse als „public“.

  • Definieren Sie eine öffentliche Methode innerhalb der Klasse.

    Wenn die Methode keine statische Methode ist, dann muss die Klasse, die die Methode enthält, einen Null-Argument-Konstruktor oder gar keinen Konstruktoren aufweisen. (Wenn Snowflake die Klasse für eine nicht statische Methode instanziiert, übergibt Snowflake keine Argumente).

    Wenn der Konstruktor einen Fehler auslöst, wird dieser Fehler zusammen mit der Ausnahmemeldung als Benutzerfehler ausgelöst.

  • Wenn die Methode Argumente akzeptiert, muss jedes Argument einer der Datentypen sein, die in der Spalte Java Data Type der SQL-Java-Typzuordnungstabelle angegeben sind.

    Berücksichtigen Sie bei der Auswahl der Datentypen von Java-Variablen die maximal und minimal möglichen Werte der Daten, die von Snowflake gesendet (und an Snowflake zurückgegeben) werden könnten.

    Methodenargumente werden über die Position gebunden, nicht über den Namen. Das erste Argument, das an die UDF übergeben wird, ist das erste Argument, das von der Java-Methode empfangen wird. In den folgenden Codebeispielen entspricht das SQL-UDF-Argument a dem Java-Methodenargument x, und b entspricht y:

    create function my_udf(x numeric(9, 0), y float) ...
    
    public static int my_udf(int a, float b) ...
    
  • Geben Sie einen geeigneten Rückgabetyp an. Da eine Java-UDF eine skalare Funktion sein muss, muss sie bei jedem Aufruf einen Wert zurückgeben. Der Rückgabetyp muss einer der in der Spalte Java Data Type der SQL-Java-Typzuordnungstabelle angegebenen Datentypen sein. Der Rückgabetyp muss mit dem SQL-Datentyp kompatibel sein, der in der RETURNS-Klausel der CREATE FUNCTION-Anweisung angegeben ist.

  • Ihre Klasse kann mehr als eine Methode enthalten. Die Methode, die von Snowflake aufgerufen wird, kann andere Methoden der gleichen Klasse oder von anderen Klassen aufrufen.

  • Ihre Klasse kann auch mehr als eine direkt aufrufbare Methode enthalten. Ihre Klasse könnte z. B. die Methoden call_me_1() und call_me_2() enthalten, und jede dieser Methoden könnte andere Methoden aufrufen.

    Wenn Ihre Klasse mehr als eine direkt aufrufbare Methode enthält, dann erstellen Sie Ihre Java-UDF(s) als vorkompilierte UDF. Sie erstellen eine JAR-Datei und führen zwei (oder mehr) CREATE FUNCTION-Anweisungen aus, von denen jede in ihrer HANDLER-Klausel eine andere Funktion angibt.

  • Ihre Methode (und alle Methoden, die von Ihrer Methode aufgerufen werden) muss die Snowflake-Einschränkungen für Java-UDFs einhalten.

Erstellen der Funktion in Snowflake

Die Informationen in diesem Abschnitt gelten für alle Java-UDFs, unabhängig davon, ob der Code inline oder vorkompiliert angegeben wird.

Sie müssen eine CREATE FUNCTION-Anweisung ausführen, um Folgendes anzugeben:

  • Name der zu verwendenden SQL-Funktion

  • Name der Java-Methode, die aufgerufen werden soll, wenn die Java-UDF aufgerufen wird

Der Name der UDF muss nicht mit dem Namen der in Java geschriebenen Handler-Methode übereinstimmen. Die Anweisung CREATE FUNCTION verknüpft den UDF-Namen mit der Java-Methode. Die folgende Abbildung veranschaulicht dies:

Using the CREATE FUNCTION Statement to Associate the Handler Method With the UDF Name

Bei der Wahl des UDF-Namens beachten Sie Folgendes:

  • Der Name muss den Regeln für Objektbezeichner folgen.

  • Wählen Sie einen Namen, der eindeutig ist, oder folgen Sie den Regeln für Überladen von UDF-Namen.

    Wichtig

    Im Gegensatz zur Überladung bei SQL-UDFs, die zwischen Funktionen sowohl nach der Anzahl als auch nach den Datentypen der Argumente unterscheiden, unterscheiden Java-UDFs Methoden nur nach der Anzahl der Argumente. Wenn zwei Java-Methoden den gleichen Namen und die gleiche Anzahl von Argumenten, aber unterschiedliche Datentypen haben, dann erzeugt der Aufruf einer UDF mit einer dieser Methoden als Handler einen Fehler, der in etwa wie folgt lautet:

    Kann nicht bestimmen, welche Implementierung von Handler „Handlername“ aufgerufen werden soll, da es mehrere Definitionen mit <Anzahl der Argumente> Argumenten in Funktion <Name der UDF> mit Handler <Klassenname>.<Handlername> gibt.

    Wenn ein Warehouse vorhanden ist, wird der Fehler zum Zeitpunkt der Erstellung der UDF erkannt. Andernfalls tritt der Fehler beim Aufruf der UDF auf.

    Das Auflösen auf Basis von Datentypen ist unpraktisch, da einige SQL-Datentypen mehr als einem Java-Datentyp und damit potenziell mehr als einer Java-UDF-Signatur zugeordnet werden können.

Methodenargumente werden über die Position gebunden, nicht über den Namen. Das erste Argument, das an die UDF übergeben wird, ist das erste Argument, das von der Java-Methode empfangen wird. In den folgenden Codebeispielen entspricht das SQL-UDF-Argument a dem Java-Methodenargument x, und b entspricht y:

create function my_udf(x numeric(9, 0), y float) ...
public static int my_udf(int a, float b) ...

Weitere Informationen zu den Datentypen von Argumenten finden Sie unter Zuordnung von SQL-Java-Datentypen für Parameter und Rückgabetypen.

Inline-UDFs vs. vorkompiliert UDFs

Der Code für einen Java-UDF kann auf eine der folgenden Arten angegeben werden:

  • Vorkompiliert: Die CREATE FUNCTION-Anweisung gibt den Speicherort einer vorhandenen JAR-Datei an. Der Benutzer muss den Java-Quellcode kompilieren und die JAR-Datei in einem Stagingbereich ablegen.

  • Inline: Die CREATE FUNCTION-Anweisung gibt den Java-Quellcode an. Snowflake kompiliert den Quellcode und speichert den kompilierten Code in einer JAR-Datei. Der Benutzer hat die Möglichkeit, einen Speicherort für die JAR-Datei anzugeben.

    • Wenn der Benutzer einen Speicherort für die JAR-Datei angibt, kompiliert Snowflake den Code einmal und behält die JAR-Datei für die zukünftige Verwendung.

    • Wenn der Benutzer keinen Speicherort für die JAR-Datei angibt, führt Snowflake eine erneute Kompilierung des Codes für jede SQL-Anweisung durch, die die UDF-Anweisung aufruft, und Snowflake bereinigt die JAR-Datei automatisch, nachdem die SQL-Anweisung beendet ist.

Es gibt einige praktische Unterschiede, die Ihre Wahl zwischen beiden Varianten beeinflussen können:

  • Inline-Java-UDFs haben folgende Vorteile:

    • Sie sind in der Regel viel einfacher zu implementieren. Die JAR-Datei muss nicht kompiliert und in einen Snowflake-Stagingbereich kopiert werden. (Beachten Sie jedoch, dass die meisten Programmierer ihren Code kompilieren und testen, bevor sie ihn in der Produktion einsetzen, sodass der meiste Java-UDF-Code irgendwann vom Entwickler kompiliert wird, auch wenn die UDF inline ist).

  • Vorkompilierte Java-UDFs haben die folgenden Vorteile:

    • Sie können sie verwenden, wenn Sie eine JAR-Datei, aber keinen Quellcode haben.

    • Sie können sie verwenden, wenn der Quellcode zu groß ist, um ihn in eine CREATE FUNCTION-Anweisung einzufügen. (Inline-Java-UDFs haben eine Obergrenze für die Quellcodegröße.)

    • Eine vorkompilierte Java-UDF kann mehrere aufrufbare Funktionen enthalten. Mehrere CREATE FUNCTION-Anweisungen können auf dieselbe JAR-Datei verweisen, geben aber unterschiedliche Handlerfunktionen innerhalb der JAR-Datei an.

      Inline-Java-UDFs enthalten normalerweise nur eine aufrufbare Funktion. (Diese aufrufbare Funktion kann andere Funktionen aufrufen, und diese anderen Funktionen können in derselben Klasse oder in anderen in den JAR-Bibliotheksdateien definierten Klassen definiert sein).

    • Wenn Sie über Tools oder eine Umgebung zum Testen oder Debuggen von JAR-Dateien verfügen, kann es bequemer sein, den größten Teil der Entwicklungsarbeit an Ihrer UDF mit JAR-Dateien zu erledigen. Dies gilt insbesondere, wenn der Code groß oder komplex ist.

Erstellen einer Inline-Java-UDF

Bei einer Inline-UDF stellen Sie den Java-Quellcode als Teil der CREATE FUNCTION-Anweisung bereit. Beispiel:

create function add(x integer, y integer)
returns integer
language java
handler='TestAddFunc.add'
target_path='@~/TestAddFunc.jar'
as
$$
    class TestAddFunc {
        public static int add(int x, int y) {
          return x + y;
        }
    }
$$;

Der Java-Quellcode wird in der AS-Klausel angegeben. Der Quellcode kann entweder in einfache Anführungszeichen oder in doppelte Dollarzeichen ($$) eingeschlossen sein. Die Verwendung der doppelten Dollarzeichen ist normalerweise einfacher, wenn der Quellcode eingebettete einfache Anführungszeichen enthält.

Der Java-Quellcode kann mehr als eine Klasse und mehr als eine Methode in einer Klasse enthalten. Daher werden Klasse und Methode für den Aufruf in der HANDLER-Klausel angegeben.

Eine Inline-Java-UDF (wie auch eine vorkompilierte Java-UDF) kann Code in JAR-Dateien aufrufen, die in der IMPORTS-Klausel enthalten sind.

Weitere Informationen zur CREATE FUNCTION-Anweisung finden Sie unter CREATE FUNCTION.

Weitere Beispiele finden Sie unter Beispiele für Inline-Java-UDFs.

Erstellen einer vorkompilierten Java-UDF

Organisieren Ihrer Dateien

Ein Java-UDF wird in einer JAR-Datei gespeichert. Wenn Sie vorhaben, den Java-Code zu kompilieren, um die JAR-Datei selbst zu erstellen, können Sie die Dateien wie unten gezeigt organisieren. Dieses Beispiel geht davon aus, dass Sie den Package-Mechanismus von Java verwenden möchten.

  • Entwicklungsverzeichnis

    • Paketverzeichnis

      • Klassendatei1.java

      • Klassendatei2.java

    • classDirectory

      • Klassendatei1.class

      • Klassendatei2.class

    • Manifestdatei.manifest (optional)

    • jar_Datei.jar

    • put_Befehl.sql

Entwicklungsverzeichnis

Dieses Verzeichnis enthält die projektspezifischen Dateien, die zum Erstellen Ihrer Java-UDF erforderlich sind.

Paketverzeichnis

Dieses Verzeichnis enthält die .java-Dateien, die kompiliert und in das Paket aufgenommen werden sollen.

Klassendatei#.java

Diese Dateien enthalten den Java-Quellcode der UDF.

Klassendatei#.class

Dies sind die .class-Dateien, die durch Kompilieren der .java-Dateien erstellt werden.

Manifestdatei.manifest

Die optionale Manifestdatei, die beim Kombinieren der .class-Dateien (und optional der Abhängigkeits-JAR-Dateien) in der JAR-Datei verwendet werden.

jar_Datei.jar

Die JAR-Datei, die den UDF-Code enthält.

put_Befehl.sql

Diese Datei enthält den SQL-Befehl PUT zum Kopieren der JAR-Datei in einen Snowflake-Stagingbereich.

Kompilieren des Java-Codes und Erstellen der JAR-Datei

So erstellen Sie eine JAR-Datei, die den kompilierten Java-Code enthält:

  • Verwenden Sie javac, um Ihre .java-Datei in eine .class-Datei zu kompilieren.

    Wenn Sie einen Compiler neuer als Version 11.x verwenden, können Sie mit der Option „–release“ angeben, dass die Zielversion Version 11 ist.

  • Legen Sie Ihre .class-Datei in eine JAR-Datei. Sie können mehrere Klassendateien (und andere JAR-Dateien) in Ihre JAR-Datei packen.

    Beispiel:

    jar cf ./my_decrement_udf_jar.jar my_decrement_udf_package/my_decrement_udf_class.class
    

    Eine Manifestdatei ist erforderlich, wenn Sie ein Paket verwenden. Wenn Sie kein Paket verwenden, ist die Manifestdatei optional. Im folgenden Beispiel wird eine Manifestdatei verwendet:

    jar cmf my_decrement_udf_manifest.manifest ./my_decrement_udf_jar.jar my_decrement_udf_package/my_decrement_udf_class.class
    

    Um die JAR-Datei mit allen enthaltenen Abhängigkeiten zu erstellen, können Sie den Maven-Befehl mvn package mit dem maven-assembly-plugin verwenden. Weitere Informationen zum maven-assembly-plugin finden Sie unter:

    Snowflake stellt automatisch die Standard-Java-Bibliotheken (z. B. java.util) bereit. Wenn Ihr Code diese Bibliotheken aufruft, müssen Sie sie nicht in Ihre JAR-Datei aufnehmen.

    Die Methoden, die Sie in Bibliotheken aufrufen, müssen denselben Snowflake-bedingten Einschränkungen folgen wie Ihre Java-Methode.

Kopieren der JAR-Datei in Ihren Stagingbereich

Da Snowflake die JAR-Datei aus einem externen oder benannten internen Stagingbereich liest, müssen Sie Ihre JAR-Datei in einen Stagingbereich kopieren. Sie können den Standard-Stagingbereich Ihres Snowflake-Benutzers oder einen anderen vorhandene Stagingbereich verwenden, oder Sie können einen neuen Stagingbereich erstellen.

Der Stagingbereich, der die JAR-Datei hostet, muss für den Eigentümer der UDF-Datei lesbar sein.

Normalerweise verwenden Sie einen benannten internen Stagingbereich und laden die Datei mit dem Befehl PUT in den Stagingbereich hoch. (Beachten Sie, dass der PUT-Befehl nicht über die Snowflake-GUI ausgeführt werden kann. Sie können zur Ausführung von PUT SnowSQL verwenden.)

Wenn Sie die JAR-Datei löschen oder umbenennen, können Sie die UDF-Datei nicht mehr aufrufen.

Snowflake empfiehlt die folgenden Best Practices:

  • Wenn Sie Ihre JAR-Datei aktualisieren müssen, gehen Sie wie folgt vor:

    • Aktualisieren Sie die Datei, wenn keine Aufrufe an die UDF erfolgen können.

    • Wenn sich die alte .jar-Datei noch im Stagingbereich befindet, muss der PUT-Befehl die OVERWRITE=TRUE-Klausel enthalten.

Im Abschnitt Beispiele finden Sie ein Beispiel für die Verwendung des PUT-Befehls zum Kopieren einer .jar-Datei in einen Stagingbereich.

Erteilen von Berechtigungen für die Funktion

Damit eine andere Rolle als der Funktionseigentümer die Funktion aufrufen kann, muss der Eigentümer der Rolle entsprechende Berechtigungen erteilen.

Die GRANT-Anweisungen für ein Java-UDF sind im Wesentlichen identisch mit den GRANT-Anweisungen für andere UDFs, wie zum Beispiel JavaScript-UDFs.

Beispiel:

GRANT USAGE ON FUNCTION my_java_udf(number, number) TO my_role;

Beispiele

Inline-Java-UDFs

Erstellen und Aufrufen einer einfachen Inline-Java-UDF

Mit den folgenden Anweisungen wird eine Inline-Java-UDF erstellt und aufgerufen. Dieser Code gibt einfach den VARCHAR-Wert zurück, der übergeben wurde.

Diese Funktion wird mit der optionalen CALLED ON NULL INPUT-Klausel deklariert, um anzugeben, dass die Funktion auch dann aufgerufen wird, wenn der Eingabewert NULL ist. (Diese Funktion würde mit und ohne diese Klausel NULL zurückgeben. Sie könnten den Code aber ändern, um NULL anders zu behandeln, z. B. um eine leere Zeichenfolge zurückzugeben).

Erstellen der UDF:

create or replace function echo_varchar(x varchar)
returns varchar
language java
called on null input
handler='TestFunc.echo_varchar'
target_path='@~/testfunc.jar'
as
'class TestFunc {
  public static String echo_varchar(String x) {
    return x;
  }
}';

Aufrufen der UDF:

SELECT echo_varchar('Hello');
+-----------------------+
| ECHO_VARCHAR('HELLO') |
|-----------------------|
| Hello                 |
+-----------------------+

Übergeben von NULL an eine Inline-Java-UDF

Hier wird die oben definierte echo_varchar()-UDF verwendet. Der SQL NULL-Wert wird implizit in einen Java null-Wert umgewandelt, und dieser Java null-Wert wird zurückgegeben und implizit wieder in SQL-NULL umgewandelt:

Aufrufen der UDF:

SELECT echo_varchar(NULL);
+--------------------+
| ECHO_VARCHAR(NULL) |
|--------------------|
| NULL               |
+--------------------+

Explizite Rückgabe von NULL

Das folgende Codebeispiel zeigt, wie Sie einen NULL-Wert explizit zurückgeben. Der Java null-Wert wird in einen SQL NULL-Wert umgewandelt.

Erstellen der UDF:

create or replace function return_a_null()
returns varchar
null
language java
handler='TemporaryTestLibrary.return_a_null'
target_path='@~/TemporaryTestLibrary.jar'
as
$$
class TemporaryTestLibrary {
  public static String return_a_null() {
    return null;
  }
}
$$;

Aufrufen der UDF:

SELECT return_a_null();
+-----------------+
| RETURN_A_NULL() |
|-----------------|
| NULL            |
+-----------------+

Übergeben eines OBJECT-Werts an eine Inline-Java-UDF

Im folgenden Beispiel werden der Datentyp SQL OBJECT und der entsprechende Java-Datentyp (Map<String, String>) verwendet, um einen Wert aus OBJECT zu extrahieren. In diesem Beispiel wird auch gezeigt, dass Sie mehrere Parameter an eine Java-UDF übergeben können.

Erstellen und laden Sie eine Tabelle, die eine Spalte vom Typ OBJECT enthält:

CREATE TABLE objectives (o OBJECT);
INSERT INTO objectives SELECT PARSE_JSON('{"outer_key" : {"inner_key" : "inner_value"} }');

Erstellen der UDF:

create or replace function extract_from_object(x OBJECT, key VARCHAR)
returns variant
language java
handler='VariantLibrary.extract'
target_path='@~/VariantLibrary.jar'
as
$$
import java.util.Map;
class VariantLibrary {
  public static String extract(Map<String, String> m, String key) {
    return m.get(key);
  }
}
$$;

Aufrufen der UDF:

SELECT extract_from_object(o, 'outer_key'), 
       extract_from_object(o, 'outer_key')['inner_key'] FROM objectives;
+-------------------------------------+--------------------------------------------------+
| EXTRACT_FROM_OBJECT(O, 'OUTER_KEY') | EXTRACT_FROM_OBJECT(O, 'OUTER_KEY')['INNER_KEY'] |
|-------------------------------------+--------------------------------------------------|
| {                                   | "inner_value"                                    |
|   "inner_key": "inner_value"        |                                                  |
| }                                   |                                                  |
+-------------------------------------+--------------------------------------------------+

Übergeben eines ARRAY-Werts an eine Inline-Java-UDF

Im folgenden Beispiel wird der SQL-Datentyp ARRAY verwendet.

Erstellen der UDF:

create or replace function multiple_functions_in_jar(array1 array)
returns varchar
language java
handler='TemporaryTestLibrary.multiple_functions_in_jar'
target_path='@~/TemporaryTestLibrary.jar'
as
$$
class TemporaryTestLibrary {
  public static String multiple_functions_in_jar(String[] array_of_strings) {
    return concatenate(array_of_strings);
  }
  public static String concatenate(String[] array_of_strings) {
    int number_of_strings = array_of_strings.length;
    String concatenated = "";
    for (int i = 0; i < number_of_strings; i++)  {
        concatenated = concatenated + " " + array_of_strings[i];
        }
    return concatenated;
  }
}
$$;

Aufrufen der UDF:

SELECT multiple_functions_in_jar(ARRAY_CONSTRUCT('Hello', 'world'));
+--------------------------------------------------------------+
| MULTIPLE_FUNCTIONS_IN_JAR(ARRAY_CONSTRUCT('HELLO', 'WORLD')) |
|--------------------------------------------------------------|
|  Hello world                                                 |
+--------------------------------------------------------------+

Vorkompilierte Java-UDFs

Erstellen und Aufrufen einer einfachen vorkompilierten Java-UDF

Mit den folgenden Anweisungen wird eine einfache Java-UDF erstellt. Dieses Beispiel folgt im Allgemeinen der unter Organisieren Ihrer Dateien beschriebenen Datei- und Verzeichnisstruktur.

Erstellen Sie eine Java-Datei, die Ihren Quellcode enthält:

package my_decrement_udf_package;


public class my_decrement_udf_class
{

public static int my_decrement_udf_method(int i)
{
    return i - 1;
}


public static void main(String[] argv)
{
    System.out.println("This main() function won't be called.");
}


}

Erstellen Sie optional eine Manifestdatei ähnlich der unten gezeigten:

Manifest-Version: 1.0
Main-Class: my_decrement_udf_class.class

Kompilieren Sie den Quellcode. In diesem Beispiel werden die generierten .class-Dateien im Verzeichnis classDirectory gespeichert.

javac -d classDirectory my_decrement_udf_package/my_decrement_udf_class.java

Erstellen Sie aus der .class-Datei eine JAR-Datei. In diesem Beispiel wird „-C classDirectory“ verwendet, um den Speicherort der .class-Dateien anzugeben:

jar cmf my_decrement_udf_manifest.manifest ./my_decrement_udf_jar.jar -C classDirectory my_decrement_udf_package/my_decrement_udf_class.class

Verwenden Sie den PUT-Befehl, um die JAR-Datei aus dem lokalen Dateisystem in einen Stagingbereich zu kopieren. Im folgenden Beispiel wird der Standard-Stagingbereich des Benutzers mit dem Namen @~ verwendet:

put
    file:///Users/Me/JavaUDFExperiments/my_decrement_udf_jar.jar
    @~/my_decrement_udf_package_dir/
    auto_compress = false
    overwrite = true
    ;

Sie können den PUT-Befehl in einer Skriptdatei speichern und diese Datei dann über snowsql ausführen. Der snowsql-Befehl sieht ungefähr wie folgt aus:

snowsql -a <account_identifier> -w <warehouse> -d <database> -s <schema> -u <user> -f put_command.sql

In diesem Beispiel wird davon ausgegangen, dass das Kennwort des Benutzers in der Umgebungsvariablen SNOWSQL_PWD angegeben ist.

Erstellen der UDF:

create function my_decrement_udf(i numeric(9, 0))
    returns numeric
    language java
    imports = ('@~/my_decrement_udf_package_dir/my_decrement_udf_jar.jar')
    handler = 'my_decrement_udf_package.my_decrement_udf_class.my_decrement_udf_method'
    ;

Aufrufen der UDF:

SELECT my_decrement_udf(-15);
+-----------------------+
| MY_DECREMENT_UDF(-15) |
|-----------------------|
|                   -16 |
+-----------------------+