如何编写udaf函数

1.需求背景

最近遇到一个需求,表“result_ABC”中存在字段“source_type”(字段值来源) 及 字段“create_date”(创建日期),需要按照source_type的不同来源获取对应的create_date。
一般这种需求sql中case when 即可搞定,但是由于数据结果集来源于多个系统的同步清洗,业务逻辑中存在多个相同的source_type及create_date,且要求该技术实现逻辑在cdh环境及tdh环境同时兼容(tdh环境对部分sql内置函数不兼容)。经过验证分析,最终考虑使用udaf函数实现逻辑。

2.分析过程

2.1 基本分析

字段值来源分別为: 0代表上一日历史数据、 1代表当天全量数据、 2代表当天新增的数据,表“result_ABC”为临时表,由三段结果集union all组成(暂且命名resultA、resultB、resultC),resultA为上一日数据结果集、resultB为当天全量数据集、resultC为当天增量数据集。

以resultA(上一日数据结果集)为例,其中source_type取值时赋值为‘0’,create_date取值为上一日清洗日期‘20220225’。由于resultA对应的数据源表datasourceA来源于多个系统,每个系统有独立主键,因此resultA(上一日数据结果集)中存在多个相同的source_type =‘0’及多个相同的create_date=‘20220225’。同理,对于resultB(当天全量数据集)中存在多个相同的source_type =‘1’及多个相同的create_date=‘20220226’,对于resultC(当天增量数据集)中存在多个相同的source_type =‘2’及多个相同的create_date=‘20220226’。

总结起来就是该需求存在多种数据场景,即入参为多个相同的source_type(值为‘0’、‘1’、‘2’)及create_date(如‘20220225’、‘20220226’),出参只能有一个create_date。

2.2 取值逻辑

1)条件1:存在上一日create_date且create_date <> 0 时,优先获取上一日create_date作为出参;
2)优先按条件1获取,如果不满足,那么按条件2获取:存在当日create_date时,获取当日create_date作为出参;
3)如果条件1 和条件2 都不满足,那么对create_date赋值为‘0’作为出参

2.3 实现方案

2.3.1 方案1:case when语句

1)简化逻辑

  select f.id as id,
         max(case when instr(concat_ws(',', collect_set(f.source_type)), '0') > 0 then
                (if(f.source_type = '0', f.create_date, 0))
               else
                (if(f.source_type = '1', f.create_date, 0))
             end) as create_date
    from result_ABC f
   group by f.id

说明:以上sql在tdh环境执行报错,日志提示不支持该语法形式的聚合逻辑。

2)TDH环境兼容语句

  select ff.id as id, max(ff.create_date) as create_date
    from (select t1.id,
                 case when t1.source_type_arr > 0 then
                    (if(t2.source_type = '0', t2.create_date, 0))
                   else
                    (if(t2.source_type = '1', t2.create_date, 0))
                 end as create_date
            from (select f.id as id,
                         instr(concat_ws(',', collect_set(f.source_type)),'0') as source_type_arr
                    from result_ABC f
                   group by f.id) t1
            left join (select f.id          as id,
                             f.source_type as source_type,
                             f.create_date as create_date
                        from result_ABC f) t2
              on t1.id = t2.id) ff
   group by ff.id

说明:以上sql语法在TDH环境执行成功。

3)方案1总结

虽然sql语句使用简单且很多内置函数可以直接使用,但是以上sql不便于对业务的直观理解。同时为了实现业务逻辑对表“result_ABC”进行自关联的处理方式,影响执行性能。

2.3.2 方案2:udaf函数

这里不准备直接上udaf代码,具体代码在后面会详细补充,要搞清楚udaf函数,先了解下几个基本概念。

1)udaf函数定义

UDAF (user defined aggregation function) 用户自定义聚合函数,多行记录汇总成一行,常用于聚合函数。

2)基本认识

一般udaf函数比较抽象,对比udf及udtf函数,难度最大。udaf函数中涉及java专业知识,如数据结构、继承、多态等模块,且类名英文定义单词较长,首次接触时确实不好理解。
对恐惧事物的认知总会有一个过程,一次看不懂就两次,再不会就三次…对于编程学习而言,都是从学习到了解、从了解到模仿、从模仿到理解、从理解到掌握,坚持就是胜利。尝试自己写一遍,边写边理解,这样才会掌握到位。

3)开发步骤

1)自定义UDAF类,需要继承AbstractGenericUDAFResolver, 定义一个UDAF的主体;
2)自定义Evaluator类,需要继承GenericUDAFEvaluator,真正实现UDAF的逻辑;
3)自定义bean类,需要继承 AbstractAggregationBuffer,用于在mapper或reducer内部传递数据;

4)主要实现方法

1)init() :初始化方法 主要用于定义数据类型
2)reset() :内存重用的方法
3)iterate() :map迭代方法
4)terminatePartial() :map的输出方法
5)merge() :合并方法
6)terminate() :reduce最终输出方法

