Création d’UDFs Java

Cette rubrique montre comment créer et installer une UDF Java (fonction définie par l’utilisateur).

Dans ce chapitre :

Écrire le code Java

Écrire la classe et la méthode Java

Écrivez une classe qui suit les spécifications ci-dessous :

  • Définissez la classe comme publique.

  • Définissez une méthode publique à l’intérieur de la classe.

    Si la méthode n’est pas une méthode statique, alors la classe qui contient la méthode doit avoir un constructeur à argument zéro, ou ne doit pas avoir défini de constructeur. (Lorsque Snowflake instancie la classe pour une méthode non statique, Snowflake ne transmet aucun argument).

    Si le constructeur génère une erreur, celle-ci est signalée comme une erreur de l’utilisateur, avec le message d’exception.

  • Si la méthode accepte des arguments, chaque argument doit être l’un des types de données spécifiés dans la colonne Java Data Type de la table SQL-Java Type Mappings.

    Lorsque vous choisissez les types de données des variables Java, tenez compte des valeurs maximales et minimales possibles des données qui pourraient être envoyées de (et retournées à) Snowflake.

    Les arguments des méthodes sont liés par leur position, et non par leur nom. Le premier argument transmis à l’UDF est le premier argument reçu par la méthode Java. Par exemple, dans les exemples de code ci-dessous, l’argument de l’UDF SQL a correspond à l’argument x de la méthode Java, et b correspond à y :

    create function my_udf(x numeric(9, 0), y float) ...
    
    public static int my_udf(int a, float b) ...
    
  • Spécifiez un type de retour approprié. Comme une UDF Java doit être une fonction scalaire, elle doit renvoyer une valeur à chaque fois qu’elle est appelée. Le type de retour doit être l’un des types de données spécifiés dans la colonne Java Data Type de la table SQL-Java Type Mappings. Le type de retour doit être compatible avec le type de données SQL spécifié dans la clause RETURNS de l’instruction CREATE FUNCTION.

  • Votre classe peut contenir plus d’une méthode. La méthode appelée par Snowflake peut appeler d’autres méthodes dans la même classe, ou dans d’autres classes.

  • Votre classe peut également contenir plus d’une méthode directement appelable. Par exemple, votre classe peut contenir des méthodes call_me_1() et call_me_2(), et chacune de ces méthodes peut appeler d’autres méthodes.

    Si votre classe contient plus d’une méthode directement appelable, créez vos UDF Java sous forme d” <label-udf_java_in_line_vs_pre_compiled> UDF précompilées. Vous créez un fichier JAR et exécutez deux (ou plusieurs) instructions CREATE FUNCTION, chacune d’entre elles spécifiant une fonction différente dans sa clause HANDLER.

  • Votre méthode (et toute méthode appelée par votre méthode) doit respecter les contraintes Snowflake imposées pour les UDFs Java.

Création de la fonction dans Snowflake

Les informations de cette section s’appliquent à toutes les UDFs Java, que le code soit spécifié en ligne ou précompilé.

Vous devez exécuter une instruction CREATE FUNCTION pour spécifier :

  • Le nom de la fonction SQL à utiliser.

  • Le nom de la méthode Java à appeler lorsque l’UDF Java est appelée.

Le nom de l’UDF ne doit pas nécessairement correspondre au nom de la méthode du handler écrite en Java. L’instruction CREATE FUNCTION associe le nom UDF à la méthode Java. Le schéma suivant illustre cette situation :

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

Pour choisir un nom pour l’UDF :

  • Suivez les règles de Identificateurs d’objet.

  • Choisissez un nom qui soit unique, ou suivez les règles pour Surcharge de noms d’UDF.

    Important

    Contrairement à la surcharge pour les UDFs SQL, qui distingue les fonctions en fonction du nombre et des types de données des arguments, les UDFs Java distinguent les méthodes en fonction uniquement du nombre d’arguments. Si deux méthodes Java ont le même nom et le même nombre d’arguments, mais des types de données différents, l’appel d’une UDF utilisant l’une de ces méthodes comme handler génère une erreur similaire à la suivante :

    Impossible de déterminer l’implémentation du handler « nom du handler » à appeler car il existe plusieurs définitions avec <nombre d’arguments> arguments dans la fonction <nom de la fonction définie par l’utilisateur> avec le handler <nom de la classe>.<nom du handler>

    Si un entrepôt est disponible, l’erreur est détectée au moment où l’UDF est créée. Sinon, l’erreur se produit lors de l’appel de l’UDF.

    La résolution basée sur les types de données n’est pas pratique car certains types de données SQL peuvent être mappés à plus d’un type de données Java et donc potentiellement à plus d’une signature UDF Java.

