Funciones definidas por el usuario (UDF) de Scala y Java en Unity Catalog

En esta página se describe cómo crear Scala y Java funciones definidas por el usuario (UDF), registrarlas en el catálogo de Unity y compartirlas en entornos de proceso. Las UDF del catálogo de Unity le permiten reutilizar la lógica de JVM existente con controles de acceso y gobernanza del catálogo de Unity.

A diferencia de las UDF de Scala con ámbito de sesión limitadas a un único cuaderno o clúster, las UDF registradas en el catálogo de Unity son:

  • Gobernado: Administrado con permisos y controles de acceso de Unity Catalog.
  • Reutilizable: compartido entre equipos, cuadernos, trabajos y almacenes de SQL.
  • Detectable: visible en el Explorador de catálogos y las tablas del sistema.
  • Aislado: Se ejecuta en entornos aislados con un coste único de arranque en frío por sesión. Las llamadas posteriores son rápidas.

Requisitos

Su área de trabajo debe estar habilitada para Unity Catalog. Se aplican los siguientes requisitos adicionales.

Proceso: se admiten todos los tipos de proceso, incluidos cuadernos y trabajos sin servidor, almacenes de SQL y canalizaciones declarativas de Spark en Lakeflow. El proceso clásico requiere Databricks Runtime 18.2 o superior. En la computación sin servidor y los almacenes SQL, la definición de UDF debe especificar la versión 4 del entorno o una superior en el campo environment_version. Este requisito se aplica a la definición de UDF, no al cuaderno o al trabajo que realiza la llamada. Consulte las versiones del entorno sin servidor .

Desarrollo:

  • Scala: 2.13.16. No se admite Scala 2.12.
  • JDK: 17.
  • Empaquetado: un archivo JAR fat que contiene todas las dependencias de terceros usadas por la UDF.

Permisos:

  • Cree una UDF: USAGE y CREATE FUNCTION en el esquema y USAGE en el catálogo.
  • Ejecute una UDF: EXECUTE en la función y USAGE en el esquema y el catálogo.
  • Acceda al archivo JAR: READ VOLUME en el volumen donde se almacena el archivo JAR.

Consulte Administrar privilegios en el catálogo de Unity para obtener más información sobre los permisos del catálogo de Unity.

Compila tu archivo JAR de UDF

Empaquete el código compilado como un ARCHIVO JAR y cárguelo en un volumen de catálogo de Unity antes de registrar la UDF. Elija un método de compilación:

Compilar localmente

Siga estos pasos para crear un archivo JAR fat mediante un entorno de desarrollo local.

Configuración del entorno

Instale las herramientas necesarias en el equipo local. Los siguientes comandos son para macOS. Para otras plataformas, instale JDK 17 y sbt (Scala) o Maven (Java) mediante el administrador de paquetes de la plataforma.

Scala

Instale JDK 17 y sbt:

brew install openjdk@17
brew install sbt

Compruebe la instalación:

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

Java

Instale JDK 17 y Maven:

brew install openjdk@17
brew install maven

Compruebe la instalación:

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

Creación del proyecto

Configure un proyecto en Scala o Java.

Scala

Cree un nuevo proyecto de Scala mediante sbt:

sbt new scala/scala-seed.g8

Cuando se le solicite, escriba un nombre de proyecto (por ejemplo, my-udf-project).

Configuración de build.sbt

Reemplace el contenido del build.sbt archivo por la siguiente configuración:

scalaVersion := "2.13.16"

ThisBuild / organization := "com.example"

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

Habilitación del complemento sbt-assembly

Cree o edite project/assembly.sbt y agregue:

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

Este complemento crea un archivo JAR fat que contiene todas las dependencias.

Java

Cree un nuevo proyecto de Maven mediante el arquetipo de inicio rápido:

mvn archetype:generate \
  -DgroupId=com.example \
  -DartifactId=my-udf \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DinteractiveMode=false

Este comando crea la estructura de proyecto estándar de Maven con src/main/java directorios y src/test/java .

Configuración de pom.xml

