Hive 简单udf入门--自然周差异计算

  Hive sql与我们普通使用的sql基本差异不大,但在大数据领域往往存在很多未知的需求,所以往往都有一个支持自定义功能函数编写的口子,让用户实现其特定的需求。(这往往并非hive独有,几乎都是标配)

  而要写udf往往也是比较简单,看几个例子,依葫芦画瓢总能搞几个。

  今天我们就来简单写一个“自然周差异计算”week_diff函数吧。

 

1. pom依赖

  依赖是环境必备。实际上,hive udf 分为几种类型,我们本文就来看看最简单的一种实现, 继承 UDF 类。

  pom.xml 必备依赖:

<dependency>
    <groupId>org.apache.hivegroupId>
    <artifactId>hive-execartifactId>
    <version>1.2.1version>
dependency>
<dependency>
    <groupId>org.apache.hadoopgroupId>
    <artifactId>hadoop-commonartifactId>
    <version>2.7.3version>
dependency>

  以上依赖,也就是一些接口定义,以及必备环境的类库引入,然后你就可以进行编写自己的UDF了。

 

2. 编写UDF实现

  这是用户要做的一件事也是唯一件可做的事,本篇是实现 UDF 功能。 UDF 是hive中一对一关系的函数调用,即给一个输入,给出一个输出。样例如下:

import com.y.udf.exception.UdfDataException;
import org.apache.hadoop.hive.ql.exec.Description;
import org.apache.hadoop.hive.ql.exec.UDF;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Date;