Les arguments des méthodes sont liés par leur position, et non par leur nom. Le premier argument transmis à l’UDF est le premier argument reçu par la méthode Java. Par exemple, dans les exemples de code ci-dessous, l’argument de l’UDF SQL a correspond à l’argument x de la méthode Java, et b correspond à y :

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

Pour plus d’informations sur les types de données des arguments, voir Mappages de type de donnée SQL-Java pour les paramètres et les types de retour.

UDFs en ligne vs. UDFs pré-compilées

Le code d’une UDF Java peut être spécifié de l’une des manières suivantes :

  • Pré-compilé : l’instruction CREATE FUNCTION spécifie l’emplacement d’un fichier JAR existant. L’utilisateur doit compiler le code source Java et placer le fichier JAR dans une zone de préparation.

  • En ligne : l’instruction CREATE FUNCTION spécifie le code source Java. Snowflake compile le code source et stocke le code compilé dans un fichier JAR. L’utilisateur a la possibilité de spécifier un emplacement pour le fichier JAR.

    • Si l’utilisateur spécifie un emplacement pour le fichier JAR, alors Snowflake compile le code une fois et conserve le fichier JAR pour une utilisation ultérieure.

    • Si l’utilisateur ne spécifie pas d’emplacement pour le fichier JAR, alors Snowflake recompile le code pour chaque instruction SQL qui appelle l’UDF, et Snowflake nettoie automatiquement le fichier JAR après la fin de l’instruction SQL.

Certaines différences pratiques peuvent influer sur le type que vous créez.

  • Les UDFs Java en ligne présentent les avantages suivants :

    • Elles sont généralement beaucoup plus faciles à mettre en œuvre. Il n’est pas nécessaire de compiler le code et de copier le fichier JAR dans une zone de préparation Snowflake. (Notez toutefois que la plupart des programmeurs compilent et testent leur code avant de le mettre en production, ainsi, la plupart du code de l’UDF Java est compilé par le développeur à un moment donné, même si l’UDF est en ligne).

  • Les UDFs Java précompilées présentent les avantages suivants :

    • Vous pouvez les utiliser lorsque vous avez un fichier JAR mais pas de code source.

    • Vous pouvez les utiliser si le code source est trop volumineux pour être collé dans une instruction CREATE FUNCTION. (Les UDFs Java en ligne ont une limite supérieure sur la taille du code source).

    • Une UDF Java précompilée peut contenir plusieurs fonctions appelables. Plusieurs instructions CREATE FUNCTION peuvent faire référence au même fichier JAR mais spécifient différentes fonctions de handler dans le fichier JAR.

      Les UDFs Java en ligne ne contiennent normalement qu’une seule fonction appelable. (Cette fonction appelable peut appeler d’autres fonctions, et ces autres fonctions peuvent être définies dans la même classe ou peuvent être définies dans d’autres classes définies dans les fichiers de la bibliothèque JAR).

    • Si vous disposez d’outils ou d’un environnement pour tester ou déboguer les fichiers JAR, il peut être plus pratique d’effectuer la majeure partie du travail de développement sur vos UDF en utilisant des fichiers JAR. Cela est particulièrement vrai si le code est vaste ou complexe.

Création d’une UDF Java en ligne

Pour une UDF en ligne, vous fournissez le code source Java dans le cadre de l’instruction CREATE FUNCTION. Par exemple :

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;
        }
    }
$$;

