Java UDFs の設計

このトピックは、Java UDFs を設計するための助けになります。

このトピックの内容:

データ型の選択

コードを記述する前に、

  • 関数が引数として受け入れるデータ型と、関数が返すデータ型を選択します。

  • タイムゾーン関連の問題を考慮に入れます。

  • NULL 値の処理方法を決定します。

パラメーターと戻り値型の SQL-Javaデータ型のマッピング

SnowflakeがJavaと SQL データ型の間で変換する方法については、 SQL とハンドラー言語間のデータ型マッピング をご参照ください。

TIMESTAMP_LTZ 値とタイムゾーン

Java UDF は多くの場合、呼び出される環境から分離されています。ただし、タイムゾーンは呼び出し元の環境から継承されます。呼び出し元のセッションがJava UDF を呼び出す前にデフォルトのタイムゾーンを設定した場合、Java UDF のデフォルトのタイムゾーンは同じになります。Java UDF は、ネイティブ TIMEZONE Snowflake SQL が使用するのと同じ IANA タイムゾーンデータベース データを使用します(つまり、タイムゾーンデータベースのリリース 2021a からのデータ)。

NULL 値

Snowflakeは、 SQL NULL と VARIANT の JSON null という2つの異なる NULL 値をサポートしています。(Snowflake VARIANT NULL については、 NULL 値 を参照。)

Javaは、1つの null 値をサポートします。これは、非プリミティブデータ型専用です。

Java UDF に対する SQL NULL 引数は、 null をサポートするJavaデータ型の場合にのみ、Java null 値に変換されます。

返されたJava null 値は、 SQL NULL に変換されます。

引数の配列および可変の数

Java UDFs は、次に挙げるJavaデータ型のいずれかの配列を受け取ることができます。

  • String

  • boolean

  • double

  • float

  • int

  • long

  • short

渡される SQL 値のデータ型には、対応するJavaデータ型との互換性が必要です。データ型の互換性の詳細については、 SQL-Javaデータ型マッピング をご参照ください。

指定されたJavaデータ型のそれぞれに、次の追加ルールが適用されます。

  • boolean: Snowflake ARRAY には BOOLEAN 要素のみが含まれている必要があり、 NULL 値は含まれていてはなりません。

  • int/short/long: Snowflake ARRAY には、スケールが0の 固定小数点 要素のみが含まれている必要があり、 NULL 値が含まれていてはなりません。

  • float/double: Snowflake ARRAY には次のいずれかが含まれている必要があります。

    ARRAY には NULL の値を含めることはできません。

Javaメソッドは、次に挙げる2つの方法のいずれかでこれらの配列を受け取ることができます。

  • Javaの配列機能を使用。

  • Javaの varargs (引数の数が可変)機能を使用。

どちらの場合も、 SQL コードは ARRAY を渡す必要があります。

ARRAY 経由の引き渡し

Javaパラメーターを配列として宣言します。たとえば、次に挙げるメソッドの3番目のパラメーターは文字列配列です。

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

Varargs経由の引き渡し

Varargsの使用は、配列の使用と非常によく似ています。

Javaコードでは、Javaのvarargs宣言スタイルを使用します。

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 に渡される行ごとに1回呼び出されます。(注: クラスの新しいインスタンスは行ごとに作成されません。Snowflakeは、同じインスタンスのハンドラーメソッドを複数回呼び出すことも、同じ静的メソッドを複数回呼び出すこともできます。)

コードの実行を最適化するために、Snowflakeは、ハンドラーメソッドの実行が速い一方で、初期化が遅い可能性があると想定しています。Snowflakeは、初期化を実行するためのタイムアウト(UDF をロードする時間と、コンストラクターが定義されている場合は、ハンドラーメソッドを含むクラスのコンストラクターを呼び出すための時間を含む)を、ハンドラーを実行するためのタイムアウト(入力1行でハンドラーを呼び出す時間)よりも長く設定します。

クラスの設計に関する追加情報は Java UDF ハンドラーの作成 にあります。

スカラー UDFs における初期化の最適化とグローバル状態の制御

