Entwerfen von Java-UDFs

Dieses Thema bietet Unterstützung beim Entwerfen von Java-UDFs.

Unter diesem Thema:

Auswählen Ihrer Datentypen

Bevor Sie Ihren Code schreiben:

  • Wählen Sie die Datentypen aus, die Ihre Funktion als Argumente akzeptieren soll, und den Datentyp, den Ihre Funktion zurückgeben soll.

  • Berücksichtigen Sie die Zeitzonenproblematik.

  • Entscheiden Sie, wie NULL-Werte behandelt werden sollen.

Zuordnung von SQL-Java-Datentypen für Parameter und Rückgabetypen

Informationen dazu, wie Snowflake zwischen Java- und SQL-Datentypen konvertiert, finden Sie unter Zuordnung von Datentypen zwischen SQL und Handler-Sprachen.

TIMESTAMP_LTZ-Werte und Zeitzonen

Ein Java-UDF ist weitgehend isoliert von der Umgebung, in der sie aufgerufen wird. Die Zeitzone wird jedoch von der aufrufenden Umgebung geerbt. Wenn die Sitzung des Aufrufers vor dem Aufruf der Java-UDF eine Standardzeitzone eingestellt hat, dann hat die Java-UDF die gleiche Standardzeitzone. Die Java-UDF verwendet dieselben Daten der IANA-Zeitzonendatenbank, die auch von der nativen TIMEZONE-Funktion in Snowflake SQL verwendet wird (d. h. Daten aus Release 2021a der Zeitzonendatenbank).

NULL-Werte

Snowflake unterstützt zwei verschiedene NULL-Werte: SQL NULL und VARIANT JSON null. (Weitere Informationen zu Snowflake VARIANT NULL finden Sie unter NULL-Werte.)

Java unterstützt einen null-Wert, der nur für nicht primitive Datentypen gilt.

Ein SQL-NULL-Argument für eine Java-UDF wird in den Java null-Wert übersetzt, aber nur für Java-Datentypen, die null unterstützen.

Ein zurückgegebener Java null-Wert wird in SQL NULL zurückübersetzt.

Arrays und variable Anzahl von Argumenten

Java-UDFs können Arrays von jedem der folgenden Java-Datentypen erhalten:

  • String

  • boolean

  • double

  • float

  • int

  • long

  • short

Der Datentyp von übergebenen SQL-Werten muss mit dem entsprechenden Java-Datentyp kompatibel sein. Weitere Informationen zur Datentypkompatibilität finden Sie unter Zuordnung von Datentypen zwischen SQL und Java.

Die folgenden zusätzlichen Regeln gelten für jeden der angegebenen Java-Datentypen:

  • boolean: Das Snowflake-ARRAY darf nur BOOLEAN-Elemente und keine NULL-Werte enthalten.

  • int/short/long: Das Snowflake-ARRAY darf nur Festkomma-Elemente mit einer Skalierung von 0 enthalten und auch keine NULL-Werte.

  • float/double: Das Snowflake-ARRAY muss enthalten:

    Das ARRAY darf keine NULL-Werte enthalten.

Java-Methoden können diese Arrays auf eine von zwei Arten erhalten:

  • Verwendung der Array-Funktion von Java.

  • Verwendung der Java-Funktion varargs (variable Anzahl von Argumenten).

In beiden Fällen muss Ihr SQL-Code ein ARRAY übergeben.

Werteübergabe via ARRAY

Deklarieren Sie den Java-Parameter als Array. In der folgenden Methode ist dritte Parameter beispielsweise ein String-Array:

static int myMethod(int fixedArgument1, int fixedArgument2, String[] stringArray)
Copy

Nachfolgend finden Sie ein vollständiges Beispiel:

Erstellen und Laden der Tabelle:

CREATE TABLE string_array_table(id INTEGER, a ARRAY);
INSERT INTO string_array_table (id, a) SELECT
        1, ARRAY_CONSTRUCT('Hello');
INSERT INTO string_array_table (id, a) SELECT
        2, ARRAY_CONSTRUCT('Hello', 'Jay');
INSERT INTO string_array_table (id, a) SELECT
        3, ARRAY_CONSTRUCT('Hello', 'Jay', 'Smith');
Copy

Erstellen der UDF:

create or replace function concat_varchar_2(a ARRAY)
returns varchar
language java
handler='TestFunc_2.concatVarchar2'
target_path='@~/TestFunc_2.jar'
as
$$
    class TestFunc_2 {
        public static String concatVarchar2(String[] strings) {
            return String.join(" ", strings);
        }
    }