Dentro de las etiquetas <project></project>, en el archivo pom.xml generado, añada un bloque <properties> con la siguiente configuración:

<properties>
  <maven.compiler.source>17</maven.compiler.source>
  <maven.compiler.target>17</maven.compiler.target>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

También dentro de las <project></project> etiquetas, agregue un <build> bloque con la siguiente configuración:

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

maven-shade-plugin crea un archivo JAR fat que contiene todas las dependencias.

Escribe tu UDF

Al escribir la UDF, consulte Tipos de datos para conocer los tipos de datos admitidos y las asignaciones de lenguaje para ver cómo se asignan los tipos de Scala y Java a tipos SQL.

El controlador de UDF debe cumplir los siguientes requisitos:

  • Scala: defina el manejador como un método de un object (no de un class). El valor HANDLER corresponde a un método en un object de Scala.
  • Java: defina el controlador como métodopublic static.
  • Firma: los tipos y el orden de los parámetros del método, así como el tipo de retorno, deben coincidir con la lista de argumentos y con el tipo RETURNS de la instrucción CREATE FUNCTION.
  • Solo valores escalares: El controlador debe devolver un único valor escalar. No se admiten los tipos de retorno de tabla.
  • Autocontenido: El manejador debe operar solo con sus argumentos de entrada. No puede usar las API de Spark ni depender de los paquetes principales de Spark. Consulte Limitaciones.

Note

Para Scala, se omite un controlador con un tipo de parámetro primitivo (como Int) y devuelve NULL cuando cualquier argumento de entrada es SQL NULL. Para recibir y manejar los valores de NULL, envuelve el parámetro en Option, como Option[Int].

Scala

Cree un objeto Scala en src/main/scala/com/example/MyUDF.scala y defina la función UDF.

Ejemplo básico

package com.example

object MyUDF {
  def addOne(x: Int): Int = x + 1
}

Ejemplo con dependencia externa

Para usar bibliotecas externas, agréguelas al build.sbt archivo:

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

A continuación, usa la dependencia en la 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")
    }
  }
}

Pruebe la UDF con pruebas unitarias antes de la implementación. Consulte Pruebas de UDF localmente.

Java

Cree una clase Java en src/main/java/com/example/MyUDF.java y defina la UDF como un método estático público.

Ejemplo básico

package com.example;

public class MyUDF {
    public static int addOne(int x) {
        return x + 1;
    }
}

Ejemplo con dependencia externa

Para usar bibliotecas externas, agréguelas a la <dependencies> sección del pom.xml archivo:

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

Luego, utilice la dependencia en su 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);
    }
}

Pruebe la UDF con pruebas unitarias antes de la implementación. Consulte Pruebas de UDF localmente.

Note

La UDF se ejecuta en un espacio aislado sin sesión activa de Spark, por lo que no puede usar las API de Spark desde el cuerpo de la función. Por ejemplo, no puede crear ni trabajar con DataFrames o conjuntos de datos, ejecutar spark.sql(...) ni acceder a SparkSession o SparkContext. La UDF debe ser una lógica autosuficiente que opere únicamente con sus argumentos de entrada. Tampoco puede depender de los paquetes principales de Spark.

Crea tu fat JAR

Compile el proyecto para crear un archivo JAR fat que contenga todas las dependencias.

Scala

En el directorio raíz del proyecto, ejecute:

sbt clean assembly

El JAR fat se crea en target/scala-2.13/ con un nombre como my-udf-assembly-0.1.0-SNAPSHOT.jar.

Java

En el directorio raíz del proyecto, ejecute:

mvn clean package

El JAR fat se crea en target/ con un nombre como my-udf-1.0-SNAPSHOT.jar.

Carga tu archivo JAR en un volumen de Unity Catalog

Si aún no tiene un volumen de Catálogo de Unity, cree uno:

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

Si otros usuarios necesitan ejecutar la función UDF, concédales READ VOLUME sobre el volumen:

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