Le code source Java est spécifié dans la clause AS. Le code source peut être délimité soit de guillemets simples, soit d’une paire de signes de dollar ($$). L’utilisation du double signe dollar est généralement plus facile si le code source contient des guillemets simples intégrés.

Le code source Java peut contenir plus d’une classe et plus d’une méthode dans une classe. La clause HANDLER spécifie donc la classe et la méthode à appeler.

Une UDF Java en ligne (comme une UDF Java précompilée) peut appeler du code dans des fichiers JAR qui sont inclus dans la clause IMPORTS.

Pour plus de détails sur la syntaxe de l’instruction CREATE FUNCTION, voir CREATE FUNCTION.

Pour plus d’exemples, voir des exemples d’UDF Java en ligne.

Création d’une UDF Java précompilée

Organiser vos fichiers

Une UDF Java est stockée dans un fichier JAR. Si vous prévoyez de compiler vous-même le code Java pour créer le fichier JAR, vous pouvez organiser les fichiers comme indiqué ci-dessous. Cet exemple suppose que vous prévoyez d’utiliser le mécanisme de paquetage de Java.

  • developmentDirectory

    • packageDirectory

      • class_file1.java

      • class_file2.java

    • classDirectory

      • class_file1.class

      • class_file2.class

    • manifest_file.manifest (facultatif)

    • jar_file.jar

    • put_command.sql

developmentDirectory

Ce répertoire contient les fichiers spécifiques au projet nécessaires à la création de votre UDF Java.

packageDirectory

Ce répertoire contient les fichiers .java à compiler et à inclure dans le package.

class_file#.java

Ces fichiers contiennent le code source Java de l’UDF.

class_file#.class

Il s’agit du ou des fichiers .class créés en compilant des fichiers .java.

manifest_file.manifest

Le fichier manifeste facultatif utilisé lors de la combinaison des fichiers .class (et éventuellement des fichiers JAR de dépendance) dans le fichier JAR.

jar_file.jar

Le fichier JAR qui contient le code de l’UDF.

put_command.sql

Ce fichier contient la commande SQL PUT permettant de copier le fichier JAR vers une zone de préparation Snowflake.

Compilation du code Java et création du fichier JAR

Pour créer un fichier JAR qui contient le code Java compilé :

  • Utilisez javac pour compiler votre fichier .java en un fichier .class.

    Si vous utilisez un compilateur plus récent que la version 11.x, vous pouvez utiliser l’option « –release » pour spécifier que la version cible est la version 11.

  • Placez votre fichier .class dans un fichier JAR. Vous pouvez regrouper plusieurs fichiers de classe (et d’autres fichiers JAR) dans votre fichier JAR.

    Par exemple :

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

    Un fichier manifeste est obligatoire si vous utilisez un package, et facultatif si vous n’utilisez pas de package. L’exemple suivant utilise un fichier manifeste :

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

    Pour construire le fichier jar avec toutes les dépendances incluses, vous pouvez utiliser la commande mvn package de Maven avec le maven-assembly-plugin. Pour plus d’informations sur le plugin maven-assembly, voir :

    Snowflake fournit automatiquement les bibliothèques Java standard (par exemple, java.util). Si votre code fait appel à ces bibliothèques, vous n’avez pas besoin de les inclure dans votre fichier JAR.

    Les méthodes que vous appelez dans les bibliothèques doivent respecter les mêmes contraintes imposées par Snowflake que votre méthode Java.

Copie du fichier JAR dans votre zone de préparation

Snowflake lit le fichier JAR depuis une zone de préparation externe ou interne nommée, vous devez donc copier votre fichier JAR dans une zone de préparation. Vous pouvez utiliser la zone de préparation par défaut de votre utilisateur Snowflake, ou une autre zone de préparation existante, ou vous pouvez créer une nouvelle zone de préparation.

La zone de préparation hébergeant le fichier JAR doit être lisible par le propriétaire de l’UDF.

En général, vous utilisiez une zone de préparation interne nommée et vous utilisez la commande PUT pour charger le fichier vers la zone de préparation. (Notez que la commande PUT ne peut pas être exécutée par la GUI Snowflake. Vous pouvez utiliser SnowSQL pour exécuter PUT.)