$$;
Copy

Aufrufen der UDF:

SELECT concat_varchar_2(a)
    FROM string_array_table
    ORDER BY id;
+---------------------+
| CONCAT_VARCHAR_2(A) |
|---------------------|
| Hello               |
| Hello Jay           |
| Hello Jay Smith     |
+---------------------+
Copy

Werteübergabe via varargs

Die Verwendung von varargs ist der Verwendung eines Arrays sehr ähnlich.

Verwenden Sie in Ihrem Java-Code den varargs-Deklarationsstil von Java:

static int myMethod(int fixedArgument1, int fixedArgument2, String ... stringArray)
Copy

Nachfolgend finden Sie ein vollständiges Beispiel. Der einzige wesentliche Unterschied zwischen diesem Beispiel und dem vorangegangenen Beispiel (für Arrays) ist die Deklaration der Parameter für die Methode.

Erstellen und Laden der Tabelle:

CREATE TABLE string_array_table(id INTEGER, a ARRAY);
INSERT INTO string_array_table (id, a) SELECT
        1, ARRAY_CONSTRUCT('Hello');
INSERT INTO string_array_table (id, a) SELECT
        2, ARRAY_CONSTRUCT('Hello', 'Jay');
INSERT INTO string_array_table (id, a) SELECT
        3, ARRAY_CONSTRUCT('Hello', 'Jay', 'Smith');
Copy

Erstellen der UDF:

create or replace function concat_varchar(a ARRAY)
returns varchar
language java
handler='TestFunc.concatVarchar'
target_path='@~/TestFunc.jar'
as
$$
    class TestFunc {
        public static String concatVarchar(String ... stringArray) {
            return String.join(" ", stringArray);
        }
    }
$$;
Copy

Aufrufen der UDF:

SELECT concat_varchar(a)
    FROM string_array_table
    ORDER BY id;
+-------------------+
| CONCAT_VARCHAR(A) |
|-------------------|
| Hello             |
| Hello Jay         |
| Hello Jay Smith   |
+-------------------+
Copy

Entwerfen von Java-UDFs unter Berücksichtigung der Snowflake-bedingten Einschränkungen

Informationen zum Entwerfen von Handler-Code, der unter Snowflake effizient ausgeführt wird, finden Sie unter Entwerfen von Handlern unter Berücksichtigung der Snowflake-bedingten Einschränkungen.

Entwerfen der Klasse

Wenn eine SQL-Anweisung Ihre Java-UDF aufruft, ruft Snowflake eine Java-Methode auf, die Sie geschrieben haben. Ihre Java-Methode wird als „Handler-Methode“ oder kurz „Handler“ bezeichnet.

Wie bei jeder Java-Methode muss Ihre Methode als Teil einer Klasse deklariert werden. Ihre Handler-Methode kann eine statische Methode oder eine Instanzenmethode der Klasse sein. Wenn Ihr Handler eine Instanzenmethode ist und Ihre Klasse einen Konstruktor ohne Argumente definiert, ruft Snowflake den Konstruktor zur Initialisierungszeit auf, um eine Instanz Ihrer Klasse zu erstellen. Wenn Ihr Handler eine statische Methode ist, muss Ihre Klasse keinen Konstruktor haben.

Der Handler wird für jede an die Java-UDF übergebene Zeile einmal aufgerufen. (Hinweis: Es wird nicht für jede Zeile eine neue Instanz der Klasse erstellt. Snowflake kann die Handler-Methode derselben Instanz mehrmals aufrufen oder dieselbe statische Methode mehrmals aufrufen).

Um die Ausführung Ihres Codes zu optimieren, geht Snowflake davon aus, dass die Initialisierung langsam sein kann, während die Ausführung der Handler-Methode schnell ist. Snowflake verwendet ein längeres Timeout für die Ausführung der Initialisierung (einschließlich Zeit für Laden der UDF und Aufruf des Konstruktors der Klasse, die die Handler-Methode enthält, falls ein Konstruktor definiert ist) als für die Ausführung des Handlers (Zeit für Aufruf Ihres Handlers mit einer Zeile der Eingabe).

Weitere Informationen zum Entwerfen der Klasse finden Sie unter Erstellen eines Java-UDF-Handlers.

Optimieren von Initialisierung und Steuerung des globalen Zustands in skalaren UDFs

