Conception d’UDFs Java

Ce sujet vous aide à concevoir des UDFs Java.

Dans ce chapitre :

Choisir vos types de données

Avant d’écrire votre code :

  • Choisissez les types de données que votre fonction doit accepter comme arguments et le type de données que votre fonction doit retourner.

  • Tenez compte des problèmes liés aux fuseaux horaires.

  • Décidez comment traiter les valeurs NULL.

Mappages de type de donnée SQL-Java pour les paramètres et les types de retour

Le tableau ci-dessous montre les mappages de types entre SQL et Java. Ces mappages s’appliquent généralement à la fois aux arguments transmis aux UDF de Java et aux valeurs renvoyées par les UDF. Il existe toutefois quelques exceptions, qui sont énumérées dans les notes de bas de page.

Certains types de données SQL (par exemple, NUMBER) sont compatibles avec plusieurs types de données Java (par exemple int, long, etc.). Dans ces cas, vous pouvez utiliser n’importe quel type de données Java ayant une capacité suffisante pour contenir les valeurs réelles qui seront transmises. Si vous transmettez une valeur SQL à un type de données Java incompatible (ou vice versa), Snowflake émet une erreur.

Type SQL

Type Java

Remarques

NUMBER

court

Ne peut être nul. Doit se situer dans l’intervalle des valeurs courtes (pas de partie fractionnaire, et la partie entière ne peut pas dépasser les valeurs courtes max/min).

NUMBER

Court

Doit se situer dans l’intervalle des valeurs courtes (pas de partie fractionnaire, et la partie entière ne peut pas dépasser les valeurs courtes max/min).

NUMBER

int

Ne peut être nul. Doit être compris dans l’intervalle des int (pas de partie fractionnaire, et la partie entière ne peut pas dépasser les valeurs int max/min).

NUMBER

Entier

Doit être compris dans l’intervalle des int (pas de partie fractionnaire, et la partie entière ne peut pas dépasser les valeurs int max/min).

NUMBER

long

Ne peut être nul. Doit être compris dans l’intervalle des valeurs longues (pas de partie fractionnaire, et la partie entière ne peut pas dépasser les valeurs longues max/min).

NUMBER

Long

Doit être compris dans l’intervalle des valeurs longues (pas de partie fractionnaire, et la partie entière ne peut pas dépasser les valeurs longues max/min).

NUMBER

java.math.BigDecimal

NUMBER

java.math.BigInteger

Doit être compris dans l’intervalle de BigInteger (pas de partie fractionnaire).

NUMBER

Chaîne

FLOAT

double

Ne peut être nul.

FLOAT

Double

FLOAT

float

Ne peut être nul. Peut entraîner une perte de précision.

FLOAT

Float

Peut entraîner une perte de précision.

FLOAT

Chaîne

Peut entraîner une perte de précision (la conversion float-> chaîne est avec perte).

VARCHAR

Chaîne

BINARY

octet[]

BINARY

Chaîne

Encode la chaîne binaire en hexadécimal. 4

BOOLEAN

booléen

Ne peut être nul.

BOOLEAN

Booléen

BOOLEAN

Chaîne

4

DATE

java.sql.Date

DATE

Chaîne

Formate la date sous la forme YYYY-MM-DD. 4

TIME

java.sql.Time

3

TIME

Chaîne

Formate l’heure sous la forme HH:MI:SS.SSSSSSSSS où la partie en fraction de secondes dépend de la précision de l’heure. 3

TIMESTAMP_LTZ

java.sql.Timestamp

Doit être compris dans l’intervalle de java.sql.Timestamp. 3

TIMESTAMP_LTZ

Chaîne

Le format de sortie est DY, DD MON YYYY HH24:MI:SS TZHTZM. 1 , 3 , 4

TIMESTAMP_NTZ

java.sql.Timestamp

Doit être compris dans l’intervalle de java.sql.Timestamp. Traite l’heure de l’horloge murale comme un décalage par rapport à l’époque Unix (imposant un fuseau horaire UTC, en fait). 3

TIMESTAMP_NTZ

Chaîne

Traite l’heure de l’horloge murale comme un décalage par rapport à l’époque Unix (imposant un fuseau horaire UTC, en fait). Le format de sortie est DY, DD MON YYYY HH:MI:SS. 2 , 3 , 4

