User-defined table-valued function (UDTF) menerima nol atau lebih nilai skalar sebagai input dan menghasilkan beberapa baris sebagai output. Setiap baris yang dihasilkan dapat memiliki satu atau lebih kolom. Hal ini berbeda dari fungsi skalar, yang memetakan input ke satu nilai output tunggal.
Flink membedakan beberapa jenis fungsi:
| Jenis fungsi | Perilaku |
|---|---|
| Fungsi skalar | Memetakan nilai skalar ke nilai skalar baru |
| Table-valued function (UDTF) | Memetakan nilai skalar ke baris-baris baru |
| Fungsi agregat | Memetakan nilai skalar dari beberapa baris ke nilai skalar baru |
| Fungsi agregat tabel | Memetakan nilai skalar dari beberapa baris ke baris-baris baru |
Gunakan UDTF ketika logika Anda harus menghasilkan beberapa baris output dari satu baris input—misalnya, memisahkan string yang dibatasi menjadi baris-baris terpisah.
Untuk latar belakang tentang user-defined functions Flink, lihat User-defined functions.
Prasyarat
Sebelum memulai, pastikan Anda telah:
-
Apache Maven terinstal di mesin lokal Anda
-
IntelliJ IDEA terinstal di mesin lokal Anda
-
Lingkungan pekerjaan Flink SQL telah disiapkan di Realtime Compute for Apache Flink
Kembangkan UDTF
Realtime Compute for Apache Flink menyediakan proyek contoh (ASI_UDX_Demo) dengan lingkungan pengembangan yang telah dikonfigurasi sebelumnya serta implementasi contoh untuk user-defined scalar functions, user-defined aggregate functions (UDAFs), dan UDTF. Akses ke repositori GitHub pihak ketiga ini kadang-kadang bisa lambat atau tidak tersedia.
-
Unduh dan ekstrak proyek contoh ASI_UDX_Demo ke mesin lokal Anda. Setelah diekstraksi, folder
ASI_UDX-maindibuat dengan file-file berikut:-
pom.xml: File konfigurasi proyek Maven, yang menjelaskan koordinat proyek, dependensi, dan pengaturan build. -
\ASI_UDX-main\src\main\java\ASI_UDF\ASI_UDTF.java: Kode Java contoh untuk UDTF.
-
-
Di IntelliJ IDEA, klik File > Open dan buka folder
ASI_UDX-mainyang telah diekstraksi. -
Buka
\ASI_UDX-main\src\main\java\ASI_UDF\ASI_UDTF.javadan implementasikan logika UDTF Anda. UDTF memperluasTableFunction<T>, di manaTadalah tipe output. Metodeeval()harus mengembalikanvoid—alih-alih pernyataanreturn, panggilcollect()untuk mengeluarkan baris output. Ini berbeda dari fungsi skalar, yang menggunakanreturn. Contoh berikut membagi string menjadi dua kolom menggunakan tanda batang vertikal (|) sebagai pembatas:package ASI_UDTF; import org.apache.flink.api.java.tuple.Tuple2; import org.apache.flink.table.functions.TableFunction; public class ASI_UDTF extends TableFunction<Tuple2<String, String>> { public void eval(String str) { String[] split = str.split("\\|"); String name = split[0]; String place = split[1]; // emit one row with two columns collect(Tuple2.of(name, place)); } }Mendeklarasikan tipe output Flink mendukung dua pendekatan untuk mendeklarasikan skema output UDTF:
-
Tipe Row dengan `@FunctionHint` dan `@DataTypeHint` (direkomendasikan): Mendeklarasikan secara eksplisit nama dan tipe bidang, sehingga skema output menjadi jelas bagi Flink dan pembaca.
@FunctionHint(output = @DataTypeHint("ROW<word STRING, length INT>")) public static class SplitFunction extends TableFunction<Row> { public void eval(String str) { for (String s : str.split(" ")) { // use collect(...) to emit a row collect(Row.of(s, s.length())); } } } -
Tipe Tuple (tipe diinferensi dari generics): Lebih sederhana untuk ditulis, tetapi nama kolom tidak eksplisit.
TableFunction<Tuple2<String, Integer>>
CatatanTipe data dan mekanisme inferensi tipe yang didukung oleh
TableFunctionbervariasi antar versi utama Flink. Contoh di atas mengikuti dokumentasi Flink 1.15 (Data types dan Type inference). Periksa dokumentasi Flink yang sesuai dengan versi Ververica Runtime (VVR) Anda. Untuk mengetahui versi Flink Anda, lihat Storage management and operations. -
-
Buka
pom.xmldi folderASI_UDX-maindan konfigurasikan dependensi Anda. Proyek contoh mencakup dependensi Maven berikut untuk Flink 1.11:<dependencies> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-streaming-java_2.12</artifactId> <version>1.11.0</version> <!--<scope>provided</scope>--> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-table</artifactId> <version>1.11.0</version> <type>pom</type> <!--<scope>provided</scope>--> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-core</artifactId> <version>1.11.0</version> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-table-common</artifactId> <version>1.11.0</version> </dependency> </dependencies>Jika UDTF Anda tidak memiliki dependensi tambahan selain yang tercantum, lanjutkan ke langkah berikutnya. Jika tidak, tambahkan entri dependensi yang diperlukan ke
pom.xml. -
Di direktori yang berisi
pom.xml, jalankan perintah berikut untuk mengemas proyek:mvn package -Dcheckstyle.skipJika build berhasil,
ASI_UDX-1.0-SNAPSHOT.jardihasilkan di direktori\ASI_UDX-main\target\. Pengembangan UDTF selesai.
Asynchronous table functions (AsyncTableFunction)
Asynchronous table function (AsyncTableFunction) mengakses layanan eksternal secara asinkron dalam Flink SQL atau Table API dan mengembalikan nol, satu, atau beberapa baris untuk setiap pemanggilan. Ini merupakan jenis user-defined table-valued function (UDTF). Skenario khas meliputi mengkueri beberapa tag berdasarkan ID produk atau mengembalikan beberapa catatan yang sesuai untuk suatu kondisi pencarian. Asynchronous table function dapat berupa user-defined function yang dipanggil oleh LATERAL TABLE dalam SQL atau joinLateral dalam Table API untuk membentuk operasi correlate asinkron. Fungsi ini juga dapat disediakan oleh konektor tabel dimensi sebagai implementasi kueri untuk asynchronous lookup join.
Parameter konfigurasi
Parameter berikut mengontrol perilaku runtime asynchronous table functions.
| Parameter | Skenario | Tujuan |
|---|---|---|
table.exec.async-table.max-concurrent-operations |
Async Correlate | Mengontrol jumlah permintaan konkuren dari fungsi tabel asinkron. |
table.exec.async-table.timeout |
Async Correlate | Menetapkan total waktu tunggu pemanggilan, termasuk retry yang diaktifkan. |
table.exec.async-table.retry-strategy |
Async Correlate | Menetapkan strategi retry untuk pemanggilan fungsi. |
table.exec.async-lookup.buffer-capacity |
Async Lookup Join | Mengontrol kapasitas buffer untuk lookup asinkron. |
table.exec.async-lookup.timeout |
Async Lookup Join | Menetapkan timeout untuk lookup tabel dimensi asinkron. Petunjuk LOOKUP yang berlaku juga dapat menentukan timeout untuk satu join. |
Contoh pengembangan
Saat mengimplementasikan AsyncTableFunction, deklarasikan metode eval publik yang non-static. Parameter pertama adalah CompletableFuture<Collection<T>> yang digunakan untuk mengirimkan hasil, dan parameter sisanya adalah parameter bisnis yang diteruskan oleh pemanggilan SQL.
Fungsi segera mengembalikan kontrol setelah mengirim permintaan asinkron, dan mengirimkan hasil dalam callback penyelesaian dengan memanggil result.complete(rows), atau mengirimkan exception dengan memanggil result.completeExceptionally(error). Beberapa baris dari satu pemanggilan dikirimkan sebagai satu koleksi. Kirim koleksi kosong jika tidak ada hasil.
package com.example.udf;
import org.apache.flink.table.functions.AsyncTableFunction;
import org.apache.flink.table.annotation.DataTypeHint;
import org.apache.flink.table.annotation.FunctionHint;
import org.apache.flink.types.Row;
import org.apache.flink.table.functions.FunctionContext;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
/**
* Queries product tags through an asynchronous HTTP interface.
*
* <p>Interface contract: 200 returns UTF-8 text with one tag per line; 204 and 404 indicate no tags.
* This example suits read-only queries that return short text. Other status codes are treated as invocation failures and handed over to the engine.
*/
@FunctionHint(output = @DataTypeHint("ROW<tag STRING>"))
public class AsyncProductTags extends AsyncTableFunction<Row> {
private transient HttpClient client;
private transient URI serviceBaseUri;
@Override
public void open(FunctionContext context) {
String baseUrl = context.getJobParameter("product.service.base-url", null);
if (baseUrl == null || baseUrl.isBlank()) {
throw new IllegalArgumentException("Configure product.service.base-url.");
}
serviceBaseUri = URI.create(baseUrl);
if (!("https".equalsIgnoreCase(serviceBaseUri.getScheme())
|| "http".equalsIgnoreCase(serviceBaseUri.getScheme()))
|| serviceBaseUri.getHost() == null
|| serviceBaseUri.getRawQuery() != null
|| serviceBaseUri.getRawFragment() != null
|| !baseUrl.endsWith("/")) {
throw new IllegalArgumentException("The service URL must be an HTTP(S) base URL that ends with a forward slash (/).");
}
client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build();
}
@Override
public boolean isDeterministic() {
// Product information may change. The same ID may return different results at different times.
return false;
}
public void eval(CompletableFuture<Collection<Row>> result, Long productId) {
if (productId == null) {
result.complete(Collections.emptyList());
return;
}
try {
HttpRequest request =
HttpRequest.newBuilder(
serviceBaseUri.resolve("products/" + productId + "/tags"))
.timeout(Duration.ofSeconds(5))
.header("Accept", "text/plain")
.GET()
.build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))
.thenApply(
response -> {
if (response.statusCode() == 200) {
return response.body().lines()
.map(String::strip)
.filter(tag -> !tag.isEmpty())
.map(tag -> Row.of(tag))
.collect(Collectors.toList());
}
if (response.statusCode() == 204 || response.statusCode() == 404) {
return Collections.<Row>emptyList();
}
throw new CompletionException(
new IOException(
"Failed to query the product. HTTP status code: " + response.statusCode()));
})
.whenComplete(
(rows, error) -> {
if (error != null) {
result.completeExceptionally(error);
} else {
result.complete(rows);
}
});
} catch (RuntimeException e) {
result.completeExceptionally(e);
}
}
}
HttpClient.sendAsync melakukan permintaan jaringan secara asinkron, sehingga fungsi tidak perlu membuat kolam thread sendiri. Callback hanya mengurai teks pendek dan mengirimkan hasil. Saat menggunakan klien lain, lebih baik gunakan juga antarmuka asinkron aslinya. Untuk perilaku klien JDK, lihat HttpClient API.
Tag produk dapat berubah seiring waktu, sehingga contoh ini menetapkan isDeterministic() ke false. Untuk fungsi yang selalu mengembalikan hasil yang sama untuk input yang sama, pertahankan perilaku default.
Penanganan timeout kustom
Secara default, jika pemanggilan eval tidak menyelesaikan CompletableFuture sebelum timeout berakhir dan retry yang dikonfigurasi telah habis, framework akan melemparkan java.util.concurrent.TimeoutException dan gagal menjalankan pekerjaan. Anda dapat mendeklarasikan metode timeout(...) yang sesuai dengan metode eval dalam subclass AsyncTableFunction untuk mengembalikan baris fallback atau mengimplementasikan penanganan timeout kustom.
Timeout cenderung lebih sering terjadi saat memanggil kluster LLM remote, dan timeout biasanya tidak boleh langsung menyebabkan pekerjaan gagal. Dengan metode timeout yang dideklarasikan, hasil fallback dikembalikan saat timeout dan pekerjaan dilanjutkan.
Metode timeout harus memenuhi semua kondisi berikut:
-
Deklarasi metode: Metode harus berupa metode instans
public, tidak bolehstatic, dan harus bernamatimeout. -
Signature selaras dengan
eval: Daftar parameter harus benar-benar identik dengan metodeevalyang sesuai. Parameter pertama adalahCompletableFutureyang tipe generiknya harus persis sama denganeval, dan tipe serta urutan parameter sisanya juga harus persis sama.timeoutmendukung overloading: Anda dapat mendeklarasikan metodetimeoutterpisah untuk setiap overloadevalyang memerlukan logika fallback. -
Penyelesaian sinkron (wajib): Callback ini berjalan pada thread mailbox operator dan harus menyelesaikan future sebelum metode mengembalikan kontrol. Segera setelah metode mengembalikan kontrol, framework memeriksa
future.isDone(). Jika future belum selesai, framework akan memaksanya diselesaikan denganIllegalStateException. Oleh karena itu:-
Jangan memulai pemanggilan asinkron lain dalam isi metode, seperti retry lain atau kueri kedua, dan mengharapkan callback-nya menyelesaikan future. Pada saat callback dipicu, framework sudah menyerah pada catatan tersebut.
-
Jangan memulai thread baru untuk menyelesaikan future secara asinkron, karena alasan yang sama.
-
Isi metode hanya melakukan satu hal: memberikan hasil fallback ringan, seperti baris konstan, baris NULL, atau koleksi kosong, atau memanggil
completeExceptionally(...)dengan exception bisnis kustom.
-
-
Melemparkan exception aman: Melemparkan exception secara sinkron dalam isi metode diperbolehkan. Framework meneruskan exception tersebut ke
ResultFuturedownstream, yang setara dengan memanggil secara eksplisitfuture.completeExceptionally(thrown).
Di luar batasan ini, framework juga menjamin perilaku stabil dalam skenario berikut:
-
Tidak ada metode
timeoutyang dideklarasikan:TimeoutExceptiondefault dari framework digunakan, dan tidak terjadi kegagalan selama pembuatan kode. -
Signature tidak kompatibel: Jika metode
timeoutmemiliki visibilitas yang valid tetapi daftar parameternya tidak dapat sesuai dengan tipe argumen aktual di lokasi pemanggilan, framework akan gagal selama perencanaan (pembuatan kode) denganValidationException. Pesan error berisi nama kelas lengkap fungsi dan menunjukkan signature yang diharapkan dan yang aktual. -
Koleksi kosong sebagai fallback: Saat
future.complete(Collections.emptyList())dipanggil, baris tersebut dibuang untuk INNER lookup join, dan bidang tabel kanan diisi dengan NULL untuk LEFT OUTER lookup join.
Contoh berikut menambahkan callback timeout ke asynchronous table function dan menunjukkan cara menyelesaikan future secara sinkron serta mengembalikan baris fallback:
import org.apache.flink.table.functions.AsyncTableFunction;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
public static class BackgroundFunctionWithTimeout extends AsyncTableFunction<Long> {
public void eval(CompletableFuture<Collection<Long>> future, Integer waitMax) {
// ...same logic as the preceding development example: dispatch the asynchronous task,
// and then complete `future` in the callback thread.
}
// The parameter list must align with eval(): the same CompletableFuture generic type,
// and the same types and order for the remaining parameters (Integer waitMax).
// The method body must complete `future` synchronously. Do not start another asynchronous call
// or a new thread to complete it asynchronously. When this callback is triggered,
// the operator has already given up on this record.
public void timeout(CompletableFuture<Collection<Long>> future, Integer waitMax) {
future.complete(Collections.singletonList(-1L));
// You can also use a custom business exception instead of the default TimeoutException:
// future.completeExceptionally(new RuntimeException(waitMax));
}
}
Daftarkan UDTF
Untuk petunjuk cara mendaftarkan UDTF, lihat Manage user-defined functions (UDFs).
Gunakan UDTF
Setelah mendaftarkan UDTF, gunakan dalam pekerjaan Flink SQL.
-
Kembangkan pekerjaan Flink SQL. Untuk informasi lebih lanjut, lihat Job development map. SQL berikut membagi bidang
messagediASI_UDTF_Sourcemenjadi dua kolom menggunakan pembatas|, lalu memasukkan hasilnya keASI_UDTF_Sink. SintaksLATERAL TABLEmelakukan cross-join setiap baris input dengan baris-baris yang dihasilkan oleh UDTF.CREATE TEMPORARY TABLE ASI_UDTF_Source ( `message` VARCHAR ) WITH ( 'connector' = 'datagen' ); CREATE TEMPORARY TABLE ASI_UDTF_Sink ( name VARCHAR, place VARCHAR ) WITH ( 'connector' = 'blackhole' ); INSERT INTO ASI_UDTF_Sink SELECT name, place FROM ASI_UDTF_Source, LATERAL TABLE(ASI_UDTF(`message`)) AS T(name, place); -
Di halaman Operation Center > Job O&M, temukan pekerjaan target dan klik Start di kolom Actions. Setelah pekerjaan dimulai, string di bidang
messagedariASI_UDTF_Sourcedibagi oleh pembatas|, dan kolom hasilnya dimasukkan keASI_UDTF_Sink.