S-JIS[2025-12-25/2026-01-04]
TsurugiのSQLのユーザー定義関数(UDF)をJavaで作る例。
|
TsurugiのUDFは、関数の処理内容をgRPCサーバーで実装する。
そのgRPCサーバーをJavaで作ってみる。
依存ライブラリーには、gRPCとprotobufを入れる。
plugins {
id 'java'
id 'application'
id 'com.google.protobuf' version '0.9.4'
}
group = 'com.example.tsurugi.udf'
version = '0.1-SNAPSHOT'
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
dependencies {
implementation 'io.grpc:grpc-netty-shaded:1.59.0'
implementation 'io.grpc:grpc-protobuf:1.59.0'
implementation 'io.grpc:grpc-stub:1.59.0'
implementation 'javax.annotation:javax.annotation-api:1.3.2'
implementation 'com.google.protobuf:protobuf-java:3.25.0'
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.25.0'
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.59.0'
}
}
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
}
sourceSets {
main {
java {
srcDirs 'build/generated/source/proto/main/grpc'
srcDirs 'build/generated/source/proto/main/java'
}
}
}
application {
mainClass = 'com.example.tsurugi.udf.UdfExampleServer'
}
task runServer(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
mainClass = 'com.example.tsurugi.udf.UdfExampleServer'
}
TsurugiのUDFはprotoファイルで定義する。
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.example.tsurugi.udf.proto";
message MyFunctionRequest {
int32 value = 1;
}
message MyFunctionResponse {
int32 value = 1;
}
service ExampleService {
rpc my_function(MyFunctionRequest) returns (MyFunctionResponse);
}
TsurugiのUDF用のrpc関数は、引数と戻り値をmessageで定義する必要がある。
Gradleでビルドすると、protoファイルからJavaのソースが生成される。
cd tsurugi-udf-example ./gradlew build
TsurugiのUDFを処理するgRPCサービスとgRPCサーバーを実装する。
package com.example.tsurugi.udf; import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.stub.StreamObserver; import java.io.IOException; import java.util.concurrent.TimeUnit; import com.example.tsurugi.udf.proto.ExampleServiceGrpc; import com.example.tsurugi.udf.proto.MyFunctionRequest; import com.example.tsurugi.udf.proto.MyFunctionResponse;
public class UdfExampleServer {
private static final int PORT = 50051;
public static void main(String[] args) throws IOException, InterruptedException {
var server = new UdfExampleServer();
server.start();
server.blockUntilShutdown();
}
private Server server;
private void start() throws IOException {
server = ServerBuilder.forPort(PORT).addService(new ExampleServiceImpl()).build().start();
System.out.println("Server started, listening on " + PORT);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.err.println("*** shutting down gRPC server since JVM is shutting down");
try {
UdfExampleServer.this.stop();
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
System.err.println("*** server shut down");
}));
}
private void stop() throws InterruptedException {
if (server != null) {
server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
}
}
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
static class ExampleServiceImpl extends ExampleServiceGrpc.ExampleServiceImplBase {
@Override
public void myFunction(MyFunctionRequest request, StreamObserver<MyFunctionResponse> responseObserver) {
System.out.println("Received request with value: " + request.getValue());
// UDFの計算本体
int result = request.getValue() * 2;
var response = MyFunctionResponse.newBuilder()
.setValue(result)
.build();
System.out.println("Sending response with value: " + result);
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
}
src/main/protoのprotoファイルを元に、udf-plugin-builderを使ってTsurugiにUDFを登録する。
TsurugiのDockerイメージのTsurugiを使い、gRPCサーバーがホストにある場合は、登録時にホストのIPアドレスを指定する。
WindowsでDokcer
Desktopを使っているならホスト名をhost.docker.internalにすればいいが、WSL2にインストールされたDockerを使っている場合は、ホストのIPアドレスを直接指定する。
WSL2から見たホストのIPアドレスは、PowerShellのipconfigコマンドで確認できる。
> ipconfig Windows IP 構成 イーサネット アダプター vEthernet (WSL (Hyper-V firewall)): 接続固有の DNS サフィックス . . . . .: リンクローカル IPv6 アドレス. . . . .: fe80::8abc:xxxx:xxxx:xxxx%46 IPv4 アドレス . . . . . . . . . . . .: 17y.yyy.yyy.yyy サブネット マスク . . . . . . . . . .: 255.255.240.0 デフォルト ゲートウェイ . . . . . . .: 〜
udf-plugin-builderを実行してTsurugiに登録する。(--output-dirで指定した場所にファイルが生成される)
~/.local/bin/udf-plugin-builder --proto-file example.proto --grpc-endpoint 17y.yyy.yyy.yyy:50051 --output-dir $TSURUGI_HOME/var/plugins/
接続先ホストとポート番号は、生成されたiniファイルを見れば確認できる。(このファイルを直接編集してもいい)
tsurugi@afcebcbb7f4d:/usr/lib/tsurugi-1.8.0$ cat $TSURUGI_HOME/var/plugins/libexample.ini [udf] enabled=true endpoint=17y.yyy.yyy.yyy:50051 secure=false
$TSURUGI_HOME/var/plugins/に配置したら、Tsurugiを再起動する。
$TSURUGI_HOME/bin/tgctl shutdown $TSURUGI_HOME/bin/tgctl start
cd tsurugi-udf-example ./gradlew runServer
$TSURUGI_HOME/bin/tgsql -c ipc:tsurugi
create table test (foo int primary key); insert into test values(1), (2), (100);
tgsql> select my_function(foo) from test; [@#0: INT] [2] [4] [200] (3 rows)
Tsurugi UDFでは、DECIMALやDATE・TIMSTAMP等をprotoファイルで記述するためのデータ型として、tsurugidb.udfパッケージのmessageを提供している。[2026-01-04]
これらをJavaのgRPCサーバーで扱う例。
syntax = "proto3";
package example.tsurugi.udf;
option java_multiple_files = true;
option java_package = "com.example.tsurugi.udf.proto";
import "tsurugidb/udf/tsurugi_types.proto";
message IncDecimalRequest {
tsurugidb.udf.Decimal value = 1;
}
message IncDecimalResponse {
tsurugidb.udf.Decimal value = 1;
}
service Example2Service {
rpc inc_decimal(IncDecimalRequest) returns (IncDecimalResponse);
}
tsurugidb.udfパッケージのmessageを使用するためには、tsurugi_types.protoをインポートする必要がある。
「tsurugi_types.protoをインポートしたprotoファイル」をコンパイルする際にtsurugi_types.protoファイルが必要になる。
このファイル自体はudf-pluginのソースアーカイブにあるが、gRPCサーバーのビルド時にファイルの場所を指定してやる必要がある。
最も手っ取り早いのは、tsurugi_types.protoをsrc/main/protoの下にコピーしてしまうこと。
(src/main/proto直下にtsurugi_types.protoを置くのではなく、パッケージ名に沿ったディレクトリーを用意する必要がある)
(例)tsurugi-udf-example/src/main/proto/tsurugidb/udf/tsurugi_types.proto
udf-pluginのソースアーカイブの下のファイルをそのまま使いたい場合は、build.gradleでtsurugi_types.protoの場所を指定する。
tsurugi_types.protoがC:/tmp/example/tsurugi-udf-0.1.0/proto/tsurugidb/udf/tsurugi_types.protoにある場合、build.gradleのsourceSetsに以下の様に追加する。
sourceSets {
main {
proto {
srcDir 'C:/tmp/example/tsurugi-udf-0.1.0/proto'
}
java {
srcDirs 'build/generated/source/proto/main/grpc'
srcDirs 'build/generated/source/proto/main/java'
}
}
}
なお、デフォルトでsrc/main/protoはソースディレクトリーに入っているので、自分でbuild.gradleに記述する必要は無い。
(「srcDir 'src/main/proto'」も記述すると、その中にあるprotoファイルが重複エラーになる)
src/main/protoも明示したい場合は、以下のようにする。
proto {
srcDirs = ['src/main/proto', 'C:/tmp/example/tsurugi-udf-0.1.0/proto']
}
※「srcDirs =」がsrcDirsを置き換えるのに対し、「srcDir」はsrcDirsに追加する挙動らしい
package com.example.tsurugi.udf; import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.stub.StreamObserver; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; import java.util.concurrent.TimeUnit; import com.example.tsurugi.udf.proto.Example2ServiceGrpc; import com.example.tsurugi.udf.proto.IncDecimalRequest; import com.example.tsurugi.udf.proto.IncDecimalResponse; import com.example.tsurugi.udf.proto.MyFunctionResponse; import com.google.protobuf.ByteString;
public class UdfExample2Server {
private static final int PORT = 50051;
public static void main(String[] args) throws IOException, InterruptedException {
final UdfExample2Server server = new UdfExample2Server();
server.start();
server.blockUntilShutdown();
}
private Server server;
private void start() throws IOException {
server = ServerBuilder.forPort(PORT).addService(new Example2ServiceImpl()).build().start();
System.out.println("Server started, listening on " + PORT);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.err.println("*** shutting down gRPC server since JVM is shutting down");
try {
UdfExample2Server.this.stop();
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
System.err.println("*** server shut down");
}));
}
private void stop() throws InterruptedException {
if (server != null) {
server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
}
}
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
static class Example2ServiceImpl extends Example2ServiceGrpc.Example2ServiceImplBase {
@Override
public void incDecimal(IncDecimalRequest request, StreamObserver<IncDecimalResponse> responseObserver) {
var value = UdfConvertUtil.toJavaBigDecimal(request.getValue());
var result = value.add(BigDecimal.ONE);
var response = IncDecimalResponse.newBuilder()
.setValue(UdfConvertUtil.toUdfDecimal(result))
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
rpcの関数をServiceImplでオーバーライドするところまでは最初の例と同じ。
問題は、Tsurugi UDFが提供しているmessage(のクラス)とJava標準のクラスとの変換。
(Pythonの場合は変換ライブラリーとしてudf-library/pythonが提供されているのだが、Java用は今のところ無いので、)変換処理は自作する必要がある。
Tsurugi UDFが提供しているmessageのクラスとJava標準のクラスとの変換処理を作ってみた。[2026-01-04]
package com.example.tsurugi.udf;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.ZoneOffset;
import java.util.concurrent.TimeUnit;
import com.google.protobuf.ByteString;
import com.tsurugidb.udf.TsurugiTypes.Date;
import com.tsurugidb.udf.TsurugiTypes.Decimal;
import com.tsurugidb.udf.TsurugiTypes.LocalDatetime;
import com.tsurugidb.udf.TsurugiTypes.LocalTime;
import com.tsurugidb.udf.TsurugiTypes.OffsetDatetime;
public class UdfConvertUtil {
public static BigDecimal toJavaBigDecimal(Decimal value) {
byte[] unscaledValue = value.getUnscaledValue().toByteArray();
int scale = -value.getExponent();
return new BigDecimal(new BigInteger(unscaledValue), scale);
}
public static Decimal toUdfDecimal(BigDecimal value) {
byte[] unscaledValue = value.unscaledValue().toByteArray();
int exponent = -value.scale();
return Decimal.newBuilder()
.setUnscaledValue(ByteString.copyFrom(unscaledValue))
.setExponent(exponent)
.build();
}
public static java.time.LocalDate toJavaDate(Date value) {
int days = value.getDays();
return java.time.LocalDate.ofEpochDay(days);
}
public static Date toUdfDate(java.time.LocalDate value) {
long days = value.toEpochDay();
return Date.newBuilder()
.setDays((int) days)
.build();
}
日付型のクラス名は、UDFがDate、java.timeはLocalDate(UDFの方はLocalが付いていない)。
public static java.time.LocalTime toJavaTime(LocalTime value) {
long nanos = value.getNanos();
return java.time.LocalTime.ofNanoOfDay(nanos);
}
public static LocalTime toUdfTime(java.time.LocalTime value) {
long nanos = value.toNanoOfDay();
return LocalTime.newBuilder()
.setNanos(nanos)
.build();
}
時刻型のクラス名は、UDFとjava.timeの単純名が同一(LocalTime)なので、片方はFQCNで書く必要がある。
(このため、他のデータ型でもjava.timeはFQCNで書くことにした)
public static java.time.LocalDateTime toJavaDateTime(LocalDatetime value) {
long seconds = value.getOffsetSeconds();
int nano = value.getNanoAdjustment();
return java.time.LocalDateTime.ofEpochSecond(seconds, nano, ZoneOffset.UTC);
}
public static LocalDatetime toUdfDateTime(java.time.LocalDateTime value) {
long seconds = value.toEpochSecond(ZoneOffset.UTC);
int nano = value.getNano();
return LocalDatetime.newBuilder()
.setOffsetSeconds(seconds)
.setNanoAdjustment(nano)
.build();
}
日時型のクラス名は、UDFはDatetime(tが小文字)、java.timeはDateTime(Tが大文字)なので、間違えやすい^^;
public static java.time.OffsetDateTime toJavaOffsetDateTime(OffsetDatetime value) {
long seconds = value.getOffsetSeconds();
int nano = value.getNanoAdjustment();
int offset = value.getTimeZoneOffset();
var ldt = java.time.LocalDateTime.ofEpochSecond(seconds, nano, ZoneOffset.UTC);
var zone = ZoneOffset.ofTotalSeconds((int) TimeUnit.MINUTES.toSeconds(offset));
return java.time.OffsetDateTime.of(ldt, zone);
}
public static OffsetDatetime toUdfOffsetDateTime(java.time.OffsetDateTime value) {
var ldt = value.toLocalDateTime();
long seconds = ldt.toEpochSecond(ZoneOffset.UTC);
int nano = ldt.getNano();
var zone = value.getOffset();
return OffsetDatetime.newBuilder()
.setOffsetSeconds(seconds)
.setNanoAdjustment(nano)
.setTimeZoneOffset((int) TimeUnit.SECONDS.toMinutes(zone.getTotalSeconds()))
.build();
}
}