SnowflakeのJava UDFsを試してみた

2021.06.29

こんにちは!DA(データアナリティクス)事業本部 インテグレーション部の大高です。

SnowflakeのPreview機能として、Java UDFsが公開されています。

UDF(ユーザー定義関数)としては、既にSQLやJavaScriptで作成できるUDFがありますが、新たに提供予定のJava UDFsでは、jarファイルを利用したUDFの作成ができるのが面白そうだと思ったので、実際に試してみました。

前提条件

以下を前提としています。

Java UDFsについて

Java UDFsはPreview機能として公開されていますが、2021年06月 現在では下記の制約があります。

  • 利用できる環境はAWSにホスティングされているSnowflakeアカウント

今回はAWSにホスティングされているSnowflakeアカウントを利用します。

Javaの環境について

Javaの環境は下記のとおり、MacにAmazon Correttoを導入しています。

なお、javacを利用するには上記の手順に加えて、以下のようなパスの追加設定が必要です。

PATH=${JAVA_HOME}/bin:${PATH}
$ java --version
openjdk 11.0.3 2019-04-16 LTS
OpenJDK Runtime Environment Corretto-11.0.3.7.1 (build 11.0.3+7-LTS)
OpenJDK 64-Bit Server VM Corretto-11.0.3.7.1 (build 11.0.3+7-LTS, mixed mode)

$ javac --version
javac 11.0.3

その他

Snowflakeの利用には、snowsqlを利用します。

作成するUDFについて

今回は「うるう年」を判定するUDFを作成してみたいと思います。仕様としては「西暦の年数を数値で渡したら、ブール値で結果を返してくれる(うるう年ならTrue)」というような関数とします。

例えば、以下のようなクエリであればTRUEが返ってくる、という感じです。

SELECT is_leap_year(2020);

Jarファイルを用意する

では、早速Javaのコードを作成してJarファイルを用意します。

下記のドキュメントによると「パブリックなクラス」および「パブリックなメソッド」を定義する必要があります。また、staticではないメソッドでも利用可能ですが、その場合は「コンストラクタを定義しない」か、「コンストラクタの引数を0にする」という必要があるそうです。

今回はstaticなメソッドを作成して、これをUDFから利用するようにしたいと思います。

ディレクトリ構成

最終的な構成は以下のようにします。今回はpackageを利用したいので、Manifestファイルも作成します。

.
├── HelloJavaUDF.jar
├── MANIFEST.MF
├── com
│   └── hello
│       └── HelloJavaUDF.class
└── src
    └── com
        └── hello
            └── HelloJavaUDF.java

Javaファイル

Javaのコードは以下のようにします。isLeapYearをUDFでの利用向けに作成しています。

src/com/hello/HelloJavaUDF.java

package com.hello;

public class HelloJavaUDF {

    public static boolean isLeapYear(int year){
        
        // 4で割り切れるない場合はうるう年ではない
        if (year % 4 != 0){
            return false;
        }

        // ただし、4で割り切れても、100で割り切れて400で割り切れない場合はうるう年でない
        if((year % 100 == 0) && (year % 400 != 0)){
            return false;
        }

        return true;
    }

    public static void main(String args[]){
        boolean isLeapYear = HelloJavaUDF.isLeapYear(Integer.valueOf(args[0]));
        System.out.println(isLeapYear ? "うるう年です" : "うるう年じゃないです");
    }
}

試しに動かすと、こんな感じです。

$ java src/com/hello/HelloJavaUDF.java 2020
うるう年です
$ java src/com/hello/HelloJavaUDF.java 2021
うるう年じゃないです

Manifestファイル

Manifestファイルは以下のように定義します。

MANIFEST.MF

Manifest-Version: 1.0
Class-Path: .
Main-Class: com.hello.HelloJavaUDF.class

この段階ではフォルダ構成はこのようになっています。

.
├── MANIFEST.MF
└── src
    └── com
        └── hello
            └── HelloJavaUDF.java

コンパイル

用意ができたので、コンパイルします。

$ javac -d ./ ./src/com/hello/HelloJavaUDF.java

これで、com配下にclassファイルが出力されます。

.
├── HelloJavaUDF.jar
├── MANIFEST.MF
├── com
│   └── hello
│       └── HelloJavaUDF.class
└── src
    └── com
        └── hello
            └── HelloJavaUDF.java

Jarファイルの作成

最後に下記を参考にJarファイルを作成します。

現在の構成に従って、下記コマンドでHelloJavaUDF.jarファイルを作成します。

$ jar cmf MANIFEST.MF ./HelloJavaUDF.jar ./com/hello/HelloJavaUDF.class

これでjarファイルが用意できました。

.
├── HelloJavaUDF.jar
├── MANIFEST.MF
├── com
│   └── hello
│       └── HelloJavaUDF.class
└── src
    └── com
        └── hello
            └── HelloJavaUDF.java

