Criação de UDFs de Java

Este tópico ajuda você a criar UDFs de Java.

Neste tópico:

Como escolher seus tipos de dados

Antes de escrever seu código:

  • Escolha os tipos de dados que sua função deve aceitar como argumentos e o tipo de dados que sua função deve retornar.

  • Leve em conta questões relacionadas ao fuso horário.

  • Decida como lidar com valores NULL.

Mapeamentos de tipos de dados SQL-Java para tipos de parâmetros e retorno

Para obter mais informações sobre como o Snowflake converte entre os tipos de dados Java e SQL, consulte Mapeamentos de tipos de dados entre linguagens do manipulador e SQL.

Valores TIMESTAMP_LTZ e fusos horários

Uma UDF de Java fica bastante isolada do ambiente no qual é chamada. Entretanto, o fuso horário é herdado do ambiente de chamada. Se a sessão do chamador definir um fuso horário padrão antes de chamar a UDF de Java, a UDF de Java tem o mesmo fuso horário padrão. A UDF de Java utiliza os mesmos dados da base de dados de fuso horário IANA usados pelo TIMEZONE SQL do Snowflake nativo (isso é, dados do lançamento 2021a do banco de dados de fuso horário).

Valores NULL

O Snowflake oferece suporte para dois valores NULL distintos: SQL NULL e JSON de VARIANT null. (Para obter mais informações sobre o VARIANT NULL do Snowflake, consulte Valores NULL).

O Java oferece suporte para um valor null, que é apenas para tipos de dados não-primitivos.

Um argumento SQL NULL para uma UDF de Java é convertido para o valor de Java null, mas somente para tipos de dados de Java que oferecem suporte para null.

Um valor de Java null retornado é convertido de volta para um NULL de SQL.

Arrays e número variável de argumentos

UDFs de Java podem receber arrays de qualquer um dos seguintes tipos de dados de Java:

  • Cadeia de caracteres

  • booleano

  • double

  • float

  • int

  • long

  • short

O tipo de dados dos valores de SQL passados deve ser compatível com o tipo de dados de Java correspondente. Para obter mais detalhes sobre compatibilidade de tipos de dados, consulte Mapeamentos de tipos de dados SQL-Java.

As seguintes regras adicionais se aplicam para cada um dos tipos de dados de Java especificados:

  • boolean: a ARRAY do Snowflake deve conter somente elementos BOOLEAN, e não deve conter nenhum valor NULL.

  • int/short/long: a ARRAY do Snowflake deve conter somente elementos de ponto fixo com uma escala de 0, e não deve conter nenhum valor NULL.

  • float/double: a ARRAY do Snowflake deve conter um dos seguintes:

    A ARRAY não deve conter nenhum valor NULL.

Os métodos de Java podem receber essas arrays de uma das duas maneiras a seguir:

  • Usando o recurso de array do Java.

  • Usando o recurso varargs (número variável de argumentos) do Java.

Em ambos os casos, seu código de SQL deve passar uma ARRAY.

Passando por uma ARRAY

Declare o parâmetro de Java como uma array. Por exemplo, o terceiro parâmetro no método a seguir é uma array de cadeias de caracteres:

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

Abaixo está um exemplo completo:

Criar e carregar a tabela:

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

Crie a 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

Chame a UDF:

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

Como passar usando o Varargs

O uso do varargs é muito semelhante ao uso de uma array.

Em seu código de Java, use o estilo de declaração do varargs de Java:

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

Abaixo está um exemplo completo. A única diferença significativa entre este exemplo e o exemplo anterior (para arrays) é a declaração dos parâmetros para o método.

Criar e carregar a tabela:

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

Crie a 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

Chame a UDF:

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

Criação de UDFs de Java que ficam dentro das restrições impostas pelo Snowflake

Para obter mais informações sobre a criação do código do manipulador que funciona bem no Snowflake, consulte Criação de manipuladores que ficam dentro das restrições impostas pelo Snowflake.

Criação da classe

Quando uma instrução de SQL chama sua UDF de Java, o Snowflake chama um método de Java que você escreveu. Seu método de Java é chamado de “método do manipulador”, ou “manipulador” abreviadamente.

Como em qualquer método de Java, seu método deve ser declarado como parte de uma classe. Seu método do manipulador pode ser um método estático ou um método de instância da classe. Se seu manipulador é um método de instância e sua classe definir um construtor sem argumentos, o Snowflake invocará seu construtor no momento da inicialização para criar uma instância de sua classe. Se seu manipulador for um método estático, sua classe não é obrigada a ter um construtor.

O manipulador é chamado uma vez para cada linha passada para a UDF de Java. (Nota: uma nova instância da classe não é criada para cada linha; o Snowflake pode chamar o mesmo método do manipulador da mesma instância mais de uma vez, ou chamar o mesmo método estático mais de uma vez).

Para otimizar a execução de seu código, o Snowflake supõe que a inicialização pode ser lenta, enquanto a execução do método do manipulador é rápida. O Snowflake define um tempo limite mais longo para executar a inicialização (incluindo o tempo para carregar sua UDF e o tempo para chamar o construtor do método do manipulador contendo a classe, se um construtor estiver definido) do que para executar o manipulador (o tempo para chamar seu manipulador com uma linha de entrada).

Informações adicionais sobre a criação da classe podem ser encontradas em Como criar um manipulador de UDF de Java.

Como otimizar a inicialização e controlar o estado global em UDFs escalares

A maioria dos manipuladores de função e procedimento deve seguir as diretrizes abaixo:

  • Se você precisar inicializar o estado compartilhado que não muda entre linhas, inicialize-o fora da função do manipulador, como no módulo e ou no construtor.

  • Escreva seu método ou função do manipulador de forma a ser thread-safe.

  • Evite armazenar e compartilhar o estado dinâmico em linhas diferentes.