//一个阶段调用一次
public  ObjectInspector init(Mode m, ObjectInspector[] parameters) throws HiveException;  
  
// 返回存储临时聚合结果的AggregationBuffer对象 
abstract AggregationBuffer getNewAggregationBuffer() throws HiveException;  
  
// mapreduce支持mapper和reducer的重用,所以为了兼容,也需要做内存的重用。  
public void reset(AggregationBuffer agg) throws HiveException;  
  
// map阶段,迭代处理原始数据parameters并保存到agg中
// 一行调用一次
public void iterate(AggregationBuffer agg, Object[] parameters) throws HiveException;  
  
// map与combiner结束返回结果,以持久化的方式返回agg表示的部分聚合结果
public Object terminatePartial(AggregationBuffer agg) throws HiveException;  
  
// combiner合并map返回的结果,还有reducer合并mapper或combiner返回的结果,合并由partial表示的部分聚合结果到agg中。  
// 当数据量达到一定程度,会调用多次
public void merge(AggregationBuffer agg, Object partial) throws HiveException;  
  
// reducer阶段,输出最终结果  
public Object terminate(AggregationBuffer agg) throws HiveException;  

3.udaf函数

3.1 对象字段封装

public class CreateDateInfo {

    /**
     * 字段值来源,0:上一日历史数据 1:当天全量数据 2:当天新增的数据
     */
    private String sourceType;

    /**
     * 创建日期
     */
    private int createDate;

    public String getSourceType() {
        return sourceType;
    }

    public void setSourceType(String sourceType) {
        this.sourceType = sourceType;
    }

    public int getCreateDate() {
        return createDate;
    }

    public void setCreateDate(int createDate) {
        this.createDate = createDate;
    }
}

3.2 udaf函数代码

package com.bigdata.udf.function.udaf;

import com.alibaba.fastjson.JSON;
import com.bigdata.udf.entity.CreateDateInfo;
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.parse.SemanticException;
import org.apache.hadoop.hive.ql.udf.generic.AbstractGenericUDAFResolver;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDAFEvaluator;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory;
import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector;
import org.apache.hadoop.hive.serde2.typeinfo.TypeInfo;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

/**
 * @Author Allan
 * @Date 2022/2/56 16:43
 * @Version 1.0
 * @Desc 按照source_type获取对应create_date
 */
public class GetCreateDateArrange extends AbstractGenericUDAFResolver {

    @Override
    public GenericUDAFEvaluator getEvaluator(TypeInfo[] info) throws SemanticException {
        return new GetCreateDateEvaluator();
    }

    public static class GetCreateDateEvaluator extends GenericUDAFEvaluator {
        protected PrimitiveObjectInspector inputOI;
        protected ObjectInspector outputOI;
        protected PrimitiveObjectInspector stringOI;
        private static final String LAST_DAY_DATA_TYPE = "0";    // 0:代表上一日历史数据
        private static final String CURRENT_DAY_DATA_TYPE = "1"; // 1:代表当天全量数据
        private static final int CREATE_DATE_DEFAULT_VALUE = 0;  // 创建日期默认值

        @Override
        public ObjectInspector init(Mode m, ObjectInspector[] parameters) throws HiveException {
            super.init(m, parameters);
            // map阶段读取SQL列,输入为String基础数据格式
            if (m == Mode.PARTIAL1 || m == Mode.COMPLETE) {
                inputOI = (PrimitiveObjectInspector) parameters[0];
            }
            stringOI = (PrimitiveObjectInspector) ObjectInspectorFactory.getReflectionObjectInspector(String.class,
                    ObjectInspectorFactory.ObjectInspectorOptions.JAVA);
            // 指定各个阶段输出数据格式都为String类型
            outputOI = ObjectInspectorFactory.getReflectionObjectInspector(String.class,
                    ObjectInspectorFactory.ObjectInspectorOptions.JAVA);
            return outputOI;
        }

        /**
         * 缓存当前数据
         */
        static class GetCreateDateInfoAgg implements AggregationBuffer {

            List<CreateDateInfo> sinfo = new ArrayList<>(128);
            
            private void put(CreateDateInfo dateInfo) {
                sinfo.add(dateInfo);
            }

            private void putAll(List<CreateDateInfo> dateInfoList) {
                sinfo.addAll(dateInfoList);
            }

            private void clear() {
                this.sinfo.clear();
            }
        }

        @Override
        public AggregationBuffer getNewAggregationBuffer() throws HiveException {
            GetCreateDateInfoAgg infoAgg = new GetCreateDateInfoAgg();
            return infoAgg;
        }

        @Override
        public void reset(AggregationBuffer aggregationBuffer) throws HiveException {
            GetCreateDateInfoAgg infoAgg = (GetCreateDateInfoAgg) aggregationBuffer;
            infoAgg.clear();

        }

