Funções definidas pelo utilizador Scala e Java (UDFs) no Catálogo Unity

Esta página descreve como criar funções definidas pelo utilizador (UDFs) em Scala e Java, registá-las no Catálogo Unity e partilhá-las entre ambientes de computação. Os UDFs do Unity Catalog permitem reutilizar lógica JVM existente com governação e controlos de acesso do Unity Catalog.

Ao contrário dos UDFs Scala com âmbito de sessão, que estão limitados a um único bloco de notas ou cluster, os UDFs registados no Unity Catalog são:

  • Governado: Gerido com permissões e controlos de acesso do Unity Catalog.
  • Reutilizável: Partilhado entre equipas, cadernos, trabalhos e armazéns SQL.
  • Descobrível: Visível no Explorador de Catálogos e nas tabelas do sistema.
  • Isolado: Executado em ambientes isolados com um custo único de arranque inicial por sessão. As chamadas seguintes são rápidas.

Requisitos

Seu espaço de trabalho deve estar habilitado para o Catálogo Unity. Aplicam-se os seguintes requisitos adicionais.

Processamento: São suportados todos os tipos de processamento, incluindo notebooks e tarefas sem servidor, armazéns SQL e Pipelines Declarativos do Spark no Lakeflow. A computação clássica requer o Databricks Runtime 18.2 ou superior. Na computação sem servidor e nos armazéns de dados SQL, a definição da UDF tem de especificar a Versão 4 do Ambiente ou superior no campo environment_version. Este requisito aplica-se à definição da UDF, não ao notebook nem ao job que a invoca. Consulte as versões do ambiente sem servidor .

Desenvolvimento:

  • Scala: 2.13.16. O Scala 2.12 não é suportado.
  • JDK: 17.
  • Embalagem: Um JAR grosso contendo todas as dependências de terceiros usadas pela UDF.

Permissões:

  • Crie um UDF: USAGE e CREATE FUNCTION no esquema, e USAGE no catálogo.
  • Executa um UDF: EXECUTE na função, e USAGE no esquema e catálogo.
  • Acede ao ficheiro JAR: READ VOLUME no volume onde o JAR está armazenado.

Consulte Gerenciar privilégios no Catálogo Unity para mais informações sobre permissões do Catálogo Unity.

Constrói o teu JAR UDF

Empacota o teu código compilado como JAR e carrega-o para um volume do Unity Catalog antes de registares o UDF. Escolha um método de construção:

Construir localmente

Siga estes passos para construir um JAR gordo utilizando um ambiente de desenvolvimento local.

Configura o teu ambiente

Instale as ferramentas necessárias na sua máquina local. Os seguintes comandos são para macOS. Para outras plataformas, instala JDK 17 e sbt (Scala) ou Maven (Java) usando o gestor de pacotes da tua plataforma.

Scala

Instale o JDK 17 e o sbt:

brew install openjdk@17
brew install sbt

Verifique a sua instalação:

java -version   # Should show Java 17
sbt --version   # Should show sbt version

Java

Instale o JDK 17 e o Maven:

brew install openjdk@17
brew install maven

Verifique a sua instalação:

java -version   # Should show Java 17
mvn --version   # Should show Maven version

Crie o seu projeto

Configura um projeto em Scala ou Java.

Scala

Crie um novo projeto Scala usando sbt:

sbt new scala/scala-seed.g8

Quando solicitado, introduza um nome de projeto (por exemplo, my-udf-project).

Configurar build.sbt

Substitua o conteúdo do seu build.sbt ficheiro pela seguinte configuração:

scalaVersion := "2.13.16"

ThisBuild / organization := "com.example"

lazy val myUDF = (project in file("."))
  .settings(
    name := "my-udf"
  )

Ativar o plugin sbt-assembly

Criar ou editar project/assembly.sbt e adicionar:

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.0.0")

Este plugin cria um fat JAR que contém todas as tuas dependências.

Java

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 padrão do projeto Maven com src/main/java diretórios e src/test/java diretórios.

Configurar pom.xml

No ficheiro pom.xml gerado, dentro das etiquetas <project></project>, adiciona 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 <project></project> tags, adicione um <build> bloco 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 autónomo que contém todas as suas dependências.

Escreve o teu UDF

Ao escrever o seu UDF, consulte os tipos de dados para tipos de dados suportados e mapeamentos de linguagem para ver como os tipos Scala e Java correspondem aos tipos SQL.

O seu handler UDF deve cumprir os seguintes requisitos:

  • Scala: Defina o handler como um método num object (não um class). O valor HANDLER corresponde a um método numa object de Scala.
  • Java: Defina o handler como um public static método.
  • Assinatura: Os tipos dos parâmetros, a ordem e o tipo de retorno do método devem corresponder à lista de argumentos e ao tipo RETURNS na sua instrução CREATE FUNCTION.
  • Apenas escalar: O processador deve devolver um único valor escalar. Os tipos de retorno de tabela não são suportados.
  • Autónomo: O manipulador deve operar apenas com os seus argumentos de entrada. Não pode usar APIs do Spark nem depender dos pacotes core do Spark. Consulte Limitações.

