sparkSQL[java api]spark Java版本相关的api

spark sql概述

Spark SQL 是 Spark 用于结构化数据(structured data)处理的 Spark 模块。

DataFrame&DataSet

DataFrame、DataSet是spark SQL中新的数据抽象,建立在RDD的基础上,所以它们也是分布式数据集。

DataFrame

DataFrame 是为数据提供了 Schema 的视图,可视作二维表,是在RDD的基础上添加了Schema的信息(包括数据集中的列、名称以及类型)。 DataFrame API提供了高层的关系操作,相对于RDD API要更加友好,降低了开发的成本。

DataSet

DataSet则是spark1.6提供的新抽象,是DataFrame的扩展。DataSet是强类型,用样例类来对DataSet中定义数据的结构信息,样例类属性直接映射到DataSet的字段。 DataFrame 是 DataSet 的特列,两者可以互转。

spark session

SparkSession 是 Spark 最新的 SQL 查询起始点,实质上是 SQLContext 和 HiveContext的组合,SparkSession 内部封装了 SparkContext,计算实际上是由 sparkContext 完成的。

想要使用sparkSQL必须以spark session为起点。

创建spark session

java 复制代码
// 1. 方式一:通过SparkConf创建sparksession
import org.apache.spark.SparkConf;  
import org.apache.spark.sql.*;
SparkConf sparkAppConf = new SparkConf().setMaster("local[*]").setAppName("sparkRddDemo");  
SparkSession sparkSession = SparkSession.builder().config(sparkAppConf).getOrCreate();


// 2. 方式二:SparkSession直接创建sparksession
import org.apache.spark.SparkConf;  
import org.apache.spark.sql.*;
SparkSession sparkSession = SparkSession.builder()  
        .appName("sparkSQLApp")  
        .config("spark.master", "local[2]")  
        .getOrCreate();

sparkSession.stop();  // 停止spark session

DataFrame使用

DataFrame需要通过spark session进行创建,创建方式有三种:

  • 从数据源(文件系统、关系型数据库等)
  • RDD
  • hive table

从文件系统读取数据创建DataFrame

spark session的read()方法会返回一个reader对象(Java api下是DataFrameReader对象),reader对象有一个通用的读取文件的方法load(),除此之外封装了一些特定文件的读取方法:

  • load:泛用性文件读取方法,默认情况下读取parquet文件
  • csv:读取csv文件,默认为逗号分隔
  • json:json文件,可以每行一个json字符串,也可以是json的列表
  • jdbc:读取JDBC
  • orc:读取orc文件
  • textFile:读取文本文件
  • ...

本地有一个json文件:

json 复制代码
{"username":  "tom", "age":  10}  
{"username":  "John", "age":  20}  
{"username":  "Yang", "age":  40}  
{"username":  "Jerry", "age":  20}

通过load以及json两种方法读取这个json文件

java 复制代码
// 1. 通过json方法读取json文件
Dataset<Row> ds = sparkSession.read().json("D:\\project\\sparkDemo\\inputs\\user.json");
// 可以看见这里返回的类型是Dataset<Row>,虽然写着DataSet,但是泛型是row,所以是dataframe数据

// 2. 通过load读取json文件
Dataset<Row> df = sparkSession.read().format("json").load("D:\\project\\sparkDemo\\inputs\\user.json");
// format方法传入一个文件类型字符串

df.show();
+---+--------+
|age|username|
+---+--------+
| 10|     tom|
| 20|    John|
| 40|    Yang|
| 20|   Jerry|
+---+--------+

RDD创建DataFrame

Java想要实现这个转换还是比较麻烦的,Scala中的RDD可以直接调用toDF()转换。 普通的javaRDD需要转换为javaRDD<Row>,然后还需要创建StructType并添加字段信息,最终调用sparksession.createDataFrame()方法传入上述两个参数构建dataframe对象。

java 复制代码
import org.apache.spark.SparkConf;  
import org.apache.spark.api.java.JavaRDD;  
import org.apache.spark.api.java.JavaSparkContext;  
import org.apache.spark.api.java.function.Function;  
import org.apache.spark.sql.Dataset;  
import org.apache.spark.sql.Row;  
import org.apache.spark.sql.RowFactory;  
import org.apache.spark.sql.SparkSession;  
import org.apache.spark.sql.types.DataTypes;  
import org.apache.spark.sql.types.StructField;  
import org.apache.spark.sql.types.StructType;  
  
