设计 Java UDFs

本主题可帮助您设计 Java UDFs。

本主题内容:

选择数据类型

在编写代码之前,请执行以下操作:

  • 选择函数应接受作为实参的数据类型,以及函数应返回的数据类型。

  • 考虑与时区相关的问题。

  • 决定如何处理 NULL 值。

参数和返回类型的 SQL-Java 数据类型映射

有关 Snowflake 如何在 Java 与 SQL 数据类型之间转换的信息,请参阅 SQL 与处理程序语言之间的数据类型映射

TIMESTAMP_LTZ 值和时区

Java UDF 在很大程度上与调用它的环境相隔离。但是,时区是从调用环境继承的。如果调用方的会话在调用 Java UDF 之前设置了默认时区,则 Java UDF 具有相同的默认时区。Java UDF 使用与 Snowflake SQL 所用的原生 TIMEZONE 相同的 IANA 时区数据库 (https://www.iana.org/time-zones) 数据(即时区数据库的版本 2021a 中的数据)。

NULL 值

Snowflake 支持两个不同的 NULL 值:SQL NULL 和 VARIANT 的 JSON null。(有关 Snowflake VARIANT NULL 的信息,请参阅 NULL 值。)

Java 支持一个 null 值,该值仅适用于非基元数据类型。

Java UDF 的 SQL NULL 实参会转换为 Java null 值,但仅适用于支持 null 的 Java 数据类型。

返回的 Java null 值将转换回 SQL NULL

实参数组和变量数量

Java UDFs 可以接收以下任何 Java 数据类型的数组:

  • 字符串

  • 布尔

  • 双精度

  • 浮点

  • 整数

  • 长整型

  • 短整型

传递的 SQL 值的数据类型必须与相应的 Java 数据类型兼容。有关数据类型兼容性的更多信息,请参阅 SQL-Java 数据类型映射

以下附加规则适用于每种指定的 Java 数据类型:

  • 布尔:Snowflake ARRAY 必须仅包含 BOOLEAN 元素,并且不得包含任何 NULL 值。

  • int/short/long:Snowflake ARRAY 必须仅包含小数位数为 0 的 定点 元素,并且不得包含任何 NULL 值。

  • float/double:Snowflake ARRAY 必须包含以下任一内容:

    • FLOAT 元素。

    • 定点 元素(具有任意小数位数)。

    ARRAY 不能包含任何 NULL 值。

Java 方法可以通过以下两种方式之一接收这些数组:

  • 使用 Java 的数组功能。

  • 使用 Java 的 *varargs*(可变的实参数量)功能。

在这两种情况下, SQL 代码都必须传递 ARRAY

通过 ARRAY 传递

将 Java 参数声明为数组。例如,以下方法中的第三个参数是字符串数组:

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

以下是完整示例:

创建并加载表:

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

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

调用 UDF:

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

通过可变实参传递

使用可变实参与使用数组非常相似。

在您的 Java 代码中,使用 Java 的可变实参声明风格:

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

以下是完整示例。此示例与前面的示例(对于数组)之间的唯一显著区别是该方法的参数声明。

创建并加载表:

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

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

调用 UDF:

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

设计保持在 Snowflake 施加的约束范围内的 Java UDFs

有关设计在 Snowflake 上运行良好的处理程序代码的信息,请参阅 设计保持在 Snowflake 施加的约束范围内的处理程序

设计类

当 SQL 语句调用 Java UDF 时,Snowflake 会调用您编写的 Java 方法。您的 Java 方法称为“处理程序方法”,简称为“处理程序”。

与任何 Java 方法一样,您的方法必须作为类的一部分声明。处理程序方法可以是类的静态方法或者实例方法。如果处理程序是实例方法,并且类定义了零实参的构造函数,则 Snowflake 会在初始化时调用构造函数,以创建类的实例。如果处理程序是静态方法,则类不需要有构造函数。

对于传递给 Java UDF 的每一行,处理程序都会调用一次。(注意:不会为每一行创建类的新实例;Snowflake 可以多次调用同一个实例的处理程序方法,也可以多次调用同一个静态方法。

为了优化代码的执行,Snowflake 假定初始化的速度可能相当缓慢,而处理程序方法的执行速度很快。Snowflake 设置的执行初始化超时(包括加载 UDF 的时间,以及调用处理程序方法的包含类的构造函数的时间 – 如果定义了构造函数)比执行处理程序的超时(使用一行输入调用处理程序的时间)更长。

有关设计类的其他信息,请参阅 创建 Java UDF 处理程序

在标量 UDFs 中优化初始化和控制全局状态

大多数函数和过程处理程序应遵循以下准则:

  • 如果需要初始化不跨行更改的共享状态,请在处理程序函数外部(例如在模块或构造函数中)对其进行初始化。

  • 编写线程安全的处理程序函数或方法。

  • 避免跨行存储和共享动态状态。

如果您的 UDF 无法遵循这些准则,或者如果您想更深入地了解这些准则的原因,请阅读接下来的几个小节。

跨调用共享状态

Snowflake 希望独立处理标量 UDFs。依赖调用间共享的状态可能会导致意外行为。这是因为系统可按任何顺序处理行,并将这些调用分散到多个 JVMs(适用于使用 Java 或 Scala 编写的处理程序)或实例(适用于使用 Python 编写的处理程序)中。

UDFs 应避免在对处理程序方法的调用之间依赖共享状态。但是,在以下两种情况下,可能希望 UDF 存储共享状态:

  • 包含不希望对每一行重复的昂贵初始化逻辑的代码。

  • 跨行(如缓存)利用共享状态的代码。

如果需要在多行中共享状态,而且状态不会随时间改变,那么可以使用构造函数通过设置实例级变量来创建共享状态。每个实例只执行一次构造函数,而处理程序则每行调用一次,因此当处理程序处理多行时,在构造函数中进行初始化的成本更低。此外,由于构造函数只被调用一次,因此无需编写线程安全的构造函数。

如果 UDF 存储的共享状态发生变化,那么您的代码就必须准备好处理对该状态的并发访问。接下来的两节详细介绍并行性和共享状态。

了解 Java UDF 并行化

为了提高性能,Snowflake 可在 JVMs 之间和内部实现并行化。

  • 跨多个 JVMs:

    Snowflake 可在 仓库 中的工作线程之间实现并行化。每个工作线程运行一个(或多个) JVMs。这意味着没有全局共享状态。最多只能在单个 JVM 内共享状态。

  • 在 JVMs 内:

    • 每个 JVM 可以执行多个线程,这些线程可以并行调用同一实例的处理程序方法。这意味着每个处理程序方法都需要是线程安全的。

    • 如果 UDF 为 IMMUTABLE,并且 SQL 语句对同一行使用相同实参调用 UDF 多次,那么 UDF 对该行的每次调用都会返回相同的值。例如,如果 UDF 为 IMMUTABLE,以下调用会为每行返回两次相同的值。

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

      如果您希望多次调用即使传递相同的实参也能返回独立的值,并且不想声明函数 VOLATILE,那么可以将多个独立的 UDFs 绑定到同一个处理程序方法上。例如:

      1. 使用如下代码,创建一个名为 @java_udf_stage/rand.jar 的 JAR 文件:

        class MyClass {
        
            private double x;
        
            // Constructor
            public MyClass()  {
                x = Math.random();
            }
        
            // Handler
            public double myHandler() {
                return x;
            }
        }
        
        Copy
      2. 创建 Java UDFs,如下所示。这些 UDFs 具有不同的名称,但使用相同的 JAR 文件和该 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. 以下代码调用了两个 UDFs。UDFs 指向同一 JAR 文件和处理程序。这些调用会创建同一类的两个实例。每个实例返回一个独立的值,因此下面的示例返回两个独立值,而不是两次返回相同的值:

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

存储 JVM 状态信息

避免依赖动态共享状态的一个原因是,行不一定按可预测的顺序进行处理。每次执行 SQL 语句时,Snowflake 都可以更改批处理数、批处理顺序以及批处理中的行顺序。如果将标量 UDF 设计为一行影响后续行的返回值,则每次执行 UDF 时, UDF 都会返回不同的结果。

处理错误

用作 UDF 的 Java 方法可以使用常规 Java 异常处理技术来捕获方法中的错误。

如果方法内部发生异常且未被方法捕获,则 Snowflake 会引发一个错误,其中包含异常的堆栈跟踪。启用 记录未处理的异常 后,Snowflake 会在事件表中记录有关未处理异常的数据。

为了结束查询并产生 SQL 错误,可以显式抛出异常而不捕获它。例如:

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

调试时,可以在 SQL 错误消息文本中包含值。为此,请将整个 Java 方法正文放在 try-catch 块中;将实参值追加到捕获的错误消息中;并使用扩展消息抛出异常。若要避免泄露敏感数据,请在将 JAR 文件部署到生产环境之前移除实参值。

遵循最佳实践

  • 编写独立于平台的代码。

    • 避免使用假设特定 CPU 架构(例如 x86)的代码。

    • 避免使用假定特定操作系统的代码。

  • 如果需要执行初始化代码,并且不想将其包含在调用的方法中,可以将初始化代码置于静态初始化块中。

  • 使用内联处理程序时,请尽可能为 CREATE FUNCTIONCREATE PROCEDURE TARGET_PATH 参数指定一个值。这将促使 Snowflake 重用以前生成的处理程序代码输出,而不是每次调用都重新进行编译。有关更多信息,请参阅 使用内联处理程序

另请参阅:

遵循良好的安全实践

要帮助确保处理程序以安全的方式运行,请参阅 UDFs 和过程的安全实践 中所述的最佳实践。

语言: 中文