TIMESTAMP_TZ

java.sql.Timestamp

Doit être compris dans l’intervalle de java.sql.Timestamp. 3

TIMESTAMP_TZ

Chaîne

Le format de sortie est DY, DD MON YYYY HH24:MI:SS TZHTZM. 1 , 3 , 4

VARIANT

Chaîne

Formate la valeur en fonction du type qui est représenté. Variante null est formatée comme la chaîne « null ».

OBJECT

Carte<Chaîne, Chaîne>

Les clés de la carte sont les clés de l’objet, et les valeurs sont formatées comme des chaînes de caractères.

OBJECT

Chaîne

Formate l’objet sous la forme d’une chaîne JSON (par exemple {"x": 3, "y": true}).

ARRAY

Chaîne[]

Formate les éléments du tableau sous forme de chaînes de caractères.

ARRAY

Chaîne

Formate le tableau comme une chaîne JSON (par exemple [1, "foo", null]).

GEOGRAPHY

Chaîne

Formate la géographie comme GeoJSON .

1(1,2)

Le format correspond au format d’horodatage DY, DD MON YYYY HH24:MI:SS TZHTZM d’Internet (RFC) tel que décrit dans Formats d’horodatage. Si un décalage de fuseau horaire (le composant TZHTZM) est présent, il s’agit généralement de chiffres (par exemple, -0700 indique 7 heures de retard sur UTC). Si le décalage du fuseau horaire est Z (pour « Zulu ») plutôt que des chiffres, c’est un synonyme de « +0000 » (UTC).

2

Le format correspond au format d’horodatage DY, DD MON YYYY HH24:MI:SS d’Internet (RFC) tel que décrit dans Formats d’horodatage. Si la chaîne est suivie d’un espace et de Z (pour « Zulu »), cela indique explicitement que le décalage est « +0000 ». (UTC).

3(1,2,3,4,5,6,7,8)

Bien que Snowflake puisse stocker des valeurs temporelles avec une précision de l’ordre de la nanoseconde, la bibliothèque java.sql.time n’offre qu’une précision de l’ordre de la milliseconde. La conversion entre les types de données Snowflake et Java peut réduire la précision effective à des millisecondes.

4(1,2,3,4,5,6)

Ce mappage de type est pris en charge lors de la conversion des arguments SQL au format Java, mais pas lors de la conversion des types de retour Java en types SQL.

Valeurs et fuseaux horaires TIMESTAMP_LTZ

Une UDF Java est largement isolée de l’environnement dans lequel elle est appelée. Cependant, le fuseau horaire est hérité de l’environnement d’appel. Si la session de l’appelant a défini un fuseau horaire par défaut avant d’appeler l’UDF Java , alors l’UDF Java a le même fuseau horaire par défaut.

Valeurs NULL

Snowflake prend en charge deux valeurs NULL distinctes : SQL NULL et JSON null de VARIANT. (Pour des informations sur les données VARIANT NULL Snowflake, voir Valeurs NULL.)

Java prend en charge une seule valeur null, qui ne concerne que les types de données non primitifs.

Un argument SQL NULL à une UDF Java se traduit par la valeur null Java, mais seulement pour les types de données Java qui prennent en charge null.

Une valeur Java null renvoyée est reconvertie au format SQL NULL.

Tableaux et nombre variable d’arguments

Les UDFs Java peuvent recevoir des tableaux de l’un des types de données Java suivants :

  • Chaîne

  • booléen

  • double

  • float

  • int

  • long

  • court

Le type de données des valeurs SQL transmises doit être compatible avec le type de données Java correspondant. Pour plus de détails sur la compatibilité des types de données, voir Mappages de type de donnée SQL-Java pour les paramètres et les types de retour.

Les règles supplémentaires suivantes s’appliquent à chacun des types de données Java spécifiés :

  • booléen : L’ARRAY Snowflake ne doit contenir que des éléments BOOLEAN, et ne doit pas contenir de valeurs NULL.

  • int/short/long : L’ARRAY Snowflake ne doit contenir que des éléments à point fixe avec une échelle de 0, et ne doit pas contenir de valeurs NULL.

  • float/double : L’ARRAY Snowflake doit contenir soit :

    Les ARRAY ne doivent pas contenir de valeurs NULL.