import java.util.ArrayList;  
import java.util.List;  
  
import static java.util.Arrays.asList;  
  
public class sparkSQLdemo {  
    public static void main(String[] args) {  
        SparkConf sparkAppConf = new SparkConf().setMaster("local[*]").setAppName("sparkRddDemo");  
  
        SparkSession sparkSession = SparkSession.builder().config(sparkAppConf)  
                .getOrCreate();  
        // 需要确保只有一个sparkContext对象,否则会抛出异常  
        // 需要通过sparkSession.sparkContext()返回的Scala的sparkContext对象创建JavaSparkContext  
        JavaSparkContext sparkContext = JavaSparkContext.fromSparkContext(sparkSession.sparkContext());  
  
        List<String> stringList = asList("HELLO", "SPARK", "HELLO", "FLINK");  
        JavaRDD<String> rdd = sparkContext.parallelize(stringList);  
        // 将普通的javaRDD转换为javaRDD<Row>,因为转换后的dataframe是Dataset<Row>类型  
        JavaRDD<Row> RowRDD = rdd.map(new Function<String, Row>() {  
            @Override  
            public Row call(String s) throws Exception {  
                return RowFactory.create(s);  
            }  
        });  
        // 定义schema  
        List<StructField> fields = new ArrayList<>();  
        fields.add(DataTypes.createStructField("word", DataTypes.StringType, true));  
        StructType schema = DataTypes.createStructType(fields);  
  
        Dataset<Row> dataFrame = sparkSession.createDataFrame(RowRDD, schema);  
  
        dataFrame.show();  
  
        sparkSession.stop();  
    }  
}

hivetable创建DataFrame

没有hive环境,跳过。

通过JDBC创建DataFrame

java 复制代码
package src.main.sqlDemo;  
  
import org.apache.spark.sql.*;  
  
/**  
 * @projectName: sparkDemo  
 * @package: src.main.sqlDemo  
 * @className: spark01SQLBasic  
 * @author: NelsonWu  
 * @description: sparkSQL read mysql table and write to mysql  
 * @date: 2024/3/6 23:22  
 * @version: 1.0  
 */public class spark04SQLJDBC {  
    public static void main(String[] args) throws AnalysisException {  
        SparkSession sparkSession = SparkSession.builder()  
                .appName("sparkSQLApp")  
                .config("spark.master", "local[2]")  
                .getOrCreate();  
        // 从MySQL加载表创建df  
        Dataset<Row> mysqlDF = sparkSession.read().format("jdbc")  
                .option("url", "jdbc:mysql://172.20.143.219:3306/test")  
                .option("driver", "com.mysql.cj.jdbc.Driver").option("user", "root")  
                .option("password", "mysql").option("dbtable", "ws").load();  
  
        mysqlDF.show();  
        // 将df新建MySQL表的形式写入MySQL数据库  
        mysqlDF.write().format("jdbc").option("url", "jdbc:mysql://172.20.143.219:3306/test")  
                .option("driver", "com.mysql.cj.jdbc.Driver").option("user", "root")  
                .option("password", "mysql").option("dbtable", "ws1").save();  
        sparkSession.stop();  
  
    }  
}

DataFrame转RDD

想从dataframe转RDD非常简单直接调用toJavaRDD方法即可,转换后的RDD对象为JavaRDD<Row>类型

java 复制代码
        JavaRDD<Row> javaRDD = dataFrame.toJavaRDD();

        javaRDD.foreach(new VoidFunction<Row>() {
            @Override
            public void call(Row row) throws Exception {
                System.out.println(row.get(0));
            }
        });

SQL语法

要想在sparkSQL中使用sql,需要有表,然后我们需要通过SqlContext这个上下文对象去调用sql()方法执行sql语句。 通过dataFrame创建临时表:

