设计 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 必须包含以下任一内容:
ARRAY 不能包含任何 NULL 值。
Java 方法可以通过以下两种方式之一接收这些数组:
使用 Java 的数组功能。
使用 Java 的 *varargs*(可变的实参数量)功能。
在这两种情况下, SQL 代码都必须传递 ARRAY。
通过 ARRAY 传递¶
将 Java 参数声明为数组。例如,以下方法中的第三个参数是字符串数组:
static int myMethod(int fixedArgument1, int fixedArgument2, String[] stringArray)
以下是完整示例:
创建并加载表:
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');创建 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); } } $$;调用 UDF:
SELECT concat_varchar_2(a) FROM string_array_table ORDER BY id; +---------------------+ | CONCAT_VARCHAR_2(A) | |---------------------| | Hello | | Hello Jay | | Hello Jay Smith | +---------------------+
通过可变实参传递¶
使用可变实参与使用数组非常相似。
在您的 Java 代码中,使用 Java 的可变实参声明风格:
static int myMethod(int fixedArgument1, int fixedArgument2, String ... stringArray)
以下是完整示例。此示例与前面的示例(对于数组)之间的唯一显著区别是该方法的参数声明。
创建并加载表:
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');创建 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); } } $$;调用 UDF:
SELECT concat_varchar(a) FROM string_array_table ORDER BY id; +-------------------+ | CONCAT_VARCHAR(A) | |-------------------| | Hello | | Hello Jay | | Hello Jay Smith | +-------------------+
设计保持在 Snowflake 施加的约束范围内的 Java UDFs¶
有关设计在 Snowflake 上运行良好的处理程序代码的信息,请参阅 设计保持在 Snowflake 施加的约束范围内的处理程序。
设计类¶
当 SQL 语句调用 Java UDF 时,Snowflake 会调用您编写的 Java 方法。您的 Java 方法称为“处理程序方法”,简称为“处理程序”。
与任何 Java 方法一样,您的方法必须作为类的一部分声明。处理程序方法可以是类的静态方法或者实例方法。如果处理程序是实例方法,并且类定义了零实参的构造函数,则 Snowflake 会在初始化时调用构造函数,以创建类的实例。如果处理程序是静态方法,则类不需要有构造函数。
对于传递给 Java UDF 的每一行,处理程序都会调用一次。(注意:不会为每一行创建类的新实例;Snowflake 可以多次调用同一个实例的处理程序方法,也可以多次调用同一个静态方法。
为了优化代码的执行,Snowflake 假定初始化的速度可能相当缓慢,而处理程序方法的执行速度很快。Snowflake 设置的执行初始化超时(包括加载 UDF 的时间,以及调用处理程序方法的包含类的构造函数的时间 – 如果定义了构造函数)比执行处理程序的超时(使用一行输入调用处理程序的时间)更长。
有关设计类的其他信息,请参阅 创建 Java UDF 处理程序。
在标量 UDFs 中优化初始化和控制全局状态¶
大多数函数和过程处理程序应遵循以下准则:
如果需要初始化不跨行更改的共享状态,请在处理程序函数外部(例如在模块或构造函数中)对其进行初始化。
编写线程安全的处理程序函数或方法。
避免跨行存储和共享动态状态。
如果您的 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;
如果您希望多次调用即使传递相同的实参也能返回独立的值,并且不想声明函数 VOLATILE,那么可以将多个独立的 UDFs 绑定到同一个处理程序方法上。例如:
使用如下代码,创建一个名为
@java_udf_stage/rand.jar
的 JAR 文件:class MyClass { private double x; // Constructor public MyClass() { x = Math.random(); } // Handler public double myHandler() { return x; } }
创建 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';
以下代码调用了两个 UDFs。UDFs 指向同一 JAR 文件和处理程序。这些调用会创建同一类的两个实例。每个实例返回一个独立的值,因此下面的示例返回两个独立值,而不是两次返回相同的值:
select my_java_udf_1(), my_java_udf_2() from table1;
存储 JVM 状态信息¶
避免依赖动态共享状态的一个原因是,行不一定按可预测的顺序进行处理。每次执行 SQL 语句时,Snowflake 都可以更改批处理数、批处理顺序以及批处理中的行顺序。如果将标量 UDF 设计为一行影响后续行的返回值,则每次执行 UDF 时, UDF 都会返回不同的结果。
处理错误¶
用作 UDF 的 Java 方法可以使用常规 Java 异常处理技术来捕获方法中的错误。
如果方法内部发生异常且未被方法捕获,则 Snowflake 会引发一个错误,其中包含异常的堆栈跟踪。启用 记录未处理的异常 后,Snowflake 会在事件表中记录有关未处理异常的数据。
为了结束查询并产生 SQL 错误,可以显式抛出异常而不捕获它。例如:
if (x < 0) {
throw new IllegalArgumentException("x must be non-negative.");
}
调试时,可以在 SQL 错误消息文本中包含值。为此,请将整个 Java 方法正文放在 try-catch 块中;将实参值追加到捕获的错误消息中;并使用扩展消息抛出异常。若要避免泄露敏感数据,请在将 JAR 文件部署到生产环境之前移除实参值。
遵循最佳实践¶
编写独立于平台的代码。
避免使用假设特定 CPU 架构(例如 x86)的代码。
避免使用假定特定操作系统的代码。
如果需要执行初始化代码,并且不想将其包含在调用的方法中,可以将初始化代码置于静态初始化块中。
使用内联处理程序时,请尽可能为 CREATE FUNCTION 或 CREATE PROCEDURE TARGET_PATH 参数指定一个值。这将促使 Snowflake 重用以前生成的处理程序代码输出,而不是每次调用都重新进行编译。有关更多信息,请参阅 使用内联处理程序。
另请参阅:
遵循良好的安全实践¶
要帮助确保处理程序以安全的方式运行,请参阅 UDFs 和过程的安全实践 中所述的最佳实践。