Les méthodes Java peuvent recevoir ces tableaux de deux manières différentes :

  • En utilisant la fonctionnalité de tableau Java.

  • En utilisant la fonctionnalité varargs (nombre variable d’arguments) de Java.

Dans les deux cas, votre code SQL doit transmettre un ARRAY.

Transmission par un ARRAY

Déclarez le paramètre Java comme un tableau. Par exemple, le troisième paramètre de la méthode suivante est un tableau Chaîne :

static int MyMethod(int fixed_argument_1, int fixed_argument_2, String[] string_array)

Vous trouverez ci-dessous un exemple complet :

Créer et charger la table :

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');

Créez l’UDF :

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

Appelez l’UDF :

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

Transmission par des Varargs

L’utilisation de varargs est très similaire à celle d’un tableau.

Dans votre code Java, utilisez le style de déclaration varargs de Java :

static int MyMethod(int fixed_argument_1, int fixed_argument_2, String ... string_array)

Vous trouverez ci-dessous un exemple complet. La seule différence significative entre cet exemple et l’exemple précédent (pour les tableaux) est la déclaration des paramètres de la méthode.

Créer et charger la table :

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');

Créez l’UDF :

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

Appelez l’UDF :

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

Concevoir des UDFs Java qui restent dans les limites des contraintes imposées par Snowflake

Pour assurer la stabilité dans l’environnement Snowflake, Snowflake impose les contraintes suivantes aux UDFs Java. Sauf indication contraire, ces limitations sont appliquées lorsque l’UDF est exécutée, et non lors de sa création.

Mémoire

Évitez de consommer trop de mémoire.

  • Les grandes valeurs de données (généralement BINARY, longues VARCHAR, ou grands types de données OBJECT ou VARIANT) peuvent consommer une grande quantité de mémoire.

  • Une profondeur de pile excessive peut consommer une grande quantité de mémoire. Snowflake a testé des appels de fonction simples imbriqués dans 50 niveaux de profondeur sans erreur. La limite maximale pratique dépend de la quantité d’informations placées sur la pile.

Les UDFs renvoient une erreur si elles consomment trop de mémoire. La limite spécifique est sujette à changement.

Durée

Évitez les algorithmes qui prennent beaucoup de temps par appel.

Si une UDF prend trop de temps pour se terminer, Snowflake arrête l’instruction SQL et renvoie une erreur à l’utilisateur. Cela limite l’impact et le coût d’erreurs telles que les boucles infinies.

Bibliothèques

Bien que votre méthode Java puisse utiliser les classes et les méthodes des bibliothèques Java standard, les restrictions de sécurité de Snowflake désactivent certaines capacités, comme l’écriture dans des fichiers. Pour plus de détails sur les restrictions de la bibliothèque, voir la section intitulée Respecter les bonnes pratiques de sécurité.

Conception de la classe

Lorsqu’une instruction SQL appelle votre UDF Java, Snowflake appelle une méthode Java que vous avez écrite. Votre méthode Java est appelée « méthode de gestion », ou « handler » en abrégé.

Comme toute méthode Java, votre méthode doit être déclarée comme faisant partie d’une classe. Votre méthode de handler peut être une méthode statique ou une méthode d’instance de la classe. Si votre handler est une méthode d’instance, et que votre classe définit un constructeur sans argument, alors Snowflake appelle votre constructeur au moment de l’initialisation pour créer une instance de votre classe. Si votre handler est une méthode statique, votre classe n’est pas tenue d’avoir un constructeur.

Le handler est appelé une fois pour chaque ligne transmise à l’UDF Java. (Remarque : une nouvelle instance de la classe n’est pas créée pour chaque ligne ; Snowflake peut appeler plusieurs fois la méthode du handler de la même instance, ou appeler plusieurs fois la même méthode statique).

Pour optimiser l’exécution de votre code, Snowflake part du principe que l’initialisation peut être lente, alors que l’exécution de la méthode du handler est rapide. Snowflake fixe un délai plus long pour l’exécution de l’initialisation (y compris le temps de chargement de votre UDF et le temps d’appel du constructeur de la classe contenant la méthode du handler, si un constructeur est défini) que pour l’exécution du handler (le temps d’appel de votre handler avec une ligne d’entrée).

Des informations supplémentaires sur la conception de la classe se trouvent dans Création d’UDFs Java.