JarファイルをSnowflakeのユーザーステージにアップロードする

jarファイルが用意できたので、Snowflakeへアップロードします。今回はユーザステージにアップロードします。

snowsqlを起動して、PUTコマンドでアップロードします。

$ snowsql
>PUT file://./HelloJavaUDF.jar @~/udf/;
HelloJavaUDF.jar_c.gz(0.00MB): [##########] 100.00% Done (0.074s, 0.01MB/s).    
╒══════════════════╤═════════════════════╤═════════════╤═════════════╤════════════════════╤════════════════════╤══════════╤═════════╕
│ source           │ target              │ source_size │ target_size │ source_compression │ target_compression │ status   │ message │
╞══════════════════╪═════════════════════╪═════════════╪═════════════╪════════════════════╪════════════════════╪══════════╪═════════╡
│ HelloJavaUDF.jar │ HelloJavaUDF.jar.gz │        1073 │         902 │ NONE               │ GZIP               │ UPLOADED │         │
╘══════════════════╧═════════════════════╧═════════════╧═════════════╧════════════════════╧════════════════════╧══════════╧═════════╛
1 Row(s) produced. Time Elapsed: 1.119s

UDFを作成する

jarファイルがアップロードできたのでUDFを作成します。snowsql上で、以下のようにして作成しました。

-- コンテキスト設定
USE DATABASE OOTAKA_SANDBOX_DB;
USE SCHEMA PUBLIC;
USE WAREHOUSE X_SMALL_WH;

-- UDFを登録
CREATE FUNCTION is_leap_year(year NUMERIC(4, 0))
RETURNS BOOLEAN
LANGUAGE java
IMPORTS = ('@~/udf/HelloJavaUDF.jar')
HANDLER = 'com.hello.HelloJavaUDF.isLeapYear'
;

成功すると、以下のように表示されます。USE WAREHOUSEを指定しているのですが、WAREHOUSEを指定して起動できるようにすることで、UDFの作成時に併せて検証をしてくれるようです。

╒═════════════════════════════════════════════╕                                 
│ status                                      │
╞═════════════════════════════════════════════╡
│ Function IS_LEAP_YEAR successfully created. │
╘═════════════════════════════════════════════╛
1 Row(s) produced. Time Elapsed: 3.482s

なお、アクティブなWAREHOUSEが無いと、以下のように表示されます。

╒═════════════════════════════════════════════════════════════════════════════════════════════════════════════╕
│ status                                                                                                      │
╞═════════════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ Function IS_LEAP_YEAR successfully created, but could not be validated since there is no active warehouse. │
╘═════════════════════════════════════════════════════════════════════════════════════════════════════════════╛

作成時に困ったこと

実は最初はjarの作成時にclassファイルを./classes/ディレクトリ以下に配置して、Manifestファイルを以下のように定義していました。

MANIFEST.MF

Manifest-Version: 1.0
Class-Path: ./classes/
Main-Class: com.hello.HelloJavaUDF.class

構成としては以下のような感じです。

.
├── HelloJavaUDF.jar
├── MANIFEST.MF
├── classes
│   └── com
│       └── hello
│           └── HelloJavaUDF.class
└── src
    └── com
        └── hello
            └── HelloJavaUDF.java

この状態でUDFを作成しようとすると、クラスパス上に該当クラスが見つからない旨のエラーが発生していました。私のJavaとManifestファイルの理解が浅いので設定が良くないのかもしれませんが、同じ事象の場合には一旦クラスファイルは階層を掘らずに作成するとうまくいくかもしれません。

100315 (P0000): User Error Report:                                              
Java Stack Trace:
java.lang.ClassNotFoundException: com.hello.HelloJavaUDF
        at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:471)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:589)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
 in function IS_LEAP_YEAR with handler com.hello.HelloJavaUDF.isLeapYear

いざ、UDFを実行する

これで準備が整いましたので、実際にUDFを呼び出してみましょう。

SELECT is_leap_year(2020);
╒════════════════════╕                                                          
│ IS_LEAP_YEAR(2020) │
╞════════════════════╡
│ True               │
╘════════════════════╛
1 Row(s) produced. Time Elapsed: 0.795s

想定どおり、Trueが取得できました!一応、2021年でも試してみましょう。

SELECT is_leap_year(2021);
╒════════════════════╕                                                          
│ IS_LEAP_YEAR(2021) │
╞════════════════════╡
│ False              │
╘════════════════════╛
1 Row(s) produced. Time Elapsed: 0.852s

良さそうですね。

まとめ

以上、SnowflakeのJava UDFを試してみました。

今回はシンプルなJavaのクラスで試してみたのですが、jarファイルを利用できるというのはとても良いなと感じました。ソースコードをローカル側で管理もできますし、jarであれば色々応用も効きそうですね。

どなたかのお役に立てば幸いです。それでは!