Suba su archivo JAR al volumen mediante Catalog Explorer:

  1. En el área de trabajo de Azure Databricks, haga clic en Data icon.Catalog para abrir el Explorador de catálogos.
  2. Seleccione el catálogo y, a continuación, seleccione el esquema que contiene el volumen.
  3. Haga clic en el nombre del volumen.
  4. Haga clic en Subir a este volumen y seleccione su archivo JAR.
  5. Haga clic en Cargar.
  6. Una vez completada la carga, haga clic en el nombre del archivo JAR.
  7. Haga clic en Copiar ruta para copiar la ruta del volumen en el portapapeles. Por ejemplo, /Volumes/my_catalog/my_schema/udf_jars/my-udf-assembly-0.1.0-SNAPSHOT.jar (Scala) o /Volumes/my_catalog/my_schema/udf_jars/my-udf-1.0-SNAPSHOT.jar (Java). Necesitará esta ruta de acceso cuando registre la UDF.

Compilación en un cuaderno

Puede compilar una UDF, empaquetarla como un archivo JAR y cargarla en un volumen de Unity Catalog directamente desde un cuaderno de Azure Databricks. Este enfoque funciona para UDF pequeñas y sin dependencias. Para las UDFs con bibliotecas de terceros, use Compilar localmente.

La siguiente celda de Python escribe una UDF de Java que limpia una cadena (recorta los espacios en blanco, contrae los espacios repetidos y convierte el texto a minúsculas), la compila con JDK 17, la empaqueta como un archivo JAR y la copia a un volumen de Unity Catalog. Actualice el/la volume_path para que apunte a un volumen existente sobre el que tenga WRITE VOLUME permisos.

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

Después de que el archivo JAR esté en el volumen, registre la UDF. Use LANGUAGE JAVA y establezca HANDLER en el método totalmente cualificado, como com.databricks.udf.StringCleanUDF.clean.

Registra tu UDF en Unity Catalog

Después de generar y subir el archivo JAR, utilice la instrucción CREATE FUNCTION para registrar su UDF en 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';

La CREATE FUNCTION instrucción usa los parámetros siguientes:

  • LANGUAGE: el idioma de la UDF.

  • HANDLER: ruta de acceso completa al método, con el formato 'package.Object.method' (Scala) o 'package.ClassName.method' (Java).

  • DETERMINISTIC: declara que la función siempre devuelve la misma salida para la misma entrada, lo que permite la optimización de consultas.

    Note

    Quite DETERMINISTIC si la función llama a API externas o tiene cualquier otro comportamiento no determinista.

  • ENVIRONMENT: define el entorno de ejecución para la UDF.

    • java_dependencies: matriz JSON de rutas de acceso a archivos JAR en los volúmenes del catálogo de Unity. Esta es la ruta de acceso del archivo que copió en el paso anterior. Use comillas simples para la matriz y comillas dobles para las rutas.
    • environment_version: debe ser '4' o superior para Scala y Java UDF. La versión 4 del entorno especifica Scala 2.13.16 y JDK 17. Consulte las versiones del entorno sin servidor .

Llamada a la UDF en SQL y cuadernos

Después del registro, puede llamar a la UDF en consultas SQL, cuadernos y 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;

Gobernanza y uso compartido

Use permisos de Catálogo de Unity para controlar quién puede ejecutar la UDF y para que sea reconocible en toda la organización.

Concesión de permisos

Use el Explorador de catálogos o SQL para conceder los permisos necesarios para que otros usuarios ejecuten las UDF.

Explorador de catálogos

  1. En la barra lateral, haga clic en Icono de datos.Catálogo.
  2. Seleccione el catálogo y, a continuación, seleccione el esquema que contiene la función.
  3. Haga clic en el nombre de la función.
  4. En la pestaña Permisos , haga clic en Conceder.
  5. Seleccione las entidades de seguridad a las que desea conceder acceso y seleccione el permiso EXECUTE.
  6. Haga clic en Confirmar.

SQL