Se sua UDF não puder seguir essas diretrizes, ou se você quiser entender mais profundamente as razões para essas diretrizes, leia as próximas subseções.

Compartilhamento de estado por chamadas

O Snowflake espera que o UDFs escalares seja processadas de forma independente. Confiar no estado compartilhado entre invocações pode resultar em um comportamento inesperado. Isso acontece pois o sistema pode processar linhas em qualquer ordem e distribuir essas invocações por vários JVMs (para manipuladores escritos em Java ou Scala) ou instâncias (para manipuladores escritos em Python).

UDFs devem evitar confiar em um estado compartilhado entre diferentes chamadas para o método do manipulador. Entretanto, há duas situações em que você pode querer que uma UDF armazene um estado compartilhado:

  • Código que contém uma lógica de inicialização cara que você não quer repetir para cada linha.

  • Código que use um estado compartilhado em linhas diferentes, como um cache.

Se você precisar compartilhar o estado em várias linhas, e se esse estado não mudar com o tempo, use um construtor para criar um estado compartilhado definindo variáveis em nível de instância. O construtor é executado apenas uma vez por instância, enquanto o manipulador é chamado uma vez por linha. Portanto, inicializar no construtor é mais barato quando um manipulador processa várias linhas. E como o construtor é chamado apenas uma vez, o construtor não precisa ser escrito de forma a ser thread-safe.

Se sua UDF armazena um estado compartilhado que muda, seu código deve estar preparado para lidar com o acesso simultâneo a esse estado. As duas próximas seções fornecem mais informações sobre paralelismo e estados compartilhados.

Entendendo a paralelização de UDF de Java

Para melhorar o desempenho, o Snowflake paraleliza tanto entre JVMs diferente quanto dentro de JVMs.

  • Entre JVMs diferentes:

    O Snowflake paralela entre os trabalhadores em um warehouse. Cada trabalhador executa uma (ou mais) JVMs. Isso significa que não existe um estado global compartilhado. No máximo, o estado só pode ser compartilhado dentro de uma única JVM.

  • Dentro de JVMs:

    • Cada JVM pode executar vários threads que podem chamar o mesmo método do manipulador da mesma instância em paralelo. Isso significa que cada método do manipulador precisa ser thread-safe.

    • Se uma UDF é IMMUTABLE e uma instrução SQL chama a UDF mais de uma vez com os mesmos argumentos para a mesma linha, a UDF retorna o mesmo valor para cada chamada para aquela linha. Por exemplo, o código a seguir retorna o mesmo valor duas vezes para cada linha se a UDF for IMMUTABLE:

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

      Se você quiser que múltiplas chamadas retornem valores independentes mesmo quando passarem os mesmos argumentos não quiser declarar a função como VOLATILE, ligue diferentes chamadas a UDFs separadas ao mesmo método do manipulador. Por exemplo:

      1. Crie um arquivo JAR chamado @java_udf_stage/rand.jar com o código:

        class MyClass {
        
            private double x;
        
            // Constructor
            public MyClass()  {
                x = Math.random();
            }
        
            // Handler
            public double myHandler() {
                return x;
            }
        }
        
        Copy
      2. Crie UDFs de Java da forma mostrada abaixo. Essas UDFs têm nomes diferentes, mas usam o mesmo arquivo JAR e o mesmo manipulador dentro desse arquivo 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. O código a seguir chama ambas as UDFs. As UDFs apontam para o mesmo arquivo e manipulador JAR. Essas chamadas criam duas instâncias da mesma classe. Cada instância retorna um valor independente, portanto o exemplo abaixo retorna dois valores independentes em vez de retornar o mesmo valor duas vezes:

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

Como armazenar informações de estado de uma JVM

Uma razão para evitar confiar no estado dinâmico compartilhado é que as linhas não são necessariamente processadas em uma ordem previsível. Cada vez que uma instrução SQL é executada, o Snowflake pode variar o número de lotes, a ordem em que os lotes são processados e a ordem das linhas dentro de um lote. Se uma UDF escalar for projetada para que uma linha afete o valor de retorno para uma linha posterior, a UDF pode retornar resultados diferentes cada vez que a UDF for executada.

Tratamento de erros

Um método de Java usado como uma UDF pode usar as técnicas normais de tratamento de exceções de Java para detectar erros dentro do método.

Se uma exceção ocorre dentro do método e não é capturada pelo método, o Snowflake gera um erro que inclui o traço da pilha para a exceção. Quando o registro de exceções não tratadas está ativado, o Snowflake registra dados sobre exceções não tratadas em uma tabela de eventos.

Você pode gerar uma exceção explicitamente sem capturá-la para encerrar a consulta e produzir um erro de SQL. Por exemplo:

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

Ao depurar, você pode incluir valores no texto da mensagem de erro do SQL. Para fazer isso, coloque todo um corpo de método de Java em um bloco try-catch; anexe valores de argumento à mensagem do erro capturado; e gere uma exceção com a mensagem estendida. Para evitar revelar dados sensíveis, remova valores de argumento antes de enviar arquivos JAR para um ambiente de produção.

Práticas recomendadas

  • Escreva código independente de plataforma.

    • Evite códigos que suponham uma arquitetura de CPU específica (por exemplo, x86).

    • Evite códigos que suponham um sistema operacional específico.

  • Se você precisar executar o código de inicialização e não quiser incluí-lo no método que você chama, você pode colocar o código de inicialização em um bloco de inicialização estático.

Consulte também:

Boas práticas de segurança

Para ajudar a garantir que seu manipulador funcione de forma segura, consulte as práticas recomendadas descritas em Práticas de segurança para UDFs e procedimentos.