Java UDFs の設計¶
このトピックは、Java UDFs を設計するための助けになります。
このトピックの内容:
データ型の選択¶
コードを記述する前に、
関数が引数として受け入れるデータ型と、関数が返すデータ型を選択します。
タイムゾーン関連の問題を考慮に入れます。
NULL 値の処理方法を決定します。
パラメーターと戻り値型の SQL-Javaデータ型のマッピング¶
次のテーブルは、 SQL とJavaの間の型マッピングを示しています。これらのマッピングは通常、Java UDF に渡される引数と UDF から返される値の両方に適用されます。ただし、脚注に記載されているいくつかの例外があります。
一部の SQL データ型(例: NUMBER)は、複数のJavaデータ型(例: int、long)と互換性があります。このような場合、渡される実際の値を保持するための十分な容量がある、任意のJavaデータ型を使用できます。互換性のないJavaデータ型に SQL 値を渡すと(またはその逆)、Snowflakeはエラーをスローします。
SQL 型 |
Java型 |
メモ |
---|---|---|
NUMBER |
short |
nullにはできません。shortの範囲内に収まる必要があります(小数部分なし。整数部分は、shortの最大値/最小値を超えることはできません)。 |
NUMBER |
Short |
shortの範囲内に収まる必要があります(小数部分なし。整数部分は、shortの最大値/最小値を超えることはできません)。 |
NUMBER |
int |
nullにはできません。intの範囲内に収まる必要があります(小数部分なし。整数部分は、intの最大値/最小値を超えることはできません)。 |
NUMBER |
Integer |
intの範囲内に収まる必要があります(小数部分なし。整数部分は、intの最大値/最小値を超えることはできません)。 |
NUMBER |
long |
nullにはできません。longの範囲内に収まる必要があります(小数部分なし。整数部分は、longの最大値/最小値を超えることはできません)。 |
NUMBER |
Long |
longの範囲内に収まる必要があります(小数部分なし。整数部分は、longの最大値/最小値を超えることはできません)。 |
NUMBER |
java.math.BigDecimal |
|
NUMBER |
java.math.BigInteger |
BigInteger の範囲内に収まる必要があります(小数部分なし)。 |
NUMBER |
String |
|
FLOAT |
double |
nullにはできません。 |
FLOAT |
Double |
|
FLOAT |
float |
nullにはできません。精度が低下する可能性があります。 |
FLOAT |
Float |
精度が低下する可能性があります。 |
FLOAT |
String |
精度が低下する可能性があります(floatから文字列への変換で損失が発生)。 |
VARCHAR |
String |
|
BINARY |
byte[] |
|
BINARY |
String |
バイナリ文字列を16進数でエンコードします。 4 |
BINARY |
InputStream |
BINARY 値をバイトのシーケンスとして公開します。 |
BOOLEAN |
boolean |
nullにはできません。 |
BOOLEAN |
Boolean |
|
BOOLEAN |
String |
|
DATE |
java.sql.Date |
|
DATE |
String |
日付を |
TIME |
java.sql.Time |
|
TIME |
String |
時刻を |
TIMESTAMP_LTZ |
java.sql.Timestamp |
java.sql.Timestampの範囲内に収まる必要があります。 3 |
TIMESTAMP_LTZ |
String |
|
TIMESTAMP_NTZ |
java.sql.Timestamp |
java.sql.Timestampの範囲内に収まる必要があります。実時間をUnixエポックからのオフセットとして扱います(事実上、 UTCタイムゾーンを強制)。 3 |
TIMESTAMP_NTZ |
String |
実時間をUnixエポックからのオフセットとして扱います(事実上、 UTCタイムゾーンを強制)。出力形式は |
TIMESTAMP_TZ |
java.sql.Timestamp |
java.sql.Timestampの範囲内に収まる必要があります。 3 |
TIMESTAMP_TZ |
String |
|
VARIANT |
バリアント データ型は、Snowparkパッケージのクラスです。詳細については、 Snowparkパッケージからのサポートされている型 をご参照ください。例については、 VARIANT 値のインラインJava UDF への引き渡し をご参照ください。 |
|
OBJECT |
Map<文字列, 文字列> |
マップのキーはオブジェクトのキーであり、値は文字列としてフォーマットされます。 |
OBJECT |
String |
オブジェクトを JSON 文字列(例: |
ARRAY |
String[] |
配列の要素を文字列としてフォーマットします。 |
ARRAY |
String |
配列を JSON 文字列(例: |
GEOGRAPHY |
String |
地理を GeoJSON としてフォーマットします。 |
GEOGRAPHY |
- 1(1,2)
この形式は、 タイムスタンプ形式 で説明されているインターネット(RFC)タイムスタンプ形式
DY, DD MON YYYY HH24:MI:SS TZHTZM
と一致します。タイムゾーンオフセット(TZHTZM
コンポーネント)が存在する場合、通常は数字です(例:-0700
は UTC からマイナス7時間であることをを示す)。タイムゾーンオフセットが数字ではなくZ
(「Zulu」の頭文字)の場合、それは「+0000」(UTC)と同義です。- 2
この形式は、 タイムスタンプ形式 で説明されているインターネット(RFC)タイムスタンプ形式
DY, DD MON YYYY HH24:MI:SS
と一致します。文字列の後にスペースとZ
(「Zulu」の頭文字)が続く場合、オフセットが「+0000」(UTC)であることを明示的に示します。- 3(1,2,3,4,5,6,7,8)
Snowflakeはナノ秒の精度で時間値を保存できますが、java.sql.timeライブラリはミリ秒の精度しか維持できません。SnowflakeとJavaの間でデータ型を変換すると、実効精度がミリ秒に低下する可能性があります。
- 4(1,2,3,4,5,6)
この型マッピングは、 SQL 引数をJavaに変換する場合はサポートされますが、Javaの戻り型を SQL 型に変換する場合はサポートされません。
- 5
JavaにはネイティブのGeographyデータ型がありません。ここで参照されている Geography データ型は、Snowparkパッケージのクラスです。詳細については、 Snowparkパッケージからのサポートされている型 をご参照ください。
Snowparkパッケージからのサポートされている型¶
ユーザー定義関数では、Snowflake SnowparkJavaパッケージ に含まれている型の特定のサブセットを使用できます。これらの型はSnowparkコードで使用するように設計されていますが、 UDFs での使用をサポートしてこれらが提供する利便性も提供します。(Snowparkの詳細については、 Snowparkドキュメント をご参照ください。)
次のテーブルのSnowpark型は、 UDF コードでサポートされています。他のSnowpark型はサポートされていないため、 UDF コードでは使用しないでください。
Snowpark型 |
Snowparkバージョンが必要 |
説明 |
---|---|---|
1.2.0以降 |
Snowflake GEOGRAPHY 型を表します。 |
|
1.4.0以降 |
Snowflake VARIANT データを表します。 |
UDF 依存関係としてのSnowparkパッケージの指定¶
Snowparkパッケージを使用する UDF コードを開発するときは、Snowparkの依存関係を使用してコードをコンパイルおよび実行できるように、開発環境をセットアップする必要があります。詳細については、 Snowpark Java用その他開発環境の設定 をご参照ください。
CREATE FUNCTION ステートメントを実行して UDF を展開する場合は、 JAR ファイルをステージにアップロードせずにSnowparkパッケージを依存関係として指定できます(ライブラリはすでにSnowflakeにあります)。これを実行するには、 PACKAGES
句でパッケージ名とバージョンを指定します。構文例については、 GEOGRAPHY 値のインラインJava UDF への引き渡し をご参照ください。
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)
以下は包括的な例です。
テーブルを作成してロードします。
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 | +---------------------+
Varargs経由の引き渡し¶
Varargsの使用は、配列の使用と非常によく似ています。
Javaコードでは、Javaのvarargs宣言スタイルを使用します。
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は UDFs に次の制限を設けています。特に明記されていない限り、これらの制限は、 UDF が作成されたときではなく、実行されたときに適用されます。
メモリ¶
あまり多くのメモリを消費しないようにしてください。
大きなデータ値(通常は BINARY、長い VARCHAR、または大きな ARRAY または OBJECT または VARIANT データ型)は、大量のメモリを消費する可能性があります。
スタックが深すぎると、大量のメモリを消費する可能性があります。Snowflakeでは、ネストされた50レベルの単純な関数呼び出しをエラーなしでテストしました。実際の上限は、スタックに配置される情報の量によって異なります。
UDFs がメモリを消費しすぎると、エラーを返します。特定の制限は変更される場合があります。
時間¶
呼び出しごとに長い時間がかかるアルゴリズムは避けてください。
UDF の完了に時間がかかりすぎると、Snowflakeは SQL ステートメントを強制終了し、ユーザーにエラーを返します。これにより、無限ループなどのエラーの影響およびコストが制限されます。
ライブラリ¶
Javaメソッドは標準のJavaライブラリのクラスとメソッドを使用できますが、Snowflakeのセキュリティ制限により、ファイルへの書き込みなど、一部の機能が無効になります。ライブラリの制限の詳細については、 優れたセキュリティプラクティスに準拠 というタイトルのセクションをご参照ください。
クラスの設計¶
SQL ステートメントがJava UDF を呼び出すと、Snowflakeは作成したJavaメソッドを呼び出します。Javaメソッドは、「ハンドラーメソッド」または略して「ハンドラー」と呼ばれます。
他のJavaメソッドと同様に、メソッドはクラスの一部として宣言する必要があります。ハンドラーメソッドは、クラスの静的メソッドまたはインスタンスメソッドにすることができます。ハンドラーがインスタンスメソッドであり、クラスが引数なしのコンストラクターを定義している場合、Snowflakeは初期化時にコンストラクターを呼び出して、クラスのインスタンスを作成します。ハンドラーが静的メソッドの場合、クラスにコンストラクターは必要ありません。
ハンドラーは、Java UDF に渡される行ごとに1回呼び出されます。(注: クラスの新しいインスタンスは行ごとに作成されません。Snowflakeは、同じインスタンスのハンドラーメソッドを複数回呼び出すことも、同じ静的メソッドを複数回呼び出すこともできます。)
コードの実行を最適化するために、Snowflakeは、ハンドラーメソッドの実行が速い一方で、初期化が遅い可能性があると想定しています。Snowflakeは、初期化を実行するためのタイムアウト(UDF をロードする時間と、コンストラクターが定義されている場合は、ハンドラーメソッドを含むクラスのコンストラクターを呼び出すための時間を含む)を、ハンドラーを実行するためのタイムアウト(入力1行でハンドラーを呼び出す時間)よりも長く設定します。
クラスの設計に関する追加情報は Java UDFs の作成 にあります。
スカラー UDFs における初期化の最適化とグローバル状態の制御¶
ほとんどのスカラー UDFs は、以下のガイドラインに従う必要があります。
行間で変化しない共有状態を初期化する必要がある場合は、 UDF クラスのコンストラクターで初期化します。
スレッドセーフになるようにハンドラーメソッドを記述します。
行間で動的状態を保存および共有することは避けてください。
UDF がこれらのガイドラインに従えない場合、またはこれらのガイドラインの理由をより深く理解したい場合は、次のいくつかのサブセクションをお読みください。
紹介¶
Snowflakeは、スカラー UDFs が独立して処理されることを期待しています。システムは任意の順序で行を処理し、それらの呼び出しを複数の JVMs に分散できるため、呼び出し間で共有される状態に依存すると、予期しない動作が発生する可能性があります。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;
同じ引数が渡された場合でも複数の呼び出しで独立した値を返すようにし、関数 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 ファイルとハンドラーをポイントします。これらの呼び出しは、同じクラスのインスタンスを2つ作成します。各インスタンスは独立した値を返すため、以下の例では、同じ値を2回返すのではなく、2つの独立した値を返します。
select my_java_udf_1(), my_java_udf_2() from table1;
JVM 状態情報の保存¶
動的共有状態に依存しない理由の1つは、行が必ずしも予測可能な順序で処理されるとは限らないことです。Snowflakeは、 SQL ステートメントが実行されるたびに、バッチの数、バッチが処理される順序、およびバッチ内の行の順序を変更できます。1つの行が後続の行の戻り値に影響を与えるようにスカラー UDF が設計されている場合、 UDF は、 UDF が実行されるたびに異なる結果を返す可能性があります。
エラーの処理¶
UDF として使用されるJavaメソッドは、通常のJava例外処理手法を使用して、メソッド内のエラーをキャッチできます。
メソッド内で例外が発生し、メソッドによってキャッチされない場合、Snowflakeは例外のスタックトレースを含むエラーを発生させます。
クエリを終了して SQL エラーを生成するために、例外をキャッチせずにスローできます。例:
if (x < 0) {
throw new IllegalArgumentException("x must be non-negative.");
}
デバッグ時に、 SQL エラーメッセージテキストに値を含めることができます。そうするには、Javaメソッドの本文全体をtry-catchブロックに配置し、キャッチしたエラーのメッセージに引数値を追加し、拡張メッセージで例外をスローします。機密データが公開されないようにするには、 JAR ファイルを実稼働環境に展開する前に引数値を削除します。
ベストプラクティスに準拠¶
プラットフォームに依存しないコードを記述します。
特定の CPU アーキテクチャ(例: x86)を想定したコードは避けます。
特定のオペレーティングシステムを想定したコードは避けます。
初期化コードを実行する必要があり、呼び出すメソッドに含めたくない場合は、初期化コードを静的初期化ブロックに入れることができます。
こちらもご参照ください。
優れたセキュリティプラクティスに準拠¶
メソッド(および呼び出すライブラリメソッド)は純粋関数として機能する必要があり、受信したデータのみに作用し、そのデータに基づいて、副作用を引き起こすことなく値を返します。コードは、妥当な量のメモリとプロセッサ時間を消費することを除いて、基になるシステムの状態に影響を与える可能性がないようにします。
Java UDFs は、制限されたエンジン内で実行されます。使用するコードも、呼び出すライブラリメソッド内のコードも、次のような禁止されているシステムコールを呼び出さないようにしてください。
プロセス制御。たとえば、プロセスをフォークすることはできません。(ただし、複数のスレッドは使用可。)
ファイルシステムへのアクセス。
次の例外を除いて、Java UDFs はファイルを読み取ったり、書き込んだりすべきではありません。
Java UDFs は、
CREATE FUNCTION
コマンドのimports
句で指定されたファイルを読み取ることができます。詳細については、 CREATE FUNCTION をご参照ください。Java UDFs は、ログファイルなどのファイルを
/tmp
ディレクトリに書き込むことができます。各クエリは、独自の/tmpが保存されている独自のメモリバックアップファイルシステムを取得するため、異なるクエリでファイル名が競合することはありません。
ただし、1つのクエリが複数の UDF を呼び出し、それらの UDFs が同じファイル名に書き込もうとすると、クエリ内で競合が発生する可能性があります。
ネットワークアクセス。
注釈
コードは、ネットワークに直接的または間接的にアクセスできないため、Snowflake JDBC ドライバーのコードを使用してデータベースにアクセスすることはできません。UDF 自体がSnowflakeのクライアントとして機能することはできません。
JNI (Java Native Interface)はサポートされていません。Snowflakeは、(Javaバイトコードではなく)ネイティブコードを含むライブラリのロードを禁止しています。
政府リージョン 内で使用される場合、Java UDFs は、連邦情報処理標準(140-2)(FIPS 140-2)要件を満たすように検証された暗号化アルゴリズムをサポートしています。Java用の BouncyCastle 暗号化 API の FIPS 承認済みモードで許可されている暗号化アルゴリズムのみを使用できます。FIPS 140-2の詳細については、 FIPS 140-2 をご参照ください。