java 复制代码
ds.createTempView("user");  // 创建临时表,生命周期为当前session  
ds.createOrReplaceTempView("tbname");  // 创建临时表,生命周期同上,如果存在同名表则替换该表,即数据替换成当前dataframe的数据  
ds.createGlobalTempView("tbname");  // 创建全局的临时表,可供所有session使用  
ds.createOrReplaceGlobalTempView("tbname");  // 同上,但是如果存在同名表会替换成当前的dataframe数据

可根据具体需求选用上述api。

java 复制代码
Dataset<Row> ds = sparkSession.read().json("D:\\project\\sparkDemo\\inputs\\user.json");  
ds.createTempView("user");  
  
SQLContext sqlContext = ds.sqlContext();  
sqlContext.sql("select username from user").show();

DSL语法

DataFrame 提供一个特定领域语言(domain-specific language, DSL)去管理结构化的数据。可以在 Scala, Java, Python 和 R 中使用 DSL,使用 DSL 语法风格不必去创建临时视图了。 DSL语法可以直接通过DataFrame对象调用DSL语法相关API使用,例如select/filter/groupBy/count等。

下面是Scala的相关用法:

scala 复制代码
df.select("username").show()
// 查询用户名称以及用户年龄+1的结果
df.select('username, 'age + 1).show()
// 过滤出年龄大于30的
df.filter($"age">30).show
// 按照年龄分组统计每个分组的数据量
df.groupBy("age").count.show
java 复制代码
df.select("username", "age").show();  // 可以正常查询
// 下面这种写法是不正确的,select内的参数必须为df的列名
df.select("age + 1").show();

Java的API目前无法像Scala一样实现DSL语法(就目前按照我的查询的资料来看),但是其中还有其他的方法:selectExpr可以实现复杂的查询

java 复制代码
df.selectExpr("age + 1").show();

这样就可以实现年龄加一的操作了。 +---------+ |(age + 1)| +---------+ | 11| | 21| | 41| | 21| +---------+

可以参考下这个网页,里面有相关的案例,虽然是Scala的 sparkbyexamples.com/spark/spark...

数据保存

dataframe提供了相关的API将数据保存到外部(文件系统、关系型数据库等)。 Java中dataframe可以通过write方法保存文件,write方法会返回一个DataFrameWriter对象,通过这个对象保存数据。 writer对象可以调用save方法保存,跟load方法一样,可以保存csv、json、orc、parquet、textfile等类型的文件,默认情况下也是parquet类型。 同时writer也单独实现了常见的数据文件方法,上述的基本都实现了,可以直接调用。

在save方法之前,可以通过mode方法传入保存模式 ![[Pasted image 20240308004428.png]] 默认情况下是error类型,目录存在则直接抛出异常提示文件已存在。

java 复制代码
df.write()  
        .mode("overwrite")  
        .csv("/home/saberbin/output");// windows下会报错Hadoop未设置  
  
df.write().mode("overwrite").format("csv").save("/home/saberbin/output");

// 通过format方法传入需要保存的文件类型,如果不传入,默认save方法保存的是parquet文件

请注意,如果是Windows系统下使用IDEA执行上述的代码,会抛出Hadoop未设置的异常,spark3.3.2环境。这个目前没有找到解决办法,可切换至Linux系统下。 保存到文件系统的情况下,save方法需要传入一个路径,其他情况可不传参数。 如果是保存到关系型数据库,需要通过option方法设置相关的参数

java 复制代码
df.write().format("jdbc").option("url", "jdbc:mysql://172.20.143.219:3306/test")  
        .option("driver", "com.mysql.cj.jdbc.Driver").option("user", "root")  
        .option("password", "mysql").option("dbtable", "ws1").save();

DataSet使用

dataset是强类型

创建DataSet(df2ds)

可以通过样例类的序列转成DataSet,实际用处不是很大。 这里通过样例类将df转换为ds

java 复制代码
import org.apache.spark.sql.*;
Dataset<Row> df = sparkSession.read().json("D:\\project\\sparkDemo\\inputs\\user.json");  
Dataset<User> userDataset = df.as(Encoders.bean(User.class));

public static class User implements Serializable {  
    private String username;  
    private Integer age;  
  
