零基础学Flink:UDF

在上一篇 文章 中我们介绍了一些 Flink SQL 的基础内容,以及与 Spark SQL 对比,有兴趣的小伙伴可以点连接进去看看。这篇文章,我们来说说UDF(User-Defined Functions)——用户自定义函数。


其实,关于UDF这部分官方文档就写的挺好的,简单明了,而且配有DEMO,有兴趣的同学,可以到 参考文档 里去找到连接。


首先,如果想使用自定义函数,那么必须在之前来注册这个函数,使用TableEnvironment的registerFunction()方法来注册。注册之后自定义函数会被插入到TableEnvironment的函数目录中,以便API或SQL正确解析并执行它。在 Flink 中,UDF分为三类:标量函数(ScalarFunction)、表函数(TableFunction) 、聚合函数(AggregateFunction)。


标量函数(ScalarFunction)


简单的说,标量函数,就是你输入几个数(0个或几个都行),经过一系列的处理,再返回给你几个数,这个案例咱们还使用上一篇文章中使用的意甲射手榜的案例,一般来说,总进球数=主场进球数+客场进球数,但是今年的规则有变,客场进球按两个球计算(本文案例和前文有区别,使用scala,大家注意一下)


import org.apache.flink.table.functions.ScalarFunction

class TotalScores extends ScalarFunction{
private var wight:Int = 1 ;
def this(wight:Int){
this()
this.wight = wight
}

def eval(home:Int,visit:Int): Int = home+visit*this.wight
}


首先,需要继承ScalarFunction该类,这里我们添加了一个构造器,传入的参数作为客场进球权重,然后实现eval方法,输入参数为主客场进球数,输出则为总进球数。


接下来,我们来写测试类:

import org.apache.flink.api.scala.ExecutionEnvironment
import org.apache.flink.api.scala.typeutils.Types
import org.apache.flink.table.api.TableEnvironment
import org.apache.flink.table.sources.CsvTableSource
import org.apache.flink.types.Row
import org.apache.flink.api.scala._

object TestScalarFunction {
def main(args: Array[String]): Unit = {
val filePath = "E:\\devlop\\workspace\\streaming1\\src\\main\\resources\\testdata.csv"
val env = ExecutionEnvironment.getExecutionEnvironment
val tableEnv = TableEnvironment.getTableEnvironment(env)
val csvtable = CsvTableSource
.builder
.path(filePath)
.ignoreFirstLine
.fieldDelimiter(",")
.field("rank", Types.INT)
.field("player", Types.STRING)
.field("club", Types.STRING)
.field("matches", Types.INT)
.field("red_card", Types.INT)
.field("total_score", Types.INT)
.field("total_score_home", Types.INT)
.field("total_score_visit", Types.INT)
.field("pass", Types.INT)
.field("shot", Types.INT)
.build
tableEnv.registerTableSource("goals", csvtable)
tableEnv.registerFunction("ts",new TotalScores(2))

val tableTest = tableEnv.sqlQuery("select player,total_score_home,total_score_visit,ts(total_score_home,total_score_visit) from goals where total_score > 10")//.scan("test").where("id='5'").select("id,sources,targets")
tableEnv.toDataSet[Row](tableTest).print()

}
}


首先别忘记引用

import org.apache.flink.api.scala._

否则会有奇怪事情发生。


然后,注册函数,默认构造客场进球权重为2

tableEnv.registerFunction("ts",new TotalScores(2))


"select player,total_score_home,total_score_visit,ts(total_score_home,total_score_visit) from goals where total_score > 10"

在SQL中使用函数 ts(total_score_home,total_score_visit) 就这么简单


我们来看下输出:


C-罗纳尔多,5,7,19

夸利亚雷拉,5,5,15

萨帕塔,1,4,9

米利克,0,1,2

皮亚特克,2,0,2

因莫比莱,3,3,9

卡普托,2,4,10




表函数(TableFunction)


