Funções definidas pelo usuário (UDFs) em Scala e Java no Unity Catalog
Esta página descreve como criar funções definidas pelo usuário (UDFs) Scala e Java, registrá-las no Unity Catalog e compartilhá-las entre ambientes de compute. As UDFs do Unity Catalog permitem reutilizar a lógica JVM existente com a governança e os controles de acesso do Unity Catalog.
Ao contrário das UDFs Scala com escopo de sessão que são limitadas a um único Notebook ou cluster, as UDFs registradas no Unity Catalog são:
- **Gerenciado**: Gerenciado com permissões e controles de acesso do Unity Catalog.
- Reutilizável : Compartilhado entre equipes, notebooks, jobs e SQL warehouses.
- Detectável : Visível no Catalog Explorer e em tabelas do sistema.
- **Isolado**: Executado em sandboxes com um custo de inicialização a frio único por sessão. Chamadas subsequentes são rápidas.
Requisitos
Seu Workspace deve estar habilitado para o Unity Catalog. Os requisitos adicionais a seguir se aplicam.
Compute : Todos os tipos de compute são suportados, incluindo notebooks e jobs serverless, SQL warehouses e Spark Declarative Pipelines no Lakeflow. O compute clássico requer o Databricks Runtime 18.2 ou acima. Em compute serverless e SQL warehouses, a definição da UDF deve especificar a Versão do Ambiente 4 ou acima no campo environment_version. Este requisito se aplica à definição da UDF, não ao notebook ou job de chamada. Consulte versões de ambiente serverless.
Desenvolvimento :
- **Scala**: 2.13.16. Scala 2.12 não é compatível.
- JDK : 17.
- Empacotamento : Um fat JAR contendo todas as dependências de terceiros usadas pela UDF.
Permissões :
- Criar uma UDF:
USAGEeCREATE FUNCTIONno esquema, eUSAGEno catálogo. - Execute uma UDF:
EXECUTEna função eUSAGEno esquema e no catálogo. - Acesse o arquivo JAR:
READ VOLUMEno volume onde o JAR está armazenado.
Consulte Gerenciar privilégios no Unity Catalog para obter mais informações sobre permissões do Unity Catalog.
Construa seu JAR de UDF
Empacote seu código compilado como um JAR e faça upload dele para um volume do Unity Catalog antes de registrar o UDF. Escolha um método de construção:
Compilar localmente
Siga estas etapas para criar um JAR grande usando um ambiente de desenvolvimento local.
Configure seu ambiente
Instale as ferramentas necessárias em sua máquina local. Os seguintes comandos são para macOS. Para outras plataformas, instale o JDK 17 e o sbt (Scala) ou Maven (Java) usando o gerenciador de pacotes da sua plataforma.
- Scala
- Java
Instale JDK 17 e sbt:
brew install openjdk@17
brew install sbt
Verifique sua instalação:
java -version # Should show Java 17
sbt --version # Should show sbt version
Instale JDK 17 e Maven:
brew install openjdk@17
brew install maven
Verifique sua instalação:
java -version # Should show Java 17
mvn --version # Should show Maven version
Crie seu projeto
Configure um projeto em Scala ou Java.
- Scala
- Java
Criar um novo projeto Scala usando sbt:
sbt new scala/scala-seed.g8
Quando solicitado, insira um nome de projeto (por exemplo, my-udf-project).
Configure build.sbt
Substitua o conteúdo do seu arquivo build.sbt pela seguinte configuração:
scalaVersion := "2.13.16"
ThisBuild / organization := "com.example"
lazy val myUDF = (project in file("."))
.settings(
name := "my-udf"
)
Habilitar o plugin sbt-assembly
Crie ou edite project/assembly.sbt e adicione:
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.0.0")
Este plug-in cria um JAR fat contendo todas as suas dependências.
Crie um novo projeto Maven usando o arquétipo de início rápido:
mvn archetype:generate \
-DgroupId=com.example \
-DartifactId=my-udf \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false
Este comando cria a estrutura de projeto padrão do Maven com os diretórios src/main/java e src/test/java.
Configurar pom.xml
No arquivo pom.xml gerado, dentro das tags <project></project>, adicione um bloco <properties> com a seguinte configuração:
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
Também dentro das tags <project></project>, adicione um bloco <build> com a seguinte configuração:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
O maven-shade-plugin cria um JAR completo contendo todas as suas dependências.
Escreva sua UDF
Ao escrever sua UDF, consulte Tipos de dados para tipos de dados compatíveis e Mapeamentos de linguagem para ver como os tipos Scala e Java mapeiam para tipos SQL.
Seu manipulador de UDF deve atender aos seguintes requisitos:
- Scala : Defina o manipulador como um método em um
object(não umclass). O valorHANDLERé resolvido para um método em um Scalaobject. - **Java**: Defina o manipulador como um
public staticmétodo. - **Assinatura**: os tipos de parâmetro, a ordem e o tipo de retorno do método devem corresponder à lista de argumentos e ao
RETURNStipo na suaCREATE FUNCTIONinstrução. - Apenas escalar : O manipulador deve retornar um único valor escalar. Tipos de retorno de tabela não são compatíveis.
- Autocontido : o manipulador deve operar apenas em seus argumentos de entrada. Não pode usar APIs Spark ou depender de pacotes centrais do Spark. Consulte Limitações.
Para Scala, um manipulador com um tipo de parâmetro primitivo (como Int) é ignorado e retorna NULL quando qualquer argumento de entrada é SQL NULL. Para receber e lidar com valores NULL, agrupe o parâmetro em Option, como Option[Int].
- Scala
- Java
Crie um objeto Scala em src/main/scala/com/example/MyUDF.scala e defina sua função UDF.
Exemplo básico
package com.example
object MyUDF {
def addOne(x: Int): Int = x + 1
}
Exemplo com dependência externa
Para usar bibliotecas externas, adicione-as ao seu arquivo build.sbt:
scalaVersion := "2.13.16"
ThisBuild / organization := "com.example"
lazy val myUDF = (project in file("."))
.settings(
name := "currency-udf",
libraryDependencies ++= Seq(
"org.apache.commons" % "commons-lang3" % "3.12.0"
)
)
Em seguida, use a dependência na sua UDF:
package com.example
import org.apache.commons.lang3.StringUtils
object CurrencyUDF {
private val rates: Map[String, Double] = Map(
"USD" -> 1.0,
"EUR" -> 1.1,
"GBP" -> 1.3,
"JPY" -> 0.007
)
def convertToUSD(price: Double, currency: String): Double = {
require(currency != null, "Currency must not be null")
val normalizedCurrency = StringUtils.upperCase(currency)
rates.get(normalizedCurrency) match {
case Some(rate) => price * rate
case None => throw new IllegalArgumentException(s"Unsupported currency: $currency")
}
}
}
Teste sua UDF com testes de unidade antes de implantar. Consulte Testando UDFs localmente.
Crie uma classe Java em src/main/java/com/example/MyUDF.java e defina sua UDF como um método estático público.
Exemplo básico
package com.example;
public class MyUDF {
public static int addOne(int x) {
return x + 1;
}
}
Exemplo com dependência externa
Para usar bibliotecas externas, adicione-as à seção <dependencies> do seu arquivo pom.xml:
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
</dependencies>
Em seguida, use a dependência na sua UDF:
package com.example;
import org.apache.commons.lang3.StringUtils;
import java.util.Map;
import java.util.HashMap;
public class CurrencyUDF {
private static final Map<String, Double> rates = new HashMap<>();
static {
rates.put("USD", 1.0);
rates.put("EUR", 1.1);
rates.put("GBP", 1.3);
rates.put("JPY", 0.007);
}
public static double convertToUSD(double price, String currency) {
if (currency == null) {
throw new IllegalArgumentException("Currency must not be null");
}
String normalizedCurrency = StringUtils.upperCase(currency);
if (!rates.containsKey(normalizedCurrency)) {
throw new IllegalArgumentException("Unsupported currency: " + currency);
}
return price * rates.get(normalizedCurrency);
}
}
Teste sua UDF com testes de unidade antes de implantar. Consulte Testando UDFs localmente.
Sua UDF é executada em uma sandbox isolada sem uma sessão Spark ativa, então ela não pode usar as Spark API de dentro do corpo da função. Por exemplo, você não pode criar ou operar em DataFrames ou datasets, executar spark.sql(...), ou acessar SparkSession ou SparkContext. A UDF deve ser uma lógica autocontida sobre seus argumentos de entrada. Também não pode depender de pacotes core do Spark.
Crie seu fat JAR
Construa seu projeto para criar um JAR completo contendo todas as dependências.
- Scala
- Java
No diretório raiz do seu projeto, execute:
sbt clean assembly
O JAR grande é criado em target/scala-2.13/ com um nome como my-udf-assembly-0.1.0-SNAPSHOT.jar.
No diretório raiz do seu projeto, execute:
mvn clean package
O JAR grande é criado em target/ com um nome como my-udf-1.0-SNAPSHOT.jar.
Faça upload do seu JAR para um volume do Unity Catalog
Se você ainda não tem um volume do Unity Catalog, crie um:
CREATE VOLUME IF NOT EXISTS my_catalog.my_schema.udf_jars
COMMENT 'Storage for UDF JAR files';
Se outros usuários precisarem executar a UDF, conceda-lhes READ VOLUME no volume:
GRANT READ VOLUME ON VOLUME my_catalog.my_schema.udf_jars TO `user@example.com`;
Faça upload do seu arquivo JAR para o volume usando o Catalog Explorer:
- No seu workspace do Databricks, clique em
Catálogo para abrir o Catalog Explorer.
- Selecione o catálogo e, em seguida, selecione o esquema que contém seu volume.
- Clique no nome do volume.
- Clique em Fazer upload para este volume e selecione seu arquivo JAR.
- Click upload .
- Após o upload ser concluído, clique no nome do seu arquivo JAR.
- Clique em Copiar caminho para copiar o caminho do volume para a área de transferência. Por exemplo,
/Volumes/my_catalog/my_schema/udf_jars/my-udf-assembly-0.1.0-SNAPSHOT.jar(Scala) ou/Volumes/my_catalog/my_schema/udf_jars/my-udf-1.0-SNAPSHOT.jar(Java). Você precisa desse caminho quando registrar a UDF.
Compilar em um notebook
Você pode compilar uma UDF, empacotá-la como um JAR e fazer upload dela para um volume do Unity Catalog diretamente de um notebook Databricks. Esta abordagem funciona para UDFs pequenas e sem dependência. Para UDFs com bibliotecas de terceiros, use Compilar localmente.
A seguinte célula Python escreve uma UDF Java que limpa uma string (remove espaços em branco, condensa espaços repetidos e usa letras minúsculas), a compila com o JDK 17, a empacota como um JAR e a copia para um volume do Unity Catalog. Atualize o volume_path para apontar para um volume existente no qual você tem permissão de WRITE VOLUME.
import os
import subprocess
import shutil
build_dir = "/tmp/udf_build"
package_dir = f"{build_dir}/src/com/databricks/udf"
classes_dir = f"{build_dir}/classes"
os.makedirs(package_dir, exist_ok=True)
os.makedirs(classes_dir, exist_ok=True)
# The UDF handler: a public static method on a plain Java class.
# The doubled backslashes produce a single backslash in the Java source (\\s+).
udf_code = """package com.databricks.udf;
public class StringCleanUDF {
public static String clean(String input) {
if (input == null) return null;
return input.trim().replaceAll("\\\\s+", " ").toLowerCase();
}
}
"""
with open(f"{package_dir}/StringCleanUDF.java", "w") as f:
f.write(udf_code)
# Compile with JDK 17 to match Environment Version 4.
subprocess.run(
["javac", "--release", "17", "-d", classes_dir, f"{package_dir}/StringCleanUDF.java"],
check=True,
)
# Package the compiled class into a JAR.
jar_path = f"{build_dir}/string_clean_udf.jar"
subprocess.run(["jar", "cf", jar_path, "-C", classes_dir, "."], check=True)
# Copy the JAR to a Unity Catalog volume.
volume_path = "/Volumes/my_catalog/my_schema/udf_jars/string_clean_udf.jar"
os.makedirs(os.path.dirname(volume_path), exist_ok=True)
shutil.copy2(jar_path, volume_path)
print(f"JAR uploaded to: {volume_path}")
Depois que o JAR estiver no volume, registre a UDF. Use LANGUAGE JAVA e defina o HANDLER para o método totalmente qualificado, como com.databricks.udf.StringCleanUDF.clean.
Registre seu UDF no Unity Catalog
Depois de construir e fazer upload do seu JAR, use a instrução CREATE FUNCTION para registrar sua UDF no Unity Catalog.
- Scala
- Java
CREATE OR REPLACE FUNCTION my_catalog.my_schema.add_one(x INT)
RETURNS INT
LANGUAGE SCALA
DETERMINISTIC
ENVIRONMENT (
java_dependencies = '["/Volumes/my_catalog/my_schema/udf_jars/my-udf-assembly-0.1.0-SNAPSHOT.jar"]',
environment_version = '4'
)
HANDLER 'com.example.MyUDF.addOne';
CREATE OR REPLACE FUNCTION my_catalog.my_schema.add_one(x INT)
RETURNS INT
LANGUAGE JAVA
DETERMINISTIC
ENVIRONMENT (
java_dependencies = '["/Volumes/my_catalog/my_schema/udf_jars/my-udf-1.0-SNAPSHOT.jar"]',
environment_version = '4'
)
HANDLER 'com.example.MyUDF.addOne';
A instrução CREATE FUNCTION usa os seguintes parâmetros:
-
LANGUAGE: O idioma da UDF. -
HANDLER: Caminho totalmente qualificado para o método, no formato'package.Object.method'(Scala) ou'package.ClassName.method'(Java). -
DETERMINISTIC: Declara que a função sempre retorna a mesma saída para a mesma entrada, permitindo a otimização da query.
Remova DETERMINISTIC se sua função chamar APIs externas ou tiver qualquer outro comportamento não determinístico.
-
ENVIRONMENT: Define o ambiente de execução para a UDF.java_dependencies: Um array JSON de caminhos de arquivos JAR em seus volumes do Unity Catalog. Este é o caminho do arquivo que você copiou no passo anterior. Use aspas simples ao redor do array e aspas duplas ao redor dos caminhos.environment_version: Deve ser'4'ou acima para UDFs Scala e Java. A Versão 4 do Ambiente especifica Scala 2.13.16 e JDK 17. Consulte Versões de ambiente Serverless.
Chame sua UDF em SQL e Notebooks
Após o registro, você pode chamar a UDF em queries SQL, notebooks e views:
-- Simple select
SELECT my_catalog.my_schema.add_one(5) AS result;
-- With table data
SELECT
id,
price,
currency,
my_catalog.my_schema.convert_to_usd(price, currency) AS price_usd
FROM my_catalog.my_schema.transactions;
-- Filtering
SELECT *
FROM my_catalog.my_schema.products
WHERE my_catalog.my_schema.convert_to_usd(price, currency) > 100;
-- Aggregation
SELECT
category,
SUM(my_catalog.my_schema.convert_to_usd(price, currency)) AS total_usd
FROM my_catalog.my_schema.sales
GROUP BY category;
Governança e compartilhamento
Use permissões do Unity Catalog para controlar quem pode executar sua UDF e para torná-la detectável em toda a sua organização.
Conceder permissões
Use o Catalog Explorer ou SQL para conceder as permissões necessárias para que outros usuários executem suas UDFs.
- Catalog Explorer
- SQL
- Na barra lateral, clique em
Catálogo .
- Selecione o catálogo e, em seguida, o esquema que contém sua função.
- Clique no nome da função.
- Na tab Permissões , clique em Conceder .
- Selecione as entidades de segurança às quais deseja conceder acesso e selecione a permissão
EXECUTE. - Clique em Confirmar .
Execute o comando a seguir em um notebook ou no editor Databricks SQL para conceder permissões EXECUTE a um usuário ou grupo.
-- Grant to a specific user
GRANT EXECUTE ON FUNCTION my_catalog.my_schema.add_one TO `user@example.com`;
-- Grant to a group
GRANT EXECUTE ON FUNCTION my_catalog.my_schema.add_one TO `data-engineers`;
Revogar permissões
Use o Catalog Explorer ou SQL para revogar permissões de outros usuários.
- Catalog Explorer
- SQL
- Na barra lateral, clique em
Catálogo .
- Selecione o catálogo e, em seguida, o esquema que contém sua função.
- Clique no nome da função.
- Na tab **Permissões**, selecione a caixa de seleção ao lado do principal do qual você deseja revogar o acesso. Clique em **Revogar**.
- Na notificação, clique em Revogar .
Execute o seguinte comando em um Notebook ou no editor do Databricks SQL para revogar as permissões EXECUTE de um usuário ou grupo.
-- Revoke from specific user
REVOKE EXECUTE ON FUNCTION my_catalog.my_schema.add_one FROM `user@example.com`;
-- Revoke from a group
REVOKE EXECUTE ON FUNCTION my_catalog.my_schema.add_one FROM `data-engineers`;
Descobrir UDFs
Para encontrar UDFs gerenciadas no Unity Catalog, query a tabela information_schema.routines, substituindo os valores my_catalog e my_schema:
SELECT
routine_catalog,
routine_schema,
routine_name,
routine_definition,
created
FROM system.information_schema.routines
WHERE routine_catalog = 'my_catalog'
AND routine_schema = 'my_schema';
Atualize sua UDF
Para atualizar uma UDF existente do Unity Catalog com código novo:
-
Fazer alterações em seu código localmente.
-
Reconstrua o JAR com um novo número de versão.
- Scala:
sbt clean assembly(por exemplo,my-udf-assembly-0.2.0-SNAPSHOT.jar) - Java:
mvn clean package(por exemplo,my-udf-2.0-SNAPSHOT.jar)
- Scala:
-
Faça upload do novo JAR para o volume do Unity Catalog.
-
Use
CREATE OR REPLACE FUNCTIONcom o mesmo nome de função para atualizar a UDF. Verifique se você referencia o JAR mais recente em seujava_dependencies.
Databricks usa o novo código na próxima invocação. Não é necessário reiniciar seu cluster.
Otimização de desempenho
Latência de cold start
A primeira chamada de UDF em uma sessão inicializa o sandbox isolado, o que adiciona latência. Chamadas subsequentes na mesma sessão são mais rápidas. Considere isso ao realizar benchmark ou projetar workloads sensíveis à latência.
Armazenamento em cache de cálculos caros
Se a sua UDF executar inicialização ou compute caros, armazene em cache o resultado para computá-lo apenas uma vez.
- Scala
- Java
Use um campo val no objeto Scala para armazenar o resultado em cache:
package example
object CachedUDF {
// Computed once and cached
val expensiveData: Map[String, Double] = {
// Load data from somewhere expensive
Map("key1" -> 1.0, "key2" -> 2.0)
}
def lookup(key: String): Double = {
expensiveData.getOrElse(key, 0.0)
}
}
Use um campo static com um bloco de inicialização estático para armazenar em cache o resultado:
package example;
import java.util.Map;
import java.util.HashMap;
public class CachedUDF {
// Computed once and cached
private static Map<String, Double> expensiveData;
static {
// Load data from somewhere expensive
expensiveData = new HashMap<>();
expensiveData.put("key1", 1.0);
expensiveData.put("key2", 2.0);
}
public static double lookup(String key) {
return expensiveData.getOrDefault(key, 0.0);
}
}
Use DETERMINISTIC quando apropriado
Marque sua UDF como DETERMINISTIC se ela sempre produzir a mesma saída para a mesma entrada. Isso permite que o otimizador de query armazene em cache os resultados e melhore o desempenho.
Limitações
- Apenas UDFs escalares são suportadas. Funções de agregação definidas pelo usuário (UDAFs) e funções de tabela definidas pelo usuário (UDTFs) não são suportadas.
- As UDFs são executadas em um sandbox isolado, sem uma sessão Spark ativa. Spark API (
SparkSession,SparkContext,spark.sql(...), operações de DataFrame e dataset) não estão disponíveis. - UDFs não podem depender de pacotes Spark core.
- UDFs não têm acesso a arquivos de Workspace ou volumes do Unity Catalog em Runtime.
Práticas recomendadas
A Databricks recomenda as seguintes práticas:
- Faça o versionamento dos seus arquivos JAR. Por exemplo,
my-udf-0.1.0.jar,my-udf-0.2.0.jar. - Valide os mapeamentos de tipo SQL antes da implantação. Consulte Mapeamentos de idioma.
- Conceda as permissões
READ VOLUMEeEXECUTEapenas aos usuários que precisam executar a UDF. Use a propriedade de grupo para UDFs compartilhadas entre equipes.
Testando UDFs localmente
Teste a sua UDF com testes de unidade antes de implantar na produção.
- Scala
- Java
Para testar src/main/scala/example/MyUDF.scala, crie um arquivo de teste em src/test/scala/example/MyUDFTest.scala:
package example
import org.scalatest.funsuite.AnyFunSuite
class MyUDFTest extends AnyFunSuite {
test("addOne should add 1 to input") {
assert(MyUDF.addOne(5) == 6)
}
test("addOne should handle negative numbers") {
assert(MyUDF.addOne(-1) == 0)
}
}
Adicionar a dependência de teste a build.sbt:
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.15" % Test
Para executar os testes:
sbt test
Para testar src/main/java/com/example/MyUDF.java, crie um arquivo de teste em src/test/java/com/example/MyUDFTest.java:
package com.example;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class MyUDFTest {
@Test
public void testAddOne() {
assertEquals(6, MyUDF.addOne(5));
}
@Test
public void testAddOneWithNegativeNumbers() {
assertEquals(0, MyUDF.addOne(-1));
}
}
Adicione a dependência JUnit à seção <dependencies> do seu pom.xml:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
Para executar os testes:
mvn test