        @Override
        public void iterate(AggregationBuffer aggregationBuffer, Object[] objects) throws HiveException {
            // "字段值来源"名称 sourceType
            final int index0 = 0;
            // "创建日期"名称 createDate
            final int index1 = 1;

            GetCreateDateInfoAgg infoAgg = (GetCreateDateInfoAgg) aggregationBuffer;
            CreateDateInfo dateInfo = new CreateDateInfo();

            // 按顺序传入入参
            if (objects[index0] != null) {
                dateInfo.setSourceType((String) stringOI.getPrimitiveJavaObject(objects[0]));
            }
            if (objects[index1] != null) {
                dateInfo.setCreateDate(Integer.parseInt((String) stringOI.getPrimitiveJavaObject(objects[1])));
            }

            infoAgg.put(dateInfo);

        }

        @Override
        public Object terminatePartial(AggregationBuffer aggregationBuffer) throws HiveException {
            GetCreateDateInfoAgg infoAgg = (GetCreateDateInfoAgg) aggregationBuffer;
            return JSON.toJSONString(infoAgg.sinfo);
        }

        @Override
        public void merge(AggregationBuffer aggregationBuffer, Object object) throws HiveException {
            if (object != null) {
                GetCreateDateInfoAgg infoAgg = (GetCreateDateInfoAgg) aggregationBuffer;
                String infoStr = (String) stringOI.getPrimitiveJavaObject(object);
                List<CreateDateInfo> dateInfoList = JSON.parseArray(infoStr, CreateDateInfo.class);
                infoAgg.putAll(dateInfoList);
            }
        }

        @Override
        public Object terminate(AggregationBuffer aggregationBuffer) throws HiveException {
            GetCreateDateInfoAgg infoAgg = (GetCreateDateInfoAgg) aggregationBuffer;
            List<CreateDateInfo> lists = infoAgg.sinfo;
            // 考虑使用HashSet及HashMap结构对结果数据进行逻辑处理
            HashSet<String> sourceTypeSet = new HashSet<>();
            HashMap<String, Integer> createDateMap = new HashMap<>();
            Integer createDateResult;

            for (CreateDateInfo info : lists) {
                sourceTypeSet.add(info.getSourceType()); // 对sourceType去重,用于后续判断逻辑
                createDateMap.put(info.getSourceType(), info.getCreateDate());
            }

            // 取值逻辑
            createDateResult = (sourceTypeSet.contains(LAST_DAY_DATA_TYPE)
                    && createDateMap.get(LAST_DAY_DATA_TYPE) != CREATE_DATE_DEFAULT_VALUE) ?
                    createDateMap.get(LAST_DAY_DATA_TYPE) : createDateMap.get(CURRENT_DAY_DATA_TYPE);
            // 返回值判空处理
            if (createDateResult == null) {
                return CREATE_DATE_DEFAULT_VALUE;
            }

            return createDateResult;
        }
    }

}

3.3 udaf函数代码总结

1)对象封装
对于表中字段值,作为属性字段进行封装,通过get() 及set() 方法获取对应属性值。
2)数据结构
对于该需求而言,业务逻辑中存在多个相同的source_type及create_date,按照‘‘2.2取值逻辑’”,优先考虑使用HashSet对source_type进行去重处理,使用HashMap对“source_type”(字段值来源) 及 字段“create_date”(创建日期)进行键值对保存。
3)缓存处理
通过继承AggregationBuffer创建ArrayList对象sinfo,封装put()、putAll()及clear()三个方法,便于后续逻辑调用。
4)输入逻辑
在iterate()方法中,入参按照顺序传入,通过创建对象调用封装好的setSourceType()及setCreateDate进行赋值,与缓存对象进行关联。
5)输出逻辑
在terminate()方法中,拿到缓存list对象后,对list进行遍历,按照“2.2取值逻辑”进行逻辑处理。特别注意的是数据场景要考虑充分,注意返回值可能为null 的情况。

4.数据验证

4.1 数据场景1

sourceType 		create_date
‘0’             20220225
‘1’             20220226

返回结果:20220225

4.2 数据场景2

sourceType 		create_date
‘0’             0
‘1’             20220226

返回结果:20220226

4.3 数据场景3

sourceType 		create_date
‘0’             0

返回结果:0(默认值赋值)

4.4 数据场景4

sourceType 		create_date
‘0’             0
‘0’             0
‘1’             20220226
‘1’             20220226
‘2’             20220225

返回结果:20220226

4.5 数据场景5

sourceType 		create_date
‘0’             0
‘0’             0
‘1’             20220226
‘1’             20220226
‘2’             20220226
‘2’             20220226

返回结果:20220226

5 总结

以上就是udaf函数编写的回顾分析,结合个人对业务的理解,具体包含需求说明、方案验证、代码编写及数据验证。希望对大家有所帮助,如有不妥之处,欢迎大家私信我多多交流!

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值