简单的说,表函数,就是你输入几个数(0个或几个都行),经过一系列的处理,再返回给你行数,返回的行可以包含一列或是多列值。这里我们使用一套新的数据案例来做一个说明。


假设这是某年四个直辖市四个季度GDP的一张透视表(说到透视表,想了解的同学可以异步到我之前的 文章 去看看)

provice,s1,s2,s3,s4
天津,10,11,13,14
北京,13,16,17,18
重庆,14,12,13,14
上海,15,11,15,17


我们来将这张透视表,还原成一张列表,接下来,我们来看代码

import org.apache.flink.table.functions.TableFunction

class UnPivotFunction(separator: String) extends TableFunction[(String)] {

@scala.annotation.varargs
def eval(strs:String*): Unit = {
strs.foreach(x=>collect(x))
}
}


函数要继承TableFunction,后面泛型需要输入返回列的类型,这里为了方便,我们就使用了字符串。我们计划在查询里面把四个季度的值都输入进来,转换成列表。collect是TableFunction提供的函数,用于添加列,eval方法的参数,可以根据你的需要自行扩展,注意在使用不确定参数值的时候,加上注解@scala.annotation.varargs


接下来,我们来测试一下


import org.apache.flink.api.common.typeinfo.Types
import org.apache.flink.api.scala.{ExecutionEnvironment, _}
import org.apache.flink.table.api.TableEnvironment
import org.apache.flink.table.sources.CsvTableSource
import org.apache.flink.types.Row
import wang.datahub.udf.UnPivotFunction

object TestMyTableFunction2 {
def main(args: Array[String]): Unit = {
val filepath = "E:\\devlop\\workspace\\testsbtflink\\src\\main\\resources\\GDP.csv"
val env = ExecutionEnvironment.getExecutionEnvironment
val tableEnv = TableEnvironment.getTableEnvironment(env)
tableEnv.registerFunction("mtf2", new UnPivotFunction("@"))
val cts = CsvTableSource.builder().ignoreFirstLine()
//provice,s1,s2,s3,s4
.field("provice",Types.STRING)
.field("s1",Types.STRING)
.field("s2",Types.STRING)
.field("s3",Types.STRING)
.field("s4",Types.STRING)
.path(filepath)
.build()

tableEnv.registerTableSource("m",cts)
val tableTest = tableEnv.sqlQuery("select provice,word from m , LATERAL TABLE(mtf2(s1,s2,s3,s4)) as T(word)")
val stream = tableEnv.toDataSet[Row](tableTest)
stream.print()

}
}


在SQL我使用了 JOIN LATERAL ,有兴趣了解的同学,可以看下云栖的文章,我放在参考文档里了。


我们来看下输出结果:


天津,10

天津,11

天津,13

天津,14

北京,13

北京,16

北京,17

北京,18

上海,15

上海,11

上海,15

上海,17

重庆,14

重庆,12

重庆,13

重庆,14


这个案例也许并不是那么恰当,其实,也可以利用到邮件切分等场景,这里算是抛砖引玉把。


聚合函数(AggregateFunction)


关于聚合函数,官方文档上的这张图,就充分的解释了其工作原理,主要计算通过

  • createAccumulator()

  • accumulate()

  • getValue()

这几个方法来完成,首先我们createAccumulator创建累加器,然后调用accumulate累加计算,最后getValue获取值。

640?wx_fmt=png

当然这只是完成了初步工作,

  • retract() 

  • merge() 

  • resetAccumulator()

我们还需要回滚,合并,重置累加器等操作以适应不同的计算场景。


好了,我们的案例,再次来到了大家喜闻乐见的意甲联赛,这次我们统计俱乐部的进球数,还是使用了一个更靠谱的规则,就是给客场进球加了一个权重,然后来计算加权场均进球数


先来创建累加器

class WeightedAvgAccum  {
var sum = 0
var count = 0
}


然后创建计算函数

