1. 程式人生 > 實用技巧 >Hive 簡單udf入門--自然周差異計算

Hive 簡單udf入門--自然周差異計算

  Hive sql與我們普通使用的sql基本差異不大,但在大資料領域往往存在很多未知的需求,所以往往都有一個支援自定義功能函式編寫的口子,讓使用者實現其特定的需求。(這往往並非hive獨有,幾乎都是標配)

  而要寫udf往往也是比較簡單,看幾個例子,依葫蘆畫瓢總能搞幾個。

  今天我們就來簡單寫一個“自然周差異計算”week_diff函式吧。

1. pom依賴

  依賴是環境必備。實際上,hive udf 分為幾種型別,我們本文就來看看最簡單的一種實現, 繼承 UDF 類。

  pom.xml 必備依賴:

<dependency>
    <groupId
>org.apache.hive</groupId> <artifactId>hive-exec</artifactId> <version>1.2.1</version> </dependency> <dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-common</artifactId> <version>2.7.3</version
> </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; /** * 功能描述: 自然周偏移計算函式 * <p>周偏移計算</p> * */ @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;