kotlin 能在 JAVA 中取代 SQL 吗

很多人都会遇到不方便使用数据库但又要结构化数据计算的情况。JAVA 8之前只能全都硬编码实现。JAVA8推出了惰性(Lazy Evaluation)的集合计算库Stream,虽然一定程度上缓解了这种状况,但仍然存在不少的缺点(详见《Stream能在Java中取代SQL吗》)。Kotlin是一门全兼容JAVA生态系统、并额外支持JavaScript的开发语言。Kotlin以Stream为基础并针对其缺点进行了改进,很重要的改进就是简化Lamda语法,其次增加了热情(Eager Evaluation,与惰性相对)的集合计算,并补充了很多集合函数。

Kotlin对Stream的改进是值得肯定的,但Kotlin仍然没有提供专业的结构化数据对象,本质不变,Kotlin就仍然无法代替SQL。事实上,Stream的很多缺点同样会在Kotlin上体现出来。我们把之前Stream贴子中的例子用Kotlin再实现一下,读者可以感受其中的异同。

集合的成员是简单数据类型时,Kotlin和Stream一样,都可以方便地实现集合计算,比如整数集合的过滤:

var numbers=listOf(3,11,21,27,9)
    var r1=numbers.filter{it>=10 && it<20}

排序:

var r2=numbers.sorted()

比如交集:

var others=listOf(2,11,21)
var result=numbers  intersect   others

上面用到的都是热情集合List<T>,在输入输出、复用、类型转换时比惰性集合Stream<T>更方便,特别适合数据量较少且性能要求不高的场景,其中交集是Stream缺乏而Kotlin新增的集合函数(中缀形式)。类似集合计算还很多,代码通常简短易懂,与SQL有相通之处,比如去重distinct\求和sum \计数count等。

但数据对象不是简单数据类型,而是记录(通常是data class)时,和Stream类似, Kotlin就不那么方便了。比如对订单表的Client字段逆序排序,对Amount字段顺序排序:

//Orders是List<Order>类型,Order定义如下:
//data class Order(var OrderID: Int,var Client: String,var   SellerId: Int, var Amount: Double, var OrderDate: Date)
var resutl=Orders.sortedBy{it.Amount}.sortedByDescending{it.Client}

可能因为使用了Stream作为基础,Kotlin的排序字段也要前后颠倒,所用函数也和简单数据类型的排序函数不同,写法麻烦了许多。

再看分组,比如对订单表的SellerId分组,对Amount求和:

var result =Orders.groupingBy(Order::SellerId).fold(0.0){acc,elem->(acc+elem.Amount)}
result.forEach{println("${it.key}:\t ${it.value}")}

Kotlin提供了fold函数,用于封装Stream中collect + Collectors + summarizingDouble + DoubleSummaryStatistics等多个类和函数组合而成的代码片段,使整体结构变得清晰许多。

但无论怎么封装,分组汇总的结果是不变的,同样是Map类型,而不是常规的记录。Kotlin的数据类型不如SQL那样一致(源和结果都是记录),这在进一步计算时会遇到类型转换的麻烦。除了fold之外,Kotlin还提供了reduce或aggregate等函数实现汇总的效果,每种函数用法各不相同,适用于不同的细分场景,全部学会要花不少精力,远不如SQL方便。

多字段分组汇总,即按年份和Client分组,对Amount求和并计数:

data class Grp(var OrderYear:Int,var SellerId:Int)
    data class Agg(var sumAmount: Double,var rowCount:Int)
    var result=Orders.groupingBy{Grp(it.OrderDate.year+1900,it.SellerId)}
        .fold(Agg(0.0,0),{
            acc, elem ->   Agg(acc.sumAmount + elem.Amount,acc.rowCount+1)
        })

.toSortedMap(compareBy<Grp> {   it. OrderYear}.thenBy {it. SellerId})
    result.forEach{println("group fields:${it.key.OrderYear}\t${it.key.SellerId}\t   aggregate fields:${it.value.sumAmount}\t${it.value.rowCount}") }

单字段分组汇总时,可以用Map里的key存储分组字段(value类似),多字段分组汇总就不能这么干了,因为key里不能放多个字段。这种情况下可以定义一个有结构的数据对象Grp,把多个分组字段拼进这个对象里,再用key来存储Grp。由于Kotlin是基于Stream的,所以同样不支持动态数据结构,必须先定义结果的数据结构再计算,SQL程序员很难适应这种死板的用法。相对的,SQL是解释型语言,动态数据结构是基本功能,不必事先定义数据结构。

上面的方法实现起来较简单,也可以麻烦一些,像Stream那篇帖子一样,把多个字段按分隔符拼成一个字段,转为单字段分组汇总,最后再按分隔符拆开。

分组汇总的最后还有个排序(不是必须的,主要为了和SQL的计算结果保持一致),可以发现Map和记录所用的排序函数不同。这些不一致的地方还有很多,导致Kotlin的学习成本相当高。

对于关联计算,比如对Orders表和Employee表进行内关联,然后对Employee.Dept进行分组,对Orders.Amount求和并计数。

// Employees是List<Employee>类型,Employee定义如下:
// data class Employee(var EId:Int, var State:String, var   Dept:String , var Name:String ,var Gender:String ,var  Salary:Int,var Birthday:Date)

data class OrderNew(var OrderID:Int ,var Client:String, var   SellerId:Employee ,var Amount:Double ,var OrderDate:Date)
    val result = Orders.map {o->var   emp=Employees.firstOrNull{it.EId==o.SellerId}
emp?.let{OrderNew(o.OrderID,o.Client,emp,o.Amount,o.OrderDate)}
        }
        .filter   {o->o!=null}
    data class Grp(var Dept:String,var Gender:String)
    data class Agg(var sumAmount: Double,var rowCount:Int)
    var result1=result.groupingBy{Grp(it!!.SellerId.Dept,it.SellerId.Gender)}
        .fold(Agg(0.0,0),{
            acc, elem ->   Agg(acc.sumAmount + elem!!.Amount,acc.rowCount+1)
        }).toSortedMap(compareBy<Grp>   { it.Dept}.thenBy {it.Gender})

Kotlin和Stream一样不支持关联,只能硬编码实现,而且实现的思路一样,都是把Order的外键SellerId替换成Employee对应的记录。因为替换后数据结构发生变化(字段类型不同),而Kotlin和Stream又不支持动态数据结构,所以要定义一个新数据结构。此外,左关联和外关联的代码同样要硬编码,关键的代码逻辑还不一样,这类不一致的现象会对程序员造成许多麻烦。应该注意到,Kotlin关联后的分组汇总代码与直接分组汇总的代码不同,而Stream是相同的,这是因为Kotlin强制要求空安全(Null Safety),关联后需要做一些处理才能保证空安全,而Stream(本质是JAVA语言)没有这样的强制要求。

关联在结构化数据计算中很重要,Kotlin对关联计算的支持和Stream一样不好,在结构化数据计算方面很不专业,远不如SQL。

从这些例子可以看出来,Kotlin的确对Stream有所改进,代码长度也有所缩短。但也应当看到,Kotlin的很多缺点与Stream几乎一致。这是因为Kotlin和 Stream(JAVA)都是编译型语言,缺乏专业的结构化数据对象,无法支持动态数据结构,难以真正简化Lambda语法,无法直接引用字段,也就不能取代SQL(详见Stream的帖子)。

如果遇到不方便使用数据库但又要结构化数据计算的情况,目前看来还是集算器SPL可靠。