import java.lang.{Integer => JInteger,String => JString}
import org.apache.flink.table.functions._
class WeightedAvg(iWeight:Int) extends AggregateFunction[JInteger, WeightedAvgAccum] {

override def createAccumulator(): WeightedAvgAccum = {
new WeightedAvgAccum
}
override def getValue(acc: WeightedAvgAccum): JInteger = {
if (acc.count == 0) {
null
} else {
acc.sum / acc.count
}
}
def accumulate(acc: WeightedAvgAccum,club:JString, home: JInteger, visit: JInteger): Unit = {

acc.sum += home + visit * iWeight
acc.count += 1
}
def resetAccumulator(acc: WeightedAvgAccum): Unit = {
acc.count = 0
acc.sum = 0
}
}


接下来,我们来测试一下:


import org.apache.flink.api.scala.typeutils.Types
import org.apache.flink.api.scala.{DataSet, ExecutionEnvironment}
import org.apache.flink.table.api.TableEnvironment
import org.apache.flink.table.sources.CsvTableSource
import org.apache.flink.types.Row
import org.apache.flink.api.scala._
object Testf {
def main(args: Array[String]): Unit = {
val filePath = "E:\\devlop\\workspace\\streaming1\\src\\main\\resources\\testdata.csv"
val env = ExecutionEnvironment.getExecutionEnvironment
val tableEnv = TableEnvironment.getTableEnvironment(env)
val csvtable = CsvTableSource
.builder
.path(filePath)
.ignoreFirstLine
.fieldDelimiter(",")
.field("rank", Types.INT)
.field("player", Types.STRING)
.field("club", Types.STRING)
.field("matches", Types.INT)
.field("red_card", Types.INT)
.field("total_score", Types.INT)
.field("total_score_home", Types.INT)
.field("total_score_visit", Types.INT)
.field("pass", Types.INT)
.field("shot", Types.INT)
.build
tableEnv.registerTableSource("test", csvtable)
tableEnv.registerFunction("myf",new MyFunction("111"))
tableEnv.registerFunction("wag",new WeightedAvg(2))


val tableTest3 = tableEnv.sqlQuery("select club,wag(club,total_score_home,total_score_visit) as ag from test group by club")
tableEnv.toDataSet[Row](tableTest3).print()
}

}



查看下结果:


切沃,2

拉齐奥,3

斯帕尔,1

博洛尼亚,1

国际米兰,3

帕尔马,2

恩波利,2

桑普多利亚,4

那不勒斯,4

都灵,2

AC米兰,3

亚特兰大,5

佛罗伦萨,2

卡利亚里,2

罗马,3

乌迪内斯,2

弗罗西诺内,2

尤文图斯,4

热那亚,3

萨索洛,2


最后(敲黑板),大家在聚合表的案例里,应该发现我使用了Java的基础类型,而不是Scala的数据类型,这是因为在UDF执行过程中,数据的创建,转换以及装箱拆箱都会带来额外的消耗,所以 Flink 官方,其实推荐UDF进来使用Java编写。


UDF其实是一个很神奇的东西,值得我们去探索与研究,下一期写点什么呢?如果您有建议或意见,欢迎与我联系,探讨。 


参考文档:

https://ci.apache.org/projects/flink/flink-docs-stable/dev/table/udfs.html