Optimisation de l’initialisation et contrôle de l’état global dans des UDFs scalaires

La plupart des UDFs scalaires doivent suivre les directives ci-dessous :

  • Si vous devez initialiser un état partagé qui ne change pas d’une ligne à l’autre, initialisez-la dans le constructeur de la classe de l’UDF.

  • Rédigez votre méthode de handler de façon à ce qu’elle soit sécurisée.

  • Évitez de stocker et de partager l’état dynamique entre les lignes.

Si votre UDF ne peut pas suivre ces directives, ou si vous souhaitez mieux comprendre les raisons de ces directives, veuillez lire les sous-sections suivantes.

Introduction

Snowflake s’attend à ce que les UDFs scalaires soient traitées indépendamment. S’appuyer sur l’état partagé entre les invocations peut entraîner un comportement inattendu, car le système peut traiter les lignes dans n’importe quel ordre et répartir ces invocations sur plusieurs JVMs. Les UDFs doivent éviter de s’appuyer sur un état partagé entre les appels vers la méthode du handler. Cependant, il existe deux situations dans lesquelles vous pourriez vouloir qu’une UDF stocke un état partagé :

  • Du code qui contient une logique d’initialisation coûteuse que vous ne voulez pas répéter pour chaque ligne.

  • Du code qui exploite l’état partagé entre les lignes, comme un cache.

Si vous devez partager un état sur plusieurs lignes, et si cet état ne change pas dans le temps, utilisez un constructeur pour créer un état partagé en définissant des variables au niveau de l’instance. Le constructeur n’est exécuté qu’une seule fois par instance, alors que le handler est appelé une fois par ligne. L’initialisation dans le constructeur est donc moins coûteuse lorsqu’un handler traite plusieurs lignes. Et comme le constructeur n’est appelé qu’une seule fois, il n’est pas nécessaire de l’écrire pour qu’il soit à l’abri des fils.

Si votre UDF stocke un état partagé qui change, votre code doit être prêt à gérer les accès concurrents à cet état. Les deux sections suivantes fournissent plus d’informations sur le parallélisme et l’état partagé.

Comprendre la parallélisation des UDF Java

Pour améliorer les performances, Snowflake effectue la parallélisation à la fois sur et dans des JVMs.

  • Sur des JVMs :

    Snowflake effectue la parallélisation entre les processus Worker dans un entrepôt. Chaque Worker exécute un (ou plusieurs) JVMs. Cela signifie qu’il n’y a pas d’état partagé global. Au maximum, l’état ne peut être partagé qu’au sein d’un seul JVM.

  • Dans des JVMs :

    • Chaque JVM peut exécuter plusieurs threads qui peuvent appeler la méthode du handler de la même instance en parallèle. Cela signifie que chaque méthode du handler doit être à l’abri des fils.

    • Si une instruction SQL appelle la même UDF plusieurs fois avec les mêmes arguments pour la même ligne, alors l’UDF renvoie la même valeur pour chaque appel pour cette ligne. Par exemple, la commande suivante renvoie deux fois la même valeur pour chaque ligne :

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

      Si vous souhaitez que plusieurs appels renvoient des valeurs indépendantes même s’ils reçoivent les mêmes arguments, liez plusieurs UDFs distinctes à la même méthode de traitement. Par exemple :

      1. Créez un fichier JAR nommé @java_udf_stage/rand.jar avec du code :

        class MyClass {
        
            private double x;
        
            // Constructor
            public MyClass()  {
                x = Math.random();
            }
        
            // Handler
            public double myHandler() {
                return x;
            }
        }
        
      2. Créez les UDFs Java comme indiqué ci-dessous. Ces UDFs portent des noms différents, mais utilisent le même fichier JAR et le même handler dans ce fichier JAR.

        create my_java_udf_1()
            returns double
            language java
            imports = ('@java_udf_stage/rand.jar')
            handler = 'MyClass.myHandler';
        
        create my_java_udf_2()
            returns double
            language java
            imports = ('@java_udf_stage/rand.jar')
            handler = 'MyClass.myHandler';
        
      3. Le code suivant appelle les deux UDFs. Les UDFs pointent vers le même fichier JAR et le même handler. Ces appels créent deux instances de la même classe. Chaque instance renvoie une valeur indépendante, de sorte que l’exemple ci-dessous renvoie deux valeurs indépendantes, plutôt que de renvoyer deux fois la même valeur :

        select
                my_java_udf_1(),
                my_java_udf_2()
            from table1;
        

