文章目录
flink sql 自定义函数UDF
flink版本:1.13.1
scala版本:2.12
1 maven 依赖引用
<properties>
<flink.version>1.13.1</flink.version>
<scala.version>2.12</scala.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-planner-blink_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-api-java-bridge_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<!-- 实现自定义的数据格式来做序列化,可以引入下面的依赖 -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-common</artifactId>
<version>${flink.version}</version>
</dependency>
</dependencies>
2. 自定义UDF函数
2.1 标量函数 (Scalar Function)
- 自定义标量函数可以把 0 个、 1 个或多个标量值转换成一个标量值,它对应的输入是一
行数据中的字段,输出则是唯一的值。所以从输入和输出表中行数据的对应关系看,标量函数
是“一对一”的转换。 - 想要实现自定义的标量函数,我们需要自定义一个类来继承抽象类 ScalarFunction,并实
现叫作 eval() 的求值方法。标量函数的行为就取决于求值方法的定义,它必须是公有的(public),
而且名字必须是 eval。求值方法 eval 可以重载多次,任何数据类型都可作为求值方法的参数
和返回值类型。 - 这里需要特别说明的是,ScalarFunction 抽象类中并没有定义 eval()方法,所以我们不能直
接在代码中重写(override);但 Table API 的框架底层又要求了求值方法必须名字为 eval()。这
是 Table API 和 SQL 目前还显得不够完善的地方,未来的版本应该会有所改进。
2.1.1 example
import com.flink.dto.Event;
import com.flink.source.ClickSource;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.functions.ScalarFunction;
import java.time.Duration;
import static org.apache.flink.table.api.Expressions.$;
import static org.apache.flink.table.api.Expressions.call;
public class SqlUdfTest {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
SingleOutputStreamOperator<Event> dataStream = env.addSource(new ClickSource())
.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event element, long recordTimestamp) {
return element.getTimestamp();
}
}));
tableEnv.createTemporaryView("table_click", dataStream);
Table tableClick = tableEnv.fromDataStream(dataStream);
// 1. 注册标量函数
tableEnv.createTemporaryFunction("HashFunction", HashFunction.class);
// 2. sql执行标量函数(推荐使用)
Table sqlResult = tableEnv.sqlQuery("select user, HashFunction(url) from table_click");
// 3. table 执行标量函数(不推荐)
Table tableResult = tableClick.select($("user"), call("HashFunction", $("user")));
// 4. 打印输出
//tableEnv.toDataStream(tableResult).print("tableResult");
tableEnv.toDataStream(sqlResult).print("sqlResult");
env.execute();
}
/**
* 自定义UDF标量函数
*/
public static class HashFunction extends ScalarFunction {
// 转换得方法名是eval,必须自己手动写
public String eval(String val) {
return val + "_" + val.hashCode();
}
}
}
输出结果
sqlResult> +I[user_3, /product?id=3_79635664]
sqlResult> +I[user_3, /product?id=3_79635664]
sqlResult> +I[user_4, /product?id=4_79635665]
sqlResult> +I[user_1, /product?id=1_79635662]
sqlResult> +I[user_1, /product?id=1_79635662]
2.2 表函数(Table Function)
- 跟标量函数一样,表函数的输入参数也可以是 0 个、1 个或多个标量值;不同的是,它可以返回任意多行数据。“多行数据”事实上就构成了一个表,所以“表函数”可以认为就是返回一个表的函数,这是一个“一对多”的转换关系。窗口 TVF,本质上就是表函数。
- 类似地,要实现自定义的表函数,需要自定义类来继承抽象类 TableFunction,内部必须要实现的也是一个名为 eval 的求值方法。与标量函数不同的是,TableFunction 类本身是有一个泛型参数T 的,这就是表函数返回数据的类型;而 eval()方法没有返回类型,内部也没有 return语句,是通过调用 collect()方法来发送想要输出的行数据的。也是通过 out.collect()来向下游发送数据的。
- 我们使用表函数,可以对一行数据得到一个表,这和 Hive 中的 UDTF 非常相似。那对于原先输入的整张表来说,又该得到什么呢?一个简单的想法是,就让输入表中的每一行,与它转换得到的表进行联结(join),然后再拼成一个完整的大表,这就相当于对原来的表进行了扩展。在 Hive 的 SQL 语法中,提供了“侧向视图”(lateral view,也叫横向视图)的功能,可以将表中的一行数据拆分成多行;Flink SQL 也有类似的功能,是用 LATERAL TABLE 语法来实现的。
- 在 SQL 中调用表函数,需要使用 LATERAL TABLE()来生成扩展的“侧向表”,然后与原始表进行联结(Join)。这里的 Join 操作可以是直接做交叉联结(cross join),在 FROM 后用逗号分隔两个表就可以;也可以是以 ON TRUE 为条件的左联结(LEFT JOIN)。
2.2.1 example
import com.flink.dto.Event;
import com.flink.source.ClickSource;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.functions.TableFunction;
import java.time.Duration;
public class Udf_tableFunction {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
SingleOutputStreamOperator<Event> dataStream = env.addSource(new ClickSource())
.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event element, long recordTimestamp) {
return element.getTimestamp();
}
}));
tableEnv.createTemporaryView("table_click", dataStream);
Table tableClick = tableEnv.fromDataStream(dataStream);
// 1. 注册函数
tableEnv.createTemporaryFunction("Split", SplitFunction.class);
// 2.查询
Table sqlResult = tableEnv.sqlQuery("select user, url, word, length " +
"from table_click, LATERAL TABLE(Split(url)) AS T(word, length)");
dataStream.print("source");
tableEnv.toDataStream(sqlResult).print("sqlResult");
env.execute();
}
/**
* 创建table Function
*/
public static class SplitFunction extends TableFunction<Tuple2<String, Integer>> {
// 转换得方法名是eval,必须自己手动写,通过调用super.collect(T)输出
public void eval(String val) {
String[] splits = val.split("\\?");
for(String filed: splits) {
super.collect(Tuple2.of(filed, filed.length()));
}
}
}
}
输出
source> Event{user='user_1', url='/product?id=1', timestamp=2022-4-15 16:09:14}
sqlResult> +I[user_1, /product?id=1, /product, 8]
sqlResult> +I[user_1, /product?id=1, id=1, 4]
source> Event{user='user_0', url='/product?id=0', timestamp=2022-4-15 16:09:15}
sqlResult> +I[user_0, /product?id=0, /product, 8]
sqlResult> +I[user_0, /product?id=0, id=0, 4]
source> Event{user='user_0', url='/product?id=0', timestamp=2022-4-15 16:09:16}
sqlResult> +I[user_0, /product?id=0, /product, 8]
sqlResult> +I[user_0, /product?id=0, id=0, 4]
2.3 聚合函数(Aggregate Function)
2.3.1 example
import com.flink.dto.Event;
import com.flink.source.ClickSource;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.functions.AggregateFunction;
import java.time.Duration;
public class UDF_AggFunction {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
SingleOutputStreamOperator<Event> dataStream = env.addSource(new ClickSource())
.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event element, long recordTimestamp) {
return element.getTimestamp();
}
}));
tableEnv.createTemporaryView("table_click", dataStream);
Table tableClick = tableEnv.fromDataStream(dataStream);
// 1. 注册函数
tableEnv.createTemporaryFunction("WeightAgg", WeightAggFunction.class);
// 2.查询
Table sqlResult = tableEnv.sqlQuery("select user, WeightAgg(user,2) " +
"from table_click group by user");
dataStream.print("source");
tableEnv.toChangelogStream(sqlResult).print("sqlResult");
env.execute();
}
public static class WeightAggFunction extends AggregateFunction<String, WeightedAvgAccumulator> {
@Override
public String getValue(WeightedAvgAccumulator acc) {
return String.valueOf(acc.sum);
}
@Override
public WeightedAvgAccumulator createAccumulator() {
return new WeightedAvgAccumulator();
}
// 累加计算方法,每来一行数据都会调用
// 累加器WeightAgg(user,1)与参数2,3意义对应
public void accumulate(WeightedAvgAccumulator acc, String iValue, Integer
iWeight) {
String[] s = iValue.split("_");
acc.sum += Integer.valueOf(s[1]) * iWeight;
}
}
// 累加器类型定义
public static class WeightedAvgAccumulator {
public long sum = 0; // 加权和
}
}