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