Stockage des informations d’état JVM

Une raison d’éviter de s’appuyer sur un état partagé dynamique est que les lignes ne sont pas nécessairement traitées dans un ordre prévisible. Chaque fois qu’une instruction SQL est exécutée, Snowflake peut faire varier le nombre de lots, l’ordre dans lequel les lots sont traités et l’ordre des lignes dans un lot. Si une UDF scalaire est conçue de telle sorte qu’une ligne affecte la valeur de retour d’une ligne suivante, alors l” UDF peut renvoyer des résultats différents chaque fois que l” UDF est exécutée.

Gestion des erreurs

Une méthode Java utilisée comme UDF peut utiliser les techniques normales de gestion des exceptions de Java pour récupérer les erreurs dans la méthode.

Si une exception se produit à l’intérieur de la méthode et n’est pas récupérée par celle-ci, Snowflake génère une erreur qui inclut la trace de la pile pour l’exception.

Vous pouvez explicitement lancer une erreur sans la capturer afin de terminer la requête et de produire une erreur SQL. Par exemple :

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

Lors du débogage, vous pouvez inclure des valeurs dans le texte du message d’erreur SQL. Pour ce faire, placez un corps de méthode Java entier dans un bloc try-catch ; ajoutez des valeurs d’argument au message de l’erreur capturée ; et lancez une erreur avec le message étendu. Pour éviter de révéler des données sensibles, supprimez les valeurs des arguments avant de déployer les fichiers JAR dans un environnement de production.

Suivre les meilleures pratiques

  • Écrivez du code indépendant de la plate-forme.

    • Évitez le code qui suppose une architecture CPU spécifique (par exemple, x86).

    • Évitez le code qui suppose un système d’exploitation spécifique.

  • Si vous devez exécuter un code d’initialisation et que vous ne souhaitez pas l’inclure dans la méthode que vous appelez, vous pouvez placer le code d’initialisation dans un bloc d’initialisation statique.

Voir aussi :

Respecter les bonnes pratiques de sécurité

  • Votre méthode (et toute méthode de bibliothèque que vous appelez) doit se comporter comme une fonction pure, agissant uniquement sur les données qu’elle reçoit et renvoyant une valeur basée sur ces données, sans provoquer d’effets secondaires. Votre code ne doit pas tenter d’affecter l’état du système sous-jacent, si ce n’est en consommant une quantité raisonnable de mémoire et de temps processeur.

  • Les UDFs Java sont exécutées dans un moteur restreint. Ni votre code ni le code des méthodes de la bibliothèque que vous utilisez ne doit effectuer d’appels système interdits, notamment :

    • Contrôle des processus. Par exemple, vous ne pouvez pas faire bifurquer un processus. (Toutefois, vous pouvez utiliser plusieurs fils).

    • Accès au système de fichiers.

      À l’exception des cas suivants, les UDFs Java ne doivent pas lire ou écrire de fichiers :

      • Les UDFs Java peuvent lire les fichiers spécifiés dans la clause imports de la commande CREATE FUNCTION. Pour plus d’informations, voir CREATE FUNCTION.

      • Les UDFs Java peuvent écrire des fichiers, tels que des fichiers journaux, dans le répertoire /tmp.

        Chaque requête dispose de son propre système de fichiers sauvegardés en mémoire dans lequel son propre /tmp est stocké, de sorte que des requêtes différentes ne peuvent pas avoir de conflits de noms de fichiers.

        Cependant, des conflits au sein d’une requête sont possibles si une seule requête appelle plusieurs UDF et que ces UDFs essaient d’écrire dans le même nom de fichier.

    • Accès au réseau.

      Note

      Comme votre code ne peut pas accéder au réseau directement ou indirectement, vous ne pouvez pas utiliser le code du pilote JDBC de Snowflake pour accéder à la base de données. Votre UDF ne peut pas elle-même agir comme un client de Snowflake.

  • JNI (Java Native Interface) n’est pas pris en charge. Snowflake interdit le chargement de bibliothèques qui contiennent du code natif (par opposition au bytecode Java).