ほとんどの関数およびプロシージャハンドラーは、次のガイドラインに従う必要があります。

  • 行全般で変化しない共有状態を初期化する必要がある場合は、ハンドラー関数の外部(モジュールまたはコンストラクターなど)で初期化します。

  • スレッドセーフになるようにハンドラー関数またはメソッドを記述します。

  • 行間で動的状態を保存および共有することは避けてください。

UDF がこれらのガイドラインに従えない場合、またはこれらのガイドラインの理由をより深く理解したい場合は、次のいくつかのサブセクションをお読みください。

呼び出し全般での状態の共有

Snowflakeは、スカラー UDFs が独立して処理されることを期待しています。呼び出しの間で共有される状態に依存すると、予期しない動作が発生する可能性があります。これは、システムが任意の順序で行を処理し、それらの呼び出しを複数の JVMs(JavaまたはScalaで記述されたハンドラーの場合)またはインスタンス(Pythonで記述されたハンドラーの場合)に分散できるためです。

UDFs ハンドラーメソッドの呼び出し全体で共有状態に依存することは避けてください。ただし、 UDF に共有状態を保存する必要がある状況は2つあります。

  • 行ごとに繰り返さない高価な初期化ロジックを含むコード。

  • キャッシュなど、行間で共有状態を活用するコード。

複数の行で状態を共有する必要があり、その状態が時間の経過とともに変化しない場合は、コンストラクターを使用してインスタンスレベルの変数を設定し、共有状態を作成します。コンストラクターはインスタンスごとに1回だけ実行されますが、ハンドラーは行ごとに1回呼び出されるため、ハンドラーが複数の行を処理する場合は、コンストラクターによる初期化の方が安価です。また、コンストラクターは1回だけ呼び出されるため、スレッドセーフになるようにコンストラクターを作成する必要はありません。

UDF に変更された共有状態が保存されている場合は、その状態への同時アクセスを処理できるようにコードを準備する必要があります。次の2つのセクションでは、並列処理と共有状態について詳しく説明します。

Java UDF 並列化について

パフォーマンスを向上させるために、Snowflakeは JVMs の全体および内部で並列化します。

  • JVMs 全体:

    Snowflakeは、 ウェアハウス のワーカー間で並列化されます。各ワーカーは1つ(または複数)の JVMs を実行します。これは、グローバル共有状態がないことを意味します。最大で、状態は単一の JVM 内でのみ共有できます。

  • JVMs 内部:

    • 各 JVM は、同じインスタンスのハンドラーメソッドを並行して呼び出すことができる複数のスレッドを実行できます。これは、各ハンドラーメソッドがスレッドセーフである必要があることを意味します。

    • UDF が IMMUTABLE で、 SQL ステートメントが同じ行に対して同じ引数を使用して UDF を複数回呼び出す場合、 UDF は、その行の呼び出しごとに同じ値を返します。たとえば、 UDF が IMMUTABLE の場合、以下は各行に対して同じ値を2回返します。

      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 ファイルとハンドラーをポイントします。これらの呼び出しは、同じクラスのインスタンスを2つ作成します。各インスタンスは独立した値を返すため、以下の例では、同じ値を2回返すのではなく、2つの独立した値を返します。

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

JVM 状態情報の保存

動的共有状態に依存しない理由の1つは、行が必ずしも予測可能な順序で処理されるとは限らないことです。Snowflakeは、 SQL ステートメントが実行されるたびに、バッチの数、バッチが処理される順序、およびバッチ内の行の順序を変更できます。1つの行が後続の行の戻り値に影響を与えるようにスカラー 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)を想定したコードは避けます。

    • 特定のオペレーティングシステムを想定したコードは避けます。

  • 初期化コードを実行する必要があり、呼び出すメソッドに含めたくない場合は、初期化コードを静的初期化ブロックに入れることができます。

こちらもご参照ください。

優れたセキュリティプラクティスに準拠

ハンドラーがセキュアに機能するようにするには、 UDFs とプロシージャのセキュリティプラクティス で説明されているベストプラクティスをご参照ください。