    public User(String username, Integer age) {  
        this.username = username;  
        this.age = age;  
    }  
    public String getUsername() {  
        return username;  
    }  
    public void setUsername(String username) {  
        this.username = username;  
    }  
    public Integer getAge() {  
        return age;  
    }  
    public void setAge(Integer age) {  
        this.age = age;  
    }}

RDD转DataSet

官方都是通过spark session的createDataset方法去创建dataset的:

java 复制代码
List<String> data = Arrays.asList("abc", "abc", "xyz");
Dataset<String> ds = context.createDataset(data, Encoders.STRING());

Encoder<Tuple2<Integer, String>> encoder2 = Encoders.tuple(Encoders.INT(), Encoders.STRING());
List<Tuple2<Integer, String>> data2 = Arrays.asList(new scala.Tuple2(1, "a");
Dataset<Tuple2<Integer, String>> ds2 = context.createDataset(data2, encoder2);

但是这些都是基础数据类型的dataset,而不是自定义的样例类。 而且上述不是通过rdd转的,而是将序列直接转换成dataset对象。不太符合我的需求。 Scala是可以直接调用方法转换。Java目前还没有找到对应的方法,有一个折中的方案就是rdd->dataframe->dataset。但是我不太想用这个。

sparkSession的createDataset方法有三种传参方式:

其中两种都是传入序列或者列表的形式,一种是通过rdd传入创建dataset

前面由rdd转df的时候是通过添加结构化类型实现的:

java 复制代码
// 定义schema  
List<StructField> fields = new ArrayList<>();  
fields.add(DataTypes.createStructField("word", DataTypes.StringType, true));  
StructType schema = DataTypes.createStructType(fields);  
  
Dataset<Row> dataFrame = sparkSession.createDataFrame(RowRDD, schema);// rdd -> dataframe

StructField只支持基础的数据类型,不能添加样例类这种自定义的类。 所以通过添加schema的只能是将rdd转成df,而不能变成ds。 之前想法是df是rdd添加了schema,ds是在df的基础上包装了强类型,那么我想在样例类RDD的基础上包装了row类型(df的基础),再转成ds应该可以,实际上试验过不行。

rdd转成列表再转换dataset

java 复制代码
JavaRDD<String> stringJavaRDD = sparkContext.textFile("D:\\project\\sparkDemo\\inputs\\user.txt");  
  
JavaRDD<User> rdd = stringJavaRDD.map(new Function<String, User>() {  
    @Override  
    public User call(String s) throws Exception {  
        String[] value = s.split(",");  
        String username = value[0];  
        String age = value[1];  
        return new User(username, Integer.valueOf(age));  
    }  
});  
  
Iterator<User> userIterator = rdd.toLocalIterator();  
  
ArrayList<User> userList = new ArrayList<>();  
while (userIterator.hasNext()){  
    userList.add(userIterator.next());  
}  
  
Dataset<User> dataset = sparkSession.createDataset(userList, Encoders.bean(User.class));  
  
dataset.show();

rdd转df转ds

dataframe转dataset

样例类: 这里的age需要为Long类型

java 复制代码
public static class User implements Serializable {  
    private String username;  
    private Long age;  
  
    public User(String username, Long age) {  
        this.username = username;  
        this.age = age;  
    }  
    public String getUsername() {  
        return username;  
    }  
    public void setUsername(String username) {  
        this.username = username;  
    }  
    public Long getAge() {  
        return age;  
    }  
    public void setAge(Long age) {  
        this.age = age;  
    }}
java 复制代码
Dataset<Row> df = sparkSession.read().json("D:\\project\\sparkDemo\\inputs\\user.json");  
  
Dataset<User> dataset = df.as(Encoders.bean(User.class));  
  
dataset.show();

因为这里自动推断了df的age为bigint,如果user类的age为Integet类型,则会抛出异常。

dataset转dataframe

dataset可以直接调用toDF方法转换dataframe,该方法可以传入schema,也可以不传

java 复制代码
Dataset<Row> dataframe = dataset.toDF();

UDF

通过 spark.udf 功能添加自定义函数, 实现自定义功能

java 复制代码
Dataset<Row> ds = sparkSession.read().json("D:\\project\\sparkDemo\\inputs\\user.json");  
  
ds.createTempView("user");  
SQLContext sqlContext = ds.sqlContext();  
sparkSession.udf().register("pefixName", new UDF1<String, String>() {  
    @Override  
    public String call(String s) throws Exception {  
        return "userName:" + s;  
    }  
}, DataTypes.StringType);  
  
sqlContext.sql("select pefixName(username) as name from user").show();  
/*  
+--------------+  
|          name|  
+--------------+  
|  userName:tom|  
| userName:John|  
| userName:Yang|  
|userName:Jerry|  
+--------------+  
 */

UDAF

强类型的实现方法:

java 复制代码
import org.apache.spark.sql.expressions.Aggregator;
sparkSession.udf().register("calAvg", functions.udaf(new CalAvg(), Encoders.INT()));  
sqlContext.sql("select calAvg(age) from user").show();  
/*  
+-----------+  
|calavg(age)|  
+-----------+  
|         22|  
+-----------+  
 */

  
public static class Buff implements Serializable {  
    public Integer total;  
    public Integer cnt;  
    public Buff(){  
  
    }    public Buff(Integer total, Integer cnt){  
        this.total = total;  
        this.cnt = cnt;  
    }    public Integer getTotal() {  
        return total;  
    }  
    public void setTotal(Integer total) {  
        this.total = total;  
    }  
    public Integer getCnt() {  
        return cnt;  
    }  
    public void setCnt(Integer cnt) {  
        this.cnt = cnt;  
    }  
    public void addTotal(Integer total){  
        this.total = this.total + total;  
    }    public void addCnt(){  
        this.cnt = this.cnt + 1;  
    }}  
  
public static class CalAvg extends Aggregator<Integer, Buff, Integer>{  
  
  
    @Override  
    public Buff zero() {  
        /*  
        初始化缓冲区  
         */        return new Buff(0, 0);  
    }  
    @Override  
    public Buff reduce(Buff b, Integer a) {  
        /*  
        更新缓冲区  
         */        b.addTotal(a);  
        b.addCnt();  
        return b;  
    }  
    @Override  
    public Buff merge(Buff b1, Buff b2) {  
        /*  
        合并缓冲区  
         */        Integer total = b1.getTotal() + b2.getTotal();  
        Integer cnt = b1.getCnt() + b2.getCnt();  
        return new Buff(total, cnt);  
    }  
    @Override  
    public Integer finish(Buff reduction) {  
        /*  
        计算结果  
         */        return reduction.total / reduction.cnt;  
    }  
    @Override  
    public Encoder<Buff> bufferEncoder() {  
        /*  
        缓冲区编码操作  
         */        return Encoders.bean(Buff.class);  
    }  
    @Override  
    public Encoder<Integer> outputEncoder() {  
        /*  
        输出结果的缓冲区操作  
         */        return Encoders.INT();  
        };    }

强类型的UDAF也可以通过DSL语法进行调用,自定义的UDAF方法需要传入样例类对象,上述的UDAF修改一下即可,但是在我的Windows以及Linux(WSL)下都会抛出空指针异常(代码倒是没有问题),不知道是否为Java api下的bug。。。所以这里就不贴出代码了。


end

相关推荐
蚂蚁数据AntData1 小时前
流批一体向量化计算引擎 Flex 在蚂蚁的探索和实践
大数据·数据仓库·spark·数据库架构
lucky_syq1 天前
Spark和MapReduce之间的区别?
大数据·spark·mapreduce
lucky_syq1 天前
Hive SQL和Spark SQL的区别?
hive·sql·spark
lucky_syq1 天前
Spark和Hadoop之间的区别
大数据·hadoop·spark
NiNg_1_2342 天前
Spark常用的转化操作和动作操作详解
大数据·ajax·spark
岑梓铭2 天前
(CentOs系统虚拟机)Standalone模式下安装部署“基于Python编写”的Spark框架
linux·python·spark·centos
Data跳动3 天前
Spark内存都消耗在哪里了?
大数据·分布式·spark
lucky_syq3 天前
流式处理,为什么Flink比Spark Streaming好?
大数据·flink·spark
goTsHgo3 天前
在 Spark 上实现 Graph Embedding
大数据·spark·embedding
程序猿小柒3 天前
【Spark】Spark SQL执行计划-精简版
大数据·sql·spark