/**
 * 功能描述: 自然周偏移计算函数
 *          

周偏移计算

*
*/ @Description(name = "week_diff", value = "_FUNC_(week_diff(date dayForJudge [, date dateReferer]) - Returns day1 与 day2 的自然周差异数, 如 -3, -1, 0, n... \n" + "_FUNC_(week_diff('2020-07-30')) - Returns 0 \n" + "_FUNC_(week_diff('2020-01-01', '2020-01-08 10:00:01')) - Returns -1 \n" + "_FUNC_(week_diff(to_date(from_unixtime(UNIX_TIMESTAMP('2020-01-01','yyyy-MM-dd'))), current_date))") public class WeekDiffUdf extends UDF { /** * 一天的毫秒数常量 */ private static final long ONE_DAY_MILLIS = 3600_000 * 24; /** * 日期格式定义 */ private final DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); /** * 与当前日期为依据,计算日期偏移 (结果一般都为 -xx) * * @param weekDayForCompare 要比较的日期格式 * @return 周差异(-1, 0, n...) */ public int evaluate(String weekDayForCompare) { if(weekDayForCompare.length() < 10) { throw new UdfDataException("要比较的日期 day1 数据格式错误, 请确认是否为 yyyy-MM-dd 格式"); } weekDayForCompare = weekDayForCompare.substring(0, 10); LocalDate day1 = LocalDate.parse(weekDayForCompare, dayFormatter); return evaluate(day1, LocalDate.now()); } /** * 日期格式入参调用计算周偏移 */ public int evaluate(Date weekDayForCompare) { LocalDate day1 = weekDayForCompare.toInstant() .atZone(ZoneOffset.ofHours(8)).toLocalDate(); return evaluate(day1, LocalDate.now()); } /** * 两个日期比较周差异 (string -> string) * * @param weekDayForCompare 被比较的日期 * @param weekDayRef 参照日期 * @return day1与day2 的周差异 * @throws UdfDataException 格式错误时抛出 */ public int evaluate(String weekDayForCompare, String weekDayRef) throws Exception { if(weekDayForCompare.length() < 10) { throw new UdfDataException("要比较的日期 day1 数据格式错误, 请确认是否为 yyyy-MM-dd 格式"); } if(weekDayRef.length() < 10) { throw new UdfDataException("参考日期 day2 数据格式错误, 请确认是否为 yyyy-MM-dd 格式"); } weekDayForCompare = weekDayForCompare.substring(0, 10); weekDayRef = weekDayRef.substring(0, 10); LocalDate day1 = LocalDate.parse(weekDayForCompare, dayFormatter); LocalDate day2 = LocalDate.parse(weekDayRef); return evaluate(day1, day2); } /** * 两个日期比较周差异 (date -> date) */ public int evaluate(Date weekDayForCompare, Date weekDayRef) { LocalDate day1 = weekDayForCompare.toInstant() .atZone(ZoneOffset.ofHours(8)).toLocalDate(); LocalDate day2 = weekDayRef.toInstant() .atZone(ZoneOffset.ofHours(8)).toLocalDate(); long day1WeekFirstTimestamp = getDayOfWeekFirstTimestamp(day1); long day2WeekFirstTimestamp = getDayOfWeekFirstTimestamp(day2); // 计算周差异算法很简单,就是获取日期所在周的第一天的时间戳相减,然后除以周单位即可得到周差异 long diffWeeks = (day1WeekFirstTimestamp - day2WeekFirstTimestamp) / (ONE_DAY_MILLIS * 7); return (int) diffWeeks; } public int evaluate(LocalDate day1, LocalDate day2) { long day1WeekFirstTimestamp = getDayOfWeekFirstTimestamp(day1); long day2WeekFirstTimestamp = getDayOfWeekFirstTimestamp(day2); long diffWeeks = (day1WeekFirstTimestamp - day2WeekFirstTimestamp) / (ONE_DAY_MILLIS * 7); return (int) diffWeeks; } /** * 获取指定日期所在自然周的 第一天的时间戳 (周一为第1天) * localDate 的周起始时间计算 * * @param day 指定日期 * @return 1434543543 时间戳 * @see #getDayOfWeekFirstTimestamp(LocalDate) */ private long getDayOfWeekFirstTimestamp(LocalDate day) { DayOfWeek dayOfWeek = day.getDayOfWeek(); // 以周一为起始点 日_周 偏移, 周一: 2, 周三: 4, SUNDAY=7,MONDAY=1 int realOffsetFromMonday = dayOfWeek.getValue() - 1; return day.atStartOfDay(ZoneOffset.ofHours(8)).toInstant().toEpochMilli() - realOffsetFromMonday * ONE_DAY_MILLIS; } }

  从上面可以看出,我们写了n个 evaluate() 方法,而这些方法都是可能被hive作为函数入口调用的,我们可以简单认为就是evaluate的重载函数。所以,不需要向外暴露的方法,就不要命名为 evaluate了。上面实现了这么多,主要就是考虑外部可能传入的不同数据类型,做的适配工作。可以适当推测,hive是通过硬反射调用 udf 的。

  可以看到,具体的函数实现比较简单,因为我们的需求就在那儿。倒也不是想炫技什么的,主要是hive也不会支持你这种需求,所以简单也还得自己来。

 

3. 编写udf单元测试

  这准确的说,是java基础知识,但这里的单元测试远比我们在hive进行函数测试来得容易,所以是有必要的。

import org.junit.Assert;
import org.junit.Test;

import java.text.SimpleDateFormat;

/**
 * 功能描述: 周函数单元测试
 *
 */
public class WeekDiffUdfTest {

    @Test
    public void testEvaluate() throws Exception {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        WeekDiffUdf udf = new WeekDiffUdf();
        int weekOffset;

        // 单入参函数测试
        String day1 = "2020-07-18";
        weekOffset = udf.evaluate(format.parse(day1));
        Assert.assertTrue("__FUNC__(string)周偏移计算错误",
                weekOffset <= -2);
        int weekOffset2 = udf.evaluate(day1);
        Assert.assertEquals("__FUNC__(string) != __FUNC__(date)",
                        weekOffset, weekOffset2);

        day1 = "2020-08-02";
        weekOffset = udf.evaluate(format.parse(day1));
        Assert.assertTrue("__FUNC__(string)周末边界偏移计算错误",
                weekOffset <= 0);

        day1 = "2020-07-27";
        weekOffset = udf.evaluate(format.parse(day1));
        Assert.assertTrue("__FUNC__(string)周一边界偏移计算错误",
                weekOffset <= 0);


        // 两个函数参数入参测试
        day1 = "2020-08-02";
        String day2 = "2020-07-25 10:09:01";
        weekOffset = udf.evaluate(day1, day2);
        Assert.assertEquals("__FUNC__(string, string)周偏移计算错误",
                            1, weekOffset);

        day1 = "2020-07-27";
        day2 = "2020-07-30 10:00:01";
        weekOffset = udf.evaluate(day1, day2);
        Assert.assertEquals("__FUNC__(string, string)周偏移计算错误",
                        0, weekOffset);

        day1 = "2020-07-27";
        day2 = "2020-08-02";
        weekOffset = udf.evaluate(format.parse(day1), format.parse(day2));
        Assert.assertEquals("__FUNC__(date, date)周一周末偏移计算错误",
                        0, weekOffset);

        day1 = "2019-12-30";
        day2 = "2020-01-02";
        weekOffset = udf.evaluate(day1, day2);
        Assert.assertEquals("__FUNC__(string, string)跨年周偏移计算错误",
                            0, weekOffset);

        day1 = "2019-12-20";
        day2 = "2020-01-01";
        weekOffset = udf.evaluate(day1, day2);
        Assert.assertEquals("__FUNC__(string, string)跨年周偏移计算错误",
                            -2, weekOffset);
        System.out.println("ok。offset:" + weekOffset);
    }
}

  测试通过,核心功能无误,可以准备打包发布hive环境了。当然是打jar包了。

 

4. 注册udf并测试

  将前面打好的包放到hive环境可触达的地方,运行加载命令!

add jar /home/hadoop/WeekDiffUdf.jar

  运行hive测试用命:(即相当于将前面的单元测试,翻译成sql在hive中进行测试)

# 创建临时函数,以便进行测试
create temporary function week_diff as "com.y.udf.WeekDiffUdf";    
select week_diff('2020-07-29') = 0 from default.dual;
select week_diff('2020-07-20') = -1 from default.dual;
select week_diff('2020-01-01', '2020-01-08 10:00:01') = -1 from default.dual;
select week_diff('2020-01-01', '2019-12-30 10:00:01') = 1 from default.dual;
select week_diff(to_date(from_unixtime(UNIX_TIMESTAMP('2020-07-28',"yyyy-MM-dd")))) = 0  from default.dual;
select week_diff(to_date(from_unixtime(UNIX_TIMESTAMP('2020-07-28',"yyyy-MM-dd"))), current_date) = 0 from default.dual;
# hive 外部会解析好字段值,再代入计算的
select my_date,week_diff(my_date) from default.account_for_test;

  如上结果,你应该会得到n个true返回值,否则单测不通过。最后一个sql只是为了验证实际运行时被代入变量的情况,意义不大。

  运行单测完成后,功能就算完成了。我们可以正式发布了,进行永久注册!如下:

CREATE FUNCTION week_diff AS 'com.y.udf.WeekDiffUdf' USING JAR 'hdfs://hadoop001:9000/lib/hadoop/WeekDiffUdf.jar';

  如此,一个自然周偏移函数udf 就完成了,你就可以像使用hive通用sql也一样去写业务了。

  可以通过 show functions; 查看已经注册了的函数列表。

  要删除已经注册的函数:

drop temporary function week_diff;
drop function week_diff;

 

你可能感兴趣的:(Hive 简单udf入门--自然周差异计算)