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

Pour plus d’informations sur la façon dont Snowflake convertit les types de données Java et SQL, voir Mappage des types de données entre SQL et les langages de traitement.

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. L’UDF Java utilise les mêmes données de la base de données des fuseaux horaires IANA que la base native TIMEZONE Snowflake SQL (c’est-à-dire les données de la version 2021a de la base de données des fuseaux horaires).

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ées SQL-Java.

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 fixedArgument1, int fixedArgument2, String[] stringArray)
Copy

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

Créez l’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

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     |
+---------------------+
Copy

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 fixedArgument1, int fixedArgument2, String ... stringArray)
Copy

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

Créez l’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

Appelez l’UDF :

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

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

Pour plus d’informations sur la conception d’un code de gestionnaire qui fonctionne bien sur Snowflake, voir Concevoir des gestionnaires qui respectent les contraintes imposées par Snowflake.

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’un gestionnaire d’UDF Java.

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

La plupart des gestionnaires de fonctions et de procédures devraient suivre les lignes directrices ci-dessous :

  • Si vous devez initialiser un état partagé qui ne change pas d’une ligne à l’autre, initialisez-le en dehors de la fonction du gestionnaire, comme dans le module ou le constructeur.

  • Rédigez votre fonction ou méthode de gestionnaire d’une manière qui respecte le niveau de thread-safety.

  • É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.

Partage des états entre les appels

Snowflake s’attend à ce que les UDFs scalaires soient traitées indépendamment. Le fait de s’appuyer sur un état partagé entre les appels peut entraîner un comportement inattendu. En effet, le système peut traiter les lignes dans n’importe quel ordre et répartir ces appels sur plusieurs JVMs (pour les gestionnaires écrits en Java ou Scala) ou instances (pour les gestionnaires écrits en Python).

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 IMMUTABLE est SQL et qu’une instruction UDF 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 si l’UDF est IMMUTABLE :

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

      Si vous souhaitez que plusieurs appels renvoient des valeurs indépendantes même s’ils reçoivent les mêmes arguments, et si vous ne souhaitez pas déclarer la fonction VOLATILE, 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;
            }
        }
        
        Copy
      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 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. 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;
        
        Copy

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. Lorsque l’enregistrement des exceptions non gérées est activé, Snowflake enregistre les données relatives aux exceptions non gérées dans une table d’événements.

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.");
    }
Copy

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é

Pour vous assurer que votre gestionnaire fonctionne de manière sécurisée, consultez les meilleures pratiques décrites dans Pratiques de sécurité pour UDFs et procédures.