Die meisten Funktions- und Prozedurhandler sollten die folgenden Richtlinien berücksichtigen:

  • Wenn Sie einen gemeinsamen Zustand (Shared State) initialisieren müssen, der sich über Zeilen hinweg nicht ändert, initialisieren Sie ihn nicht in der Handler-Funktion sondern im Modul oder Konstruktor.

  • Schreiben Sie Ihre Handler-Funktion so, dass sie threadsicher ist.

  • Vermeiden Sie das Speichern und Freigeben eines dynamischen Zustands über Zeilen hinweg.

Wenn Ihre UDF diesen Richtlinien nicht folgen kann oder wenn Sie ein tieferes Verständnis der Gründe für diese Richtlinien wünschen, lesen Sie die nächsten Unterabschnitte.

Gemeinsamer Zustand für alle Aufrufe (Shared State)

Snowflake erwartet, dass skalare UDFs unabhängig verarbeitet werden. Wenn Sie sich zwischen Aufrufen auf einen gemeinsamen Zustand verlassen, kann dies zu unerwartetem Verhalten führen. Dies liegt daran, dass das System Zeilen in beliebiger Reihenfolge verarbeiten kann und die Aufrufe auf mehrere JVMs (für in Java oder Scala geschriebene Handler) oder Instanzen (für in Python geschriebene Handler) verteilt werden.

Bei UDFs sollte es vermieden werden, dass sich beim Aufrufen der Handler-Methode auf einen gemeinsamen Zustand verlassen wird. Es gibt jedoch zwei Situationen, in denen Sie möglicherweise möchten, dass eine UDF einen gemeinsamen Zustand speichert:

  • Code, der teure Initialisierungslogik enthält, die nicht bei jeder Zeile wiederholt werden soll.

  • Code, der einen gemeinsamen Zustand über Zeilen hinweg nutzt, z. B. als Cache.

Wenn Sie den Zustand über mehrere Zeilen hinweg gemeinsam nutzen müssen und sich dieser Zustand im Laufe der Zeit nicht ändert, dann verwenden Sie einen Konstruktor, um den gemeinsamen Zustand durch das Einstellen von Variablen auf Instanzenebene zu erstellen. Der Konstruktor wird nur einmal pro Instanz ausgeführt, während der Handler einmal pro Zeile aufgerufen wird, sodass die Initialisierung im Konstruktor günstiger ist, wenn ein Handler mehrere Zeilen verarbeitet. Und da der Konstruktor nur einmal aufgerufen wird, muss er nicht thread-sicher geschrieben werden.

Wenn Ihre UDF einen gemeinsamen Zustand speichert, der sich ändert, muss Ihr Code darauf vorbereitet sein, den gleichzeitigen Zugriff mit diesem Zustand zu verarbeiten. In den nächsten beiden Abschnitten finden Sie weitere Informationen zur Parallelität und zum gemeinsamen Zustand.

Erläuterungen zur Java-UDF-Parallelisierung