Si vous supprimez ou renommez le fichier JAR, vous ne pouvez plus appeler l’UDF.

Snowflake recommande de suivre ces meilleures pratiques :

  • Si vous devez mettre à jour votre fichier JAR, alors :

    • Mettez-le à jour tant qu’aucun appel vers l’UDF ne peut être fait.

    • Si l’ancien fichier .jar se trouve toujours dans la zone de préparation, la commande PUT doit inclure la clause OVERWRITE=TRUE.

Voir la section Exemples pour un exemple de commande PUT pour copier un fichier .jar dans une zone de préparation.

Accorder des privilèges sur la fonction

Pour qu’un rôle autre que le propriétaire de la fonction puisse appeler la fonction, le propriétaire doit accorder les privilèges appropriés au rôle.

Les instructions GRANT pour une UDF Java sont essentiellement identiques aux instructions GRANT pour d’autres UDFs, comme les UDFs JavaScript.

Par exemple :

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

Exemples

UDFs en ligne Java

Création et appel d’une simple UDF Java en ligne

Les instructions suivantes créent et appellent une UDF Java en ligne. Ce code renvoie simplement le VARCHAR qui lui est transmis.

Cette fonction est déclarée avec la clause facultative CALLED ON NULL INPUT pour indiquer que la fonction est appelée même si la valeur de l’entrée est NULL. (Cette fonction renverrait NULL avec ou sans cette clause, mais vous pourriez modifier le code pour traiter NULL d’une autre manière, par exemple, pour renvoyer une chaîne vide).

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

Appelez l’UDF :

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

Transmission d’un NULL à une UDF Java en ligne

Ceci utilise les echo_varchar() UDF définies ci-dessus. La valeur SQL NULL est implicitement convertie en null Java, et cette null Java est retournée et implicitement reconvertie au format SQL NULL :

Appelez l’UDF :

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

Retourner NULL explicitement

Le code suivant montre comment retourner une valeur NULL de manière explicite. La valeur Java null est convertie au format SQL NULL.

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

Appelez l’UDF :

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

Transmission d’un OBJECT à une UDF Java en ligne

L’exemple suivant utilise le type de données SQL OBJECT et le type de données Java correspondant (Map<chaîne, chaîne>), et extrait une valeur à partir de OBJECT. Cet exemple montre également que vous pouvez transmettre plusieurs paramètres à une UDF Java.

Créer et charger une table qui contient une colonne de type OBJECT :

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

Créez l’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);
  }
}
$$;

Appelez l’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"        |                                                  |
| }                                   |                                                  |
+-------------------------------------+--------------------------------------------------+

Transmission d’un ARRAY à une UDF Java en ligne

L’exemple suivant utilise le type de données SQL ARRAY.

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

Appelez l’UDF :

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

UDFs Java précompilées

Création et appel d’une simple UDF Java précompilée

Les instructions suivantes créent une simple UDF Java. Cet échantillon suit généralement la structure des fichiers et des répertoires décrite dans Organiser vos fichiers.

Créez un fichier java qui contient votre code source :

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


}

En option, créez un fichier manifeste similaire à celui présenté ci-dessous :

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

Compilez le code source. Cet exemple stocke le ou les fichiers .class générés dans le répertoire nommé classDirectory.

javac -d classDirectory my_decrement_udf_package/my_decrement_udf_class.java

Créez le fichier JAR à partir du fichier .class. Cet exemple utilise « -C classDirectory » pour spécifier l’emplacement des fichiers .class :

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

Utilisez la commande PUT pour copier le fichier JAR du système de fichiers local vers une zone de préparation. Cet exemple utilise la zone de préparation par défaut de l’utilisateur, nommée @~ :

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

Vous pouvez stocker la commande PUT dans un fichier de script, puis exécuter ce fichier via snowsql. La commande snowsql doit être similaire à ce qui suit :

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

Cet exemple suppose que le mot de passe de l’utilisateur est spécifié dans la variable d’environnement SNOWSQL_PWD.

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

Appelez l’UDF :

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