Ejecute el siguiente comando en un cuaderno o en el editor de SQL de Databricks para conceder EXECUTE permisos a un usuario o 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`;

Revocar permisos

Use el Explorador de catálogos o SQL para revocar permisos de otros usuarios.

Explorador de catálogos

  1. En la barra lateral, haga clic en Icono de datos.Catálogo.
  2. Seleccione el catálogo y, a continuación, seleccione el esquema que contiene la función.
  3. Haga clic en el nombre de la función.
  4. En la pestaña Permisos , active la casilla situada junto a la entidad de seguridad desde la que desea revocar el acceso. Haga clic en Revocar.
  5. En la notificación, haga clic en Revocar.

SQL

Ejecute el siguiente comando en un cuaderno o en el editor de SQL de Databricks para revocar EXECUTE permisos de un usuario o 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`;

Detección de UDF

Para encontrar las UDF gestionadas en Unity Catalog, consulte la tabla information_schema.routines, sustituyendo los valores my_catalog y 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';

Actualización de la UDF

Para actualizar una UDF de catálogo de Unity existente con código nuevo:

  1. Realice cambios en el código localmente.
  2. Vuelva a generar el archivo JAR con un nuevo número de versión.
    • Scala: sbt clean assembly (por ejemplo, my-udf-assembly-0.2.0-SNAPSHOT.jar)
    • Java: mvn clean package (por ejemplo, my-udf-2.0-SNAPSHOT.jar)
  3. Suba el nuevo archivo JAR al volumen de Unity Catalog.
  4. Use CREATE OR REPLACE FUNCTION con el mismo nombre de función para actualizar la UDF. Compruebe que hace referencia al archivo JAR más reciente en java_dependencies.

Azure Databricks usa el nuevo código en la siguiente invocación. No es necesario reiniciar el clúster.

Optimización del rendimiento

Latencia de inicio en frío

La primera llamada ADF de una sesión inicializa el espacio aislado, que agrega latencia. Las llamadas posteriores en la misma sesión son más rápidas. Tenga en cuenta esto al realizar pruebas comparativas o diseñar cargas de trabajo sensibles a la latencia.

Almacenamiento en caché de cálculos costosos

Si la UDF realiza una inicialización o cálculo costosos, almacene en caché el resultado para calcularlo solo una vez.

Scala

Use un val campo en el objeto Scala para almacenar en caché el 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 un static campo con un bloque de inicializador estático para almacenar en caché el 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);
    }
}

Utilice DETERMINISTIC cuando corresponda

Marque la UDF como DETERMINISTIC si siempre generara la misma salida para la misma entrada. Esto permite al optimizador de consultas almacenar en caché los resultados y mejorar el rendimiento.

Limitaciones

  • Solo se admiten UDF escalares. No se admiten funciones de agregado definidas por el usuario (UDF) ni funciones de tabla definidas por el usuario (UDF).
  • Las UDF se ejecutan en un espacio aislado sin sesión activa de Spark. Las API de Spark (SparkSession, SparkContext, spark.sql(...), operaciones de DataFrame y Dataset) no están disponibles.
  • Las UDF no pueden depender de los paquetes principales de Spark.
  • Las UDF no tienen acceso a los archivos del área de trabajo ni a los volúmenes del catálogo de Unity en tiempo de ejecución.

procedimientos recomendados

Databricks recomienda los procedimientos siguientes:

  • Asigne una versión a los archivos JAR. Por ejemplo: my-udf-0.1.0.jar, my-udf-0.2.0.jar.
  • Valide las asignaciones de tipos de SQL antes de la implementación. Consulte Asignaciones de idioma.
  • Conceda permisos de READ VOLUME y de EXECUTE solo a los usuarios que necesiten ejecutar la UDF. Utilice la propiedad del grupo para las UDF compartidas por varios equipos.

Probar UDF localmente

Pruebe la UDF con pruebas unitarias antes de realizar la implementación en producción.

Scala

Para probar src/main/scala/example/MyUDF.scala, cree un archivo de prueba en 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)
  }
}

Agregue la dependencia de prueba a build.sbt:

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

Para ejecutar las pruebas:

sbt test

Java

Para probar src/main/java/com/example/MyUDF.java, cree un archivo de prueba en 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));
    }
}

Añada la dependencia de JUnit a la sección <dependencies> de pom.xml:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>

Para ejecutar las pruebas:

mvn test

Recursos adicionales