Um die Verarbeitungsleistung zu verbessern, nutzt Snowflake die Parallelisierung sowohl zwischen mehrere JVMs als auch innerhalb von JVMs.

  • Mehrere JVMs:

    Snowflake parallelisiert mehrere Ausführungen in einem Warehouse. Jede Ausführung nutzt eine (oder mehrere) JVMs. Dies bedeutet, dass es keinen globalen gemeinsamen Zustand gibt. Der Zustand kann höchstens innerhalb einer einzelnen JVM gemeinsam genutzt werden.

  • Innerhalb von JVMs:

    • Jede JVM kann mehrere Threads ausführen, die parallel die Handler-Methode der gleichen Instanz aufrufen können. Das bedeutet, dass jede Handler-Methode thread-sicher sein muss.

    • Wenn eine UDF mit IMMUTABLE eingerichtet ist und eine SQL-Anweisung diese UDF mehr als einmal mit denselben Argumenten für dieselbe Zeile aufruft, dann gibt die UDF bei jedem Aufruf für diese Zeile den gleichen Wert zurück. Im Folgenden wird z. B. zweimal derselbe Wert für jede Zeile zurückgegeben, wenn die UDF mit IMMUTABLE eingerichtet ist:

      select
             my_java_udf(42),
             my_java_udf(42)
          from table1;
      
      Copy

      Wenn Sie möchten, dass mehrere Aufrufe unabhängige Werte zurückgeben, auch wenn die gleichen Argumente übergeben werden, und wenn Sie nicht die Funktion VOLATILE deklarieren möchten, dann binden Sie mehrere separate UDFs an die gleiche Handler-Methode. Beispiel:

      1. Erstellen Sie eine JAR-Datei mit dem Namen @java_udf_stage/rand.jar mit Code:

        class MyClass {
        
            private double x;
        
            // Constructor
            public MyClass()  {
                x = Math.random();
            }
        
            // Handler
            public double myHandler() {
                return x;
            }
        }
        
        Copy
      2. Erstellen Sie die Java-UDFs wie unten gezeigt. Diese UDFs haben unterschiedliche Namen, verwenden aber die gleiche JAR-Datei und innerhalb dieser JAR-Datei den gleichen Handler.

        create function my_java_udf_1()
            returns double
            language java
            imports = ('@java_udf_stage/rand.jar')
            handler = 'MyClass.myHandler';
        
        create function my_java_udf_2()
            returns double
            language java
            imports = ('@java_udf_stage/rand.jar')
            handler = 'MyClass.myHandler';
        
        Copy
      3. Der folgende Code ruft beide UDFs auf. Die UDFs verweisen auf die gleiche JAR-Datei und den gleichen Handler. Diese Aufrufe erstellen zwei Instanzen der gleichen Klasse. Jede Instanz gibt einen unabhängigen Wert zurück, sodass das folgende Beispiel zwei unabhängige Werte zurückgibt, anstatt zweimal denselben Wert zu liefern:

        select
                my_java_udf_1(),
                my_java_udf_2()
            from table1;
        
        Copy

Speichern von JVM-Statusinformationen

Ein Grund, sich nicht auf einen dynamischen gemeinsamen Zustand zu verlassen, besteht darin, dass Zeilen nicht unbedingt in einer vorhersehbaren Reihenfolge verarbeitet werden. Bei jeder Ausführung einer SQL-Anweisung kann Snowflake die Anzahl der Batches, die Reihenfolge, in der die Batches verarbeitet werden, und die Reihenfolge der Zeilen innerhalb eines Batches variieren. Wenn eine skalare UDF so gestaltet ist, dass eine Zeile den Rückgabewert einer nachfolgenden Zeile beeinflusst, dann kann die UDF bei jeder Ausführung unterschiedliche Ergebnisse zurückgeben.

Fehlerbehandlung

Eine als UDF verwendete Java-Methode kann die üblichen Java-Techniken zur Ausnahmebehandlung verwenden, um Fehler innerhalb der Methode abzufangen.

Wenn eine Ausnahme innerhalb der Methode auftritt und nicht von der Methode abgefangen wird, gibt Snowflake einen Fehler aus, der den Stacktrace für die Ausnahme enthält. Wenn die Protokollierung von unbehandelten Ausnahmen aktiviert ist, protokolliert Snowflake Daten zu unbehandelten Ausnahmen in einer Ereignistabelle.

Sie können explizit einen Fehler auszulösen, ohne ihn abzufangen, um die Abfrage zu beenden und einen SQL-Fehler zu erzeugen. Beispiel:

if (x < 0)  {
    throw new IllegalArgumentException("x must be non-negative.");
    }
Copy

Beim Debugging können Sie in den Text der SQL-Fehlermeldung Werte integrieren. Setzen Sie dazu den gesamten Textkörper der Java-Methode in einen „try-catch“-Block, und fügen Sie Argumentwerte zur abgefangenen Fehlermeldung hinzu. Lösen Sie dann einen Fehler mit der erweiterten Meldung aus. Um die Offenlegung sensibler Daten zu verhindern, entfernen Sie die Argumente, bevor Sie die JAR-Dateien in einer Produktionsumgebung bereitstellen.

Befolgen von Best Practices

  • Schreiben Sie plattformunabhängigen Code.

    • Vermeiden Sie Code, der eine bestimmte CPU-Architektur voraussetzt (z. B. x86).

    • Vermeiden Sie Code, der ein bestimmtes Betriebssystem voraussetzt.

  • Wenn Sie Initialisierungscode ausführen müssen und diesen nicht in die aufgerufene Methode aufnehmen möchten, können Sie den Initialisierungscode in einen statischen Initialisierungsblock einfügen.

Siehe auch:

Einsetzen von bewährten Sicherheitsmethoden

Damit Ihr Handler auf sichere Weise funktioniert, beachten Sie die unter Sicherheitsverfahren für UDFs und Prozeduren beschriebenen Best Practices.