SparkSQL獲取dataframe(Java)

寫在前面

使用SparkSQL讀取數據庫數據並返回dataframe,感覺都要被各種示例寫爛了,本文大體上是沒有新意的,只不過加了些細節,對需要的人的而言還是比較重要的。

此外,示例方法均是使用Java編寫,爲什麼不用Scala呢,實在是語法糖對於我這樣的水平最多隻到泛型爲止的人而言,過於抽象了,過了一個月就不太記得之前寫的是啥了,還是習慣明確對象。

正文

介紹下幾塊細節吧。

  1. 驅動使用getDriver方法獲取,注意其中的hbase指的是phoenix而不是原生HBase。

  2. 使用sql語句獲取數據並進行處理。答案就在dbtable參數中傳入sql 語句並且sql語句需要包裹一層,並且另起一個別名作爲表名。

option("dbtable", "(select * from table) wangleai")
  1. 在使用oracle的時候,可以使用sessionInitStatement參數在會話創建之後讀取數據之前,執行自定義sql語句,一般用於修改會話的相關配置。注意此參數只有在2.3版本以上纔有。官網鏈接
    至於我代碼裏寫的sql的作用,那是用來改變日期類型和時間戳類型的默認時間格式,這樣在sql語句裏就可以直接這麼寫了(用於增量是巨好用的)。
option("oracle.jdbc.mapDateToTimestamp", "false").option("sessionInitStatement", initSql)
select * from table where datefield > '2019-11-01 00:00:00'
  1. 這個是重頭戲了,那就是分區字段!如果需要獲取並處理超大規模的數據時,建議一定要有分區字段,用於partitionColumn, numPartitions,lowerBound, upperBound這四個選項。
    partitionColumn字段目前僅支持數字、日期或者時間戳,用於Spark程序使用此字段內容將數據分成多個區塊去執行。numPartitions決定了要分成多少個區域,lowerBound和upperBound分別使用partitionColumn的上屆和下屆,配合numPartitions決定每個區域使用哪些數據。
    注意的是,partitionColumn, lowerBound, upperBound三者必須同時出現。而且使用分區字段時,查詢的sql語句也需要加入分區字段。
    (代碼示例中使用的是一個自增長的數字類型ID作爲分區字段)
option("lowerBound", minNum).option("upperBound", maxNum).option("numPartitions", numPartitions + "").option("partitionColumn", partitionField)
  1. 然後就是代碼了:
    public static void main(String[] args) {
        //自己設置Spark配置O!
        SparkConf conf = new SparkConf().setAppName("wangleai");
        SparkSession spark = SparkSession.builder().config(conf).getOrCreate();
        Map<String, String> jdbcConfig = new HashMap<>(8);
        //數據庫類型
        jdbcConfig.put("database", "mysql");
        //連接字符串
        jdbcConfig.put("url", "jdbc:mysql://localhost:3306/mydb");
        //用戶名
        jdbcConfig.put("username", "username");
        //密碼
        jdbcConfig.put("password", "password");
        //查詢sql,在Spark中本人比較習慣處理sql的查詢結果
        jdbcConfig.put("sql", "Select * from mytable");
        //分區字段,這個比較重要,特別是數據量大的時候
        jdbcConfig.put("partitionfield", "");
        Dataset<Row> jdbcDf = getJdbcDf(jdbcConfig, spark);
        jdbcDf.show();
        jdbcDf.foreachPartition(partition -> {
            while (partition.hasNext()) {
                Row row = partition.next();
                //對每一行做任何你想做的處理
                System.out.println(row);
            }
        });
    }

    /**
     * 獲取jdbc df
     *
     * @param jdbcConfig 配置
     * @param spark      會話
     * @return
     */
    private static Dataset<Row> getJdbcDf(Map<String, String> jdbcConfig, SparkSession spark) {
        String dbType = jdbcConfig.get("database");
        String url = jdbcConfig.get("url");
        String userName = jdbcConfig.get("username");
        String password = jdbcConfig.get("password");
        //直接使用表名
        //String exeSql = jdbcConfig.get("sql");
        //使用sql查詢語句
        String exeSql = String.format("(%s) wangleai", jdbcConfig.get("sql"));
        String partitionField = jdbcConfig.get("partitionfield");
        String driver = getDriver(dbType);
        DataFrameReader dataFrameReader = spark.read()
                .format("jdbc")
                .option("url", url)
                .option("user", userName)
                .option("password", password)
                .option("fetchsize", 200)
                .option("driver", driver);
        if ("oracle".equalsIgnoreCase(dbType)) {
            //修改oracle會話默認時間格式
            String initSql = "BEGIN " +
                    "EXECUTE IMMEDIATE 'ALTER SESSION SET NLS_DATE_FORMAT=\"YYYY-MM-DD HH24:MI:SS\"';" +
                    "EXECUTE IMMEDIATE 'ALTER SESSION SET NLS_TIMESTAMP_FORMAT=\"YYYY-MM-DD HH24:MI:SS\"';" +
                    "END;";
            //此參數需要保證Spark版本大於2.3,從而在獲取數據前修改會話的一些配置
            dataFrameReader.option("oracle.jdbc.mapDateToTimestamp", "false").option("sessionInitStatement", initSql);
        }

        Dataset<Row> jdbcDf;
        if (!"".equals(partitionField)) {
            // 採用分區讀取數據
            exeSql = exeSql.replaceAll("(?i)from", "," + partitionField + " from");
            // 每批數量
            int minNum = 1;
            int maxNum = 100000000;
            int pageNum = 10000 * 10;
            long numPartitions = (maxNum - minNum) / pageNum + 1;
            jdbcDf = dataFrameReader.option("dbtable", exeSql)
                    .option("lowerBound", minNum)
                    .option("upperBound", maxNum)
                    .option("numPartitions", numPartitions + "")
                    .option("partitionColumn", partitionField).load();
        } else {
            jdbcDf = dataFrameReader.option("dbtable", exeSql).load();
        }
        return jdbcDf;
    }

    /**
     * 獲取驅動類
     *
     * @param dataBase 數據庫類型
     * @return
     */
    private static String getDriver(String dataBase) {
        String driver = "";
        switch (dataBase.toLowerCase()) {
            case "hive":
                driver = "org.apache.hive.jdbc.HiveDriver";
                break;
            case "hbase":
                driver = "org.apache.phoenix.jdbc.PhoenixDriver";
                break;
            case "postgresql":
                driver = "org.postgresql.Driver";
                break;
            case "kylin":
                driver = "org.apache.kylin.jdbc.Driver";
                break;
            case "mysql":
                driver = "com.mysql.jdbc.Driver";
                break;
            case "oracle":
                driver = "oracle.jdbc.driver.OracleDriver";
                break;
            case "sqlserver":
                driver = "com.microsoft.sqlserver.jdbc.SQLServerDriver";
                break;
            default:
                System.out.println("暫不支持的數據庫類型:" + dataBase);
                break;
        }
        return driver;
    }

寫在後面

原本是準備1024寫的,但是工作之後時間真的過得實在是太快了,哎,老了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章