Note

Para Scala, um handler com um tipo de parâmetro primitivo (como Int) é ignorado e retorna NULL quando qualquer argumento de entrada é SQL NULL. Para receber e tratar valores NULL, envolva o parâmetro em Option, por exemplo, Option[Int].

Scala

Criar um objeto Scala em src/main/scala/com/example/MyUDF.scala e definir a 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 build.sbt ficheiro:

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"
    )
  )

Depois usa a dependência no teu 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 o seu UDF com testes unitários antes de o implementar. Consulte Testar UDFs localmente.

Java

Crie uma classe Java em src/main/java/com/example/MyUDF.java e defina a UDF como um método público estático.

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 à <dependencies> secção do seu pom.xml ficheiro:

<dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.12.0</version>
    </dependency>
</dependencies>

Em seguida, utiliza a dependência no teu 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 o seu UDF com testes unitários antes de o implementar. Consulte Testar UDFs localmente.

Note

O seu UDF corre num sandbox isolado sem uma sessão Spark ativa, por isso não pode usar APIs Spark dentro do corpo da função. Por exemplo, não pode criar nem trabalhar com DataFrames ou Datasets, executar spark.sql(...) nem aceder a SparkSession ou SparkContext. A UDF deve ser uma lógica autónoma sobre os seus argumentos de entrada. Também não pode depender dos pacotes core do Spark.

Constrói o teu JAR gordo

Constrói o teu projeto para criar um JAR gordo contendo todas as dependências.

Scala

A partir do diretório raiz do seu projeto, execute:

sbt clean assembly

O fat JAR é criado em target/scala-2.13/ com um nome do tipo my-udf-assembly-0.1.0-SNAPSHOT.jar.

Java

A partir do diretório raiz do seu projeto, execute:

mvn clean package

O fat JAR é criado em target/ com um nome do tipo my-udf-1.0-SNAPSHOT.jar.

Carregue o seu JAR para um volume do Unity Catalog

Se ainda não tem um volume do Catálogo Unity, crie um:

CREATE VOLUME IF NOT EXISTS my_catalog.my_schema.udf_jars
COMMENT 'Storage for UDF JAR files';

Se outros utilizadores precisarem de executar o UDF, conceda-lhes READ VOLUME no volume:

GRANT READ VOLUME ON VOLUME my_catalog.my_schema.udf_jars TO `user@example.com`;

Carregue o seu ficheiro JAR para o volume usando o Explorador de Catálogos:

  1. No seu espaço de trabalho do Azure Databricks, clique no ícone Dados.Catálogo para abrir o Catalog Explorer.
  2. Selecione o catálogo e depois selecione o esquema que contém o seu volume.
  3. Clique no nome do volume.
  4. Clique em Carregar para este volume e selecione o seu ficheiro JAR.
  5. Clique em Carregar.
  6. Depois de concluído o carregamento, clique no nome do seu ficheiro JAR.
  7. Clique em Copiar caminho para copiar o caminho do volume para a sua prancheta. 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). Precisas deste caminho quando registas o UDF.

Construir num caderno

Pode compilar um UDF, empacotá-lo como JAR e carregá-lo diretamente para um volume do Unity Catalog a partir de um notebook Azure Databricks. Esta abordagem funciona para UDFs pequenas e sem dependências. Para UDFs com bibliotecas de terceiros, use Build locally.

A célula Python seguinte escreve um UDF Java que limpa uma string (elimina espaços em branco, colapsa espaços repetidos e minúsculas), compila-a com JDK 17, empacota-a como JAR e copia-a para um volume do Unity Catalog. Atualiza o volume_path para apontar para um volume existente para o qual tens WRITE VOLUME permissão.

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 de o JAR estar no volume, registar o UDF. Use LANGUAGE JAVA e defina o HANDLER para o método totalmente qualificado, como com.databricks.udf.StringCleanUDF.clean.

Registe o teu UDF no Catálogo Unity

Depois de compilar e carregar o seu JAR, use a CREATE FUNCTION instrução para registar o seu UDF no Unity Catalog.

Scala

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

Java

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 CREATE FUNCTION instrução utiliza os seguintes parâmetros:

  • LANGUAGE: A linguagem 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 devolve sempre a mesma saída para a mesma entrada, permitindo a otimização da consulta.

    Note

    Remova DETERMINISTIC se a sua função chama APIs externas ou tem qualquer outro comportamento não determinístico.

  • ENVIRONMENT: Define o ambiente de execução para o UDF.

    • java_dependencies: Um array JSON de caminhos de ficheiros JAR nos volumes do Unity Catalog. Este é o caminho do ficheiro que copiaste na etapa anterior. Utilize aspas simples à volta da matriz e aspas duplas à volta dos caminhos.
    • environment_version: Deve ser '4' ou superior para UDFs de Scala e de Java. A versão 4 do ambiente especifica o Scala 2.13.16 e o JDK 17. Consulte as versões do ambiente sem servidor .

