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¶
A tabela abaixo mostra os mapeamentos de tipo entre SQL e Java. Esses mapeamentos geralmente se aplicam tanto aos argumentos passados à UDF de Java quanto aos valores retornados da UDF. Entretanto, há algumas exceções, que estão listadas em notas de rodapé.
Alguns tipos de dados de SQL (por exemplo, NUMBER) são compatíveis com vários tipos de dados de Java (por exemplo, int, long, etc.). Nesses casos, você pode usar qualquer tipo de dados de Java que tenha capacidade suficiente para manter os valores reais que serão passados. Se você passar um valor de SQL para um tipo de dados de Java incompatível (ou vice-versa), o Snowflake gerará um erro.
Tipo de SQL |
Tipo de Java |
Notas |
---|---|---|
NUMBER |
short |
Não pode ser nulo. Deve caber na faixa de valores do short (nenhuma parte fracionária, e a parte inteira não pode exceder os valores máximos/minutos do short). |
NUMBER |
Short |
Deve caber na faixa de valores do short (nenhuma parte fracionária, e a parte inteira não pode exceder os valores máximos/minutos do short). |
NUMBER |
int |
Não pode ser nulo. Deve caber na faixa do int (nenhuma parte fracionária, e a parte inteira não pode exceder os valores máximos/mínimos do int). |
NUMBER |
Inteiro |
Deve caber na faixa do int (nenhuma parte fracionária, e a parte inteira não pode exceder os valores máximos/mínimos do int). |
NUMBER |
long |
Não pode ser nulo. Deve caber na faixa do long (nenhuma parte fracionária, e a parte inteira não pode exceder os valores máximos/mínimos do long). |
NUMBER |
Long |
Deve caber na faixa do long (nenhuma parte fracionária, e a parte inteira não pode exceder os valores máximos/mínimos do long). |
NUMBER |
java.math.BigDecimal |
|
NUMBER |
java.math.BigInteger |
Deve caber na faixa do BigInteger (nenhuma parte fracionária). |
NUMBER |
Cadeia de caracteres |
|
FLOAT |
double |
Não pode ser nulo. |
FLOAT |
Double |
|
FLOAT |
float |
Não pode ser nulo. Pode resultar em perda de precisão. |
FLOAT |
Float |
Pode resultar em perda de precisão. |
FLOAT |
Cadeia de caracteres |
Pode resultar em perda de precisão (a conversão float -> string causa perdas). |
VARCHAR |
Cadeia de caracteres |
|
BINARY |
byte[] |
|
BINARY |
Cadeia de caracteres |
Codifica a cadeia de caracteres binária em hexadecimal. 4 |
BINARY |
InputStream |
Expõe o valor BINARY como uma sequência de bytes. |
BOOLEAN |
booleano |
Não pode ser nulo. |
BOOLEAN |
Booleano |
|
BOOLEAN |
Cadeia de caracteres |
|
DATE |
java.sql.Date |
|
DATE |
Cadeia de caracteres |
Formata a data como |
TIME |
java.sql.Time |
|
TIME |
Cadeia de caracteres |
Formata o tempo como |
TIMESTAMP_LTZ |
java.sql.Timestamp |
Deve caber na faixa do java.sql.Timestamp. 3 |
TIMESTAMP_LTZ |
Cadeia de caracteres |
O formato de saída é |
TIMESTAMP_NTZ |
java.sql.Timestamp |
Deve caber na faixa de java.sql.Timestamp. Trata o tempo real como uma compensação da época Unix (efetivamente impondo um fuso horário UTC). 3 |
TIMESTAMP_NTZ |
Cadeia de caracteres |
Trata o tempo real como uma compensação da época Unix (efetivamente impondo um fuso horário UTC). O formato de saída é |
TIMESTAMP_TZ |
java.sql.Timestamp |
Deve caber na faixa do java.sql.Timestamp. 3 |
TIMESTAMP_TZ |
Cadeia de caracteres |
O formato de saída é |
VARIANT |
O tipo de dados Variant é uma classe no pacote Snowpark. Para obter mais informações, consulte Tipos suportados do pacote Snowpark. Para obter um exemplo, consulte Como passar um valor VARIANT para uma UDF de Java inline. |
|
OBJECT |
Map<String, String> |
As chaves do mapa são as chaves do objeto e os valores são formatados como cadeias de caracteres. |
OBJECT |
Cadeia de caracteres |
Formata o objeto como uma cadeia de caracteres JSON (por exemplo, |
ARRAY |
String[] |
Formata os elementos da array como cadeias de caracteres. |
ARRAY |
Cadeia de caracteres |
Formata a matriz como uma cadeia de cadeia de caracteres JSON (por exemplo, |
GEOGRAPHY |
Cadeia de caracteres |
Formata a geografia como GeoJSON. |
GEOGRAPHY |
- 1(1,2)
O formato corresponde ao formato de carimbo de data/hora da internet (RFC)
DY, DD MON YYYY HH24:MI:SS TZHTZM
, conforme descrito em Formatos de carimbo de data/hora. Se uma diferença de fuso horário (o componenteTZHTZM
) estiver presente, normalmente são dígitos (por exemplo,-0700
indica 7 horas atrás de UTC). Se a diferença do fuso horário forZ
(de “Zulu”) e não dígitos, isso é sinônimo de “+0000” (UTC).- 2
O formato corresponde ao formato de carimbo de data/hora da internet (RFC)
DY, DD MON YYYY HH24:MI:SS
, conforme descrito em Formatos de carimbo de data/hora. Se a cadeia de cadeia de caracteres for seguida por um espaço eZ
(de “Zulu”), isso indica explicitamente que a diferença é “+0000” (UTC).- 3(1,2,3,4,5,6,7,8)
Embora o Snowflake possa armazenar valores de tempo com precisão de nanossegundos, a biblioteca de tempo java.sql.time mantém apenas uma precisão de milissegundos. A conversão entre os tipos de dados do Snowflake e do Java pode reduzir a precisão efetiva para milissegundos.
- 4(1,2,3,4,5,6)
Esse tipo de mapeamento é suportado na conversão de argumentos de SQL para Java, mas não na conversão de tipos de retorno de Java para tipos de SQL.
- 5
O Java não tem um tipo de dados Geography nativo. O tipo de dados Geography referido aqui é uma classe do pacote Snowpark. Para obter mais informações, consulte Tipos suportados do pacote Snowpark.
Tipos suportados do pacote Snowpark¶
Em uma função definida pelo usuário, é possível usar um subconjunto específico de tipos que estão incluídos no pacote do Snowflake Snowpark Java package. Embora estes tipos sejam projetados para uso em código do Snowpark, alguns também são suportados para uso em UDFs pela conveniência que podem proporcionar. (Para obter mais informações sobre o Snowpark, consulte a documentação Snowpark.)
Os tipos do Snowpark na tabela a seguir são suportados em código de UDF. Você não deve usar outros tipos do Snowpark em código de UDF; eles não são suportados lá.
Tipo do Snowpark |
Versão necessária do Snowpark |
Descrição |
---|---|---|
1.2.0 e superior |
Representa o tipo do Snowflake GEOGRAPHY. Para um exemplo que utiliza o tipo de dados |
|
1.4.0 e superior |
Representa dados de VARIANT do Snowflake. Para um exemplo que utiliza o tipo de dados |
Como especificar o pacote Snowpark como uma dependência de UDF¶
Ao desenvolver o código de uma UDF que utiliza o pacote Snowpark, você precisará configurar seu ambiente de desenvolvimento para que possa compilar e executar o código com dependências do Snowpark. Para obter mais informações, consulte Como configurar outros ambientes de desenvolvimento para o Snowpark Java.
Ao implantar uma UDF executando a instrução CREATE FUNCTION, você pode especificar o pacote Snowpark como uma dependência sem carregar o arquivo JAR para um estágio (a biblioteca já está no Snowflake). Para isso, especifique o nome e a versão do pacote na cláusula PACKAGES
. Para obter um exemplo de sintaxe, consulte Como passar um valor GEOGRAPHY para uma UDF de Java inline.
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 para tipos de parâmetros e retorno.
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:
Elementos FLOAT.
Elementos de ponto fixo (com qualquer escala).
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)
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');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); } } $$;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 | +---------------------+
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)
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');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); } } $$;Chame a UDF:
SELECT concat_varchar(a) FROM string_array_table ORDER BY id; +-------------------+ | CONCAT_VARCHAR(A) | |-------------------| | Hello | | Hello Jay | | Hello Jay Smith | +-------------------+
Criação de UDFs de Java que ficam dentro das restrições impostas pelo Snowflake¶
Para garantir a estabilidade dentro do ambiente do Snowflake, o Snowflake impõe as seguintes restrições a UDFs de Java. A menos que o contrário seja indicado, essas limitações são aplicadas quando a UDF é executada, não quando ela é criada.
Memória¶
Evite consumir memória demais.
Grandes valores de dados (normalmente tipo de dados BINARY, VARCHAR longos ou ARRAY ou OBJECT ou VARIANT grandes) podem consumir uma grande quantidade de memória.
Uma profundidade de pilha excessiva pode consumir uma grande quantidade de memória. O Snowflake testou chamadas de funções simples com aninhamento de 50 níveis de profundidade sem erros. O limite máximo prático depende de quanta informação é colocada na pilha.
UDFs retornam um erro se elas consumirem memória demais. O limite específico está sujeito a mudanças.
Hora¶
Evite algoritmos que demorem muito tempo por chamada.
Se uma UDF leva tempo demais para ser concluída, o Snowflake interrompe a instrução SQL e retorna um erro ao usuário. Isso limita o impacto e o custo de erros como loops infinitos.
Bibliotecas¶
Embora seu método de Java possa usar classes e métodos nas bibliotecas padrão de Java, as restrições de segurança do Snowflake desabilitam algumas capacidades, tais como escrever em arquivos. Para obter mais detalhes sobre restrições de bibliotecas, consulte a seção intitulada Boas práticas de segurança.
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 Criação de UDFs Java.
Como otimizar a inicialização e controlar o estado global em UDFs escalares¶
A maioria das UDFs escalares deve seguir as diretrizes abaixo:
Se você precisar inicializar o estado compartilhado que não muda entre linhas, inicialize-o no construtor da classe da UDF.
Escreva seu método 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.
Introdução¶
O Snowflake espera que o UDFs escalares seja processadas de forma independente. Confiar em um estado compartilhado entre invocações pode resultar em comportamento inesperado, pois o sistema pode processar linhas em qualquer ordem e espalhar essas invocações por várias JVMs. 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;
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:
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; } }
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';
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;
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.
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.");
}
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¶
Seu método (e qualquer método de biblioteca que você chamar) deve agir como uma função pura, agindo somente sobre os dados que recebe e retornando um valor baseado nesses dados, sem causar efeitos colaterais. Seu código não deve tentar afetar o estado do sistema subjacente, a não ser consumir uma quantidade razoável de memória e tempo de processador.
UDFs de Java são executadas dentro de um mecanismo restrito. Nem seu código nem o código nos métodos de biblioteca que você utiliza devem empregar quaisquer chamadas de sistema proibidas, inclusive:
Controle de processo. Por exemplo, você não pode bifurcar um processo. (Entretanto, você pode usar threads múltiplos).
Acesso ao sistema de arquivos.
Com as seguintes exceções, UDFs de Java não devem ler ou escrever arquivos:
UDFs de Java podem ler arquivos especificados na cláusula
imports
do comandoCREATE FUNCTION
. Para obter mais informações, consulte CREATE FUNCTION.UDFs de Java podem escrever arquivos, tais como arquivos de log, no diretório
/tmp
.Cada consulta recebe seu próprio sistema de arquivo com memória, no qual seu próprio /tmp é armazenado, de modo que consultas diferentes não podem ter conflitos de nome de arquivo.
Entretanto, conflitos dentro de uma consulta são possíveis se uma única consulta chamar mais de uma UDF e essas UDFs tentarem escrever no mesmo nome de arquivo.
Acesso à rede.
Nota
Como seu código não pode acessar a rede direta ou indiretamente, você não pode usar o código no driver JDBC do Snowflake para acessar o banco de dados. Sua UDF em si não pode agir como cliente do Snowflake.
JNI (Java Native Interface) não é suportado. O Snowflake proíbe o carregamento de bibliotecas que contenham código nativo (ao contrário de código de bytes de Java).
Quando usado dentro de uma região do governo, UDFs de Java oferecem suporte para algoritmos de criptografia que são validados para atender aos requisitos do padrão Federal Information Processing Standard (140-2) (FIPS 140-2). Somente podem ser usados algoritmos criptográficos que são permitidos no modo aprovado pelo FIPS da API de criptografia BouncyCastle para Java. Para obter mais informações sobre o FIPS 140-2, consulte FIPS 140-2.