https://yq.aliyun.com/articles/674345

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 在 Apache Flink 中,你可以通过实现自定义函数(UDF)来扩展 Flink 的功能。 要实现一个 UDF,你需要创建一个类并实现 `org.apache.flink.api.common.functions.Function` 接口。例如,以下是一个简单的 UDF 示例,它将输入字符串转换为大写: ``` import org.apache.flink.api.common.functions.Function; public class UpperCase implements Function { public String map(String value) { return value.toUpperCase(); } } ``` 接下来,你可以在 Flink 程序中使用 `UpperCase` UDF,方法是将它传递给 `DataStream` 或 `Table` 的 `map` 方法。 例如,以下是如何使用 `UpperCase` UDF 的示例: ``` DataStream<String> input = ...; DataStream<String> output = input.map(new UpperCase()); ``` 你还可以使用 `map` 方法的多参数形式,以便让 UDF 可以使用多个输入参数。这是一个使用两个输入参数的例子: ``` public class Add implements Function { public int map(int a, int b) { return a + b; } } DataStream<Tuple2<Integer, Integer>> input = ...; DataStream<Integer> output = input.map(new Add()); ``` 注意,当使用多个输入参数时,你需要使用 Tuple 类型作为输入类型。 ### 回答2: Flink(Apache Flink)是一个开源的流处理框架,支持高吞吐量和低延迟的大规模数据流处理。UDF(User-Defined Function)是Flink提供的一种机制,用于开发自定义的数据处理逻辑。 编写FlinkUDF有以下步骤: 1. 定义UDF类:首先,需要创建一个类来实现UDF接口或继承提供的抽象类。UDF接口或抽象类包含了需要实现的方法,根据具体需要选择合适的接口或抽象类。 2. 实现方法:在UDF类中,需要实现UDF接口或抽象类中定义的方法。方法根据自定义的数据处理逻辑来编写。例如,如果需要对输入的数据进行某种计算,可以在方法中编写相应的计算代码。 3. 注册UDF:在Flink作业中,需要将自定义的UDF注册到作业的执行环境中。可以使用env.registerFunction()方法来注册UDF。注册时,需要指定UDF的名字、实现类以及接受的参数类型。 4. 使用UDF:在Flink流处理作业中,可以通过调用注册好的UDF来处理数据。可以使用.map()、.flatMap()等操作符来应用UDF,根据需要将UDF应用到流处理的每个元素上。 总结起来,编写FlinkUDF主要包括定义UDF类,实现方法,注册UDF,以及在流处理作业中使用UDF。通过这些步骤,可以将自定义的数据处理逻辑应用于Flink流式计算中,实现个性化的数据处理需求。 ### 回答3: Flink是一个基于流式数据和批处理数据的分布式计算框架,用户可以自定义函数来处理数据。UDF(User Defined Function)是一种用户自定义的函数,可以在Flink中使用。 在Flink中,编写UDF的一般步骤如下: 1. 创建一个类,实现`org.apache.flink.api.common.functions.MapFunction`、`org.apache.flink.api.common.functions.FlatMapFunction`、`org.apache.flink.api.common.functions.FilterFunction`或其他Flink提供的函数接口,根据需求选择合适的接口。 2. 在类中重写相应的方法,根据输入数据的类型和业务需求进行处理,并返回结果。 3. 在Flink程序中使用UDF,可以通过`.map()`、`.flatMap()`、`.filter()`等函数将UDF应用到数据流中的每个元素。 例如,假设我们想在Flink中实现一个UDF来对输入的字符串进行大写转换,可以按照以下方式编写: ```java import org.apache.flink.api.common.functions.MapFunction; public class MyUDF implements MapFunction<String, String> { @Override public String map(String value) throws Exception { return value.toUpperCase(); } } ``` 然后在Flink程序中使用这个UDF: ```java import org.apache.flink.api.java.tuple.Tuple2; import org.apache.flink.streaming.api.datastream.DataStream; import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; public class FlinkUDFExample { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); DataStream<String> input = env.fromElements("hello", "world", "flink"); DataStream<String> output = input.map(new MyUDF()); output.print(); env.execute("Flink UDF Example"); } } ``` 以上代码中,我们创建了一个`MyUDF`类实现了`MapFunction`接口,并在`map()`方法中将输入字符串转换为大写。然后在Flink程序中使用了这个UDF来对输入的数据流进行转换操作。最后通过`print()`函数将转换的结果打印出来。 总结来说,Flink中编写UDF的关键是实现Flink提供的函数接口,并重写相关方法,在Flink程序中使用这些UDF来对数据流进行处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

麒思妙想

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值