Chame a sua UDF em SQL e notebooks

Após o registo, pode chamar a UDF em consultas SQL, cadernos e vistas:

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

Governação e partilha

Use permissões do Unity Catalog para controlar quem pode executar o seu UDF e torná-lo detectável em toda a sua organização.

Conceder permissões

Use o Explorador de Catálogos ou SQL para conceder as permissões necessárias a outros utilizadores para executarem os seus UDFs.

Explorador de Catálogos

  1. Na barra lateral, clique no ícone Dados.Catálogo.
  2. Selecione o catálogo e depois selecione o esquema que contém a sua função.
  3. Clica no nome da função.
  4. No separador Permissões, clique em Conceder.
  5. Selecione as entidades às quais pretende conceder acesso e selecione a permissão EXECUTE.
  6. Clique em Confirmar.

SQL

Execute o comando seguinte num caderno ou no editor SQL do Databricks para conceder EXECUTE permissões a um utilizador 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 Explorador de Catálogos ou SQL para revogar permissões a outros utilizadores.

Explorador de Catálogos

  1. Na barra lateral, clique no ícone Dados.Catálogo.
  2. Selecione o catálogo e depois selecione o esquema que contém a sua função.
  3. Clica no nome da função.
  4. No separador Permissões, selecione a caixa de seleção junto da entidade à qual pretende revogar o acesso. Clique Revogar.
  5. Na notificação, clique em Revoke.

SQL

Execute o seguinte comando num caderno ou no editor SQL do Databricks para revogar EXECUTE permissões a um utilizador 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`;

Descubra as UDFs

Para encontrar os UDFs geridos no Unity Catalog, consulta 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 a sua UDF

Para atualizar um UDF existente do Catálogo Unity com novo código:

  1. Faça alterações ao seu código localmente.
  2. Reconstruir 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)
  3. Carregar o novo JAR no volume do Unity Catalog.
  4. Use CREATE OR REPLACE FUNCTION com o mesmo nome de função para atualizar o UDF. Verifique que faz referência ao JAR mais recente no seu ficheiro java_dependencies.

O Azure Databricks usa o novo código na próxima invocação. Não precisas de reiniciar o teu cluster.

Otimização do desempenho

Latência de arranque a frio

A primeira chamada UDF numa sessão inicializa o sandbox isolado, o que adiciona latência. As chamadas subsequentes na mesma sessão são mais rápidas. Tenha isto em conta ao fazer benchmarks ou desenhar cargas de trabalho sensíveis à latência.

Armazenamento em cache de cálculos dispendiosos

Se o seu UDF realizar inicializações ou cálculos dispendiosos, armazene o resultado em cache para o calcular apenas uma vez.

Scala

Use um val campo no objeto Scala para armazenar em cache o resultado:

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)
  }
}

Java

Use um static campo com um bloco inicializador estático para armazenar o resultado em cache:

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 o seu UDF como DETERMINISTIC se ele produzisse sempre a mesma saída para a mesma entrada. Isto permite ao otimizador de consultas armazenar em cache os resultados e melhorar o desempenho.

Limitations

  • Apenas UDFs escalares são suportados. Funções agregadas definidas pelo utilizador (UDAFs) e funções de tabela definidas pelo utilizador (UDTFs) não são suportadas.
  • Os UDFs funcionam num sandbox isolado sem nenhuma sessão Spark ativa. As APIs do Spark (SparkSession, SparkContext, spark.sql(...), DataFrame e operações de conjunto de dados) não estão disponíveis.
  • Os UDFs não podem depender dos pacotes core do Spark.
  • Os UDFs não têm acesso a ficheiros de workspace ou volumes do Unity Catalog em tempo de execução.

Melhores práticas

A Databricks recomenda as seguintes práticas:

  • Atribua versões aos seus ficheiros JAR. Por exemplo, my-udf-0.1.0.jar, my-udf-0.2.0.jar.
  • Valide os mapeamentos de tipos SQL antes da implementação. Ver Mapeamentos linguísticos.
  • Conceda READ VOLUME e EXECUTE conceda permissões apenas a utilizadores que precisam de executar o UDF. Use a propriedade de grupo para UDFs partilhados entre equipas.

Testar UDFs localmente

Teste o seu UDF com testes unitários antes de o implementar em produção.

Scala

Para testar src/main/scala/example/MyUDF.scala, crie um ficheiro 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)
  }
}

Adicione a dependência do teste a build.sbt:

libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.15" % Test

Para realizar os testes:

sbt test

Java

Para testar src/main/java/com/example/MyUDF.java, crie um ficheiro 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 do JUnit à secçã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 realizar os testes:

mvn test

Recursos adicionais