0%

Groovy 4之新特性GINQ预览

背景介绍

C#最先引入LINQ特性,其强大的功能令我垂涎已久,并琢磨着为Groovy也添加类似特性,终于在Groovy 4中决定完成这一夙愿。GINQ是Groovy-Integrated Query的缩写,意在对集合的操作以类SQL的方式完成,将来或许会扩展至对数据库的支持。另外,结合Groovy的XML, JSON, YAML等文本解析特性,GINQ可天然地支持对这些文本内容进行查询。

原理介绍

为了保持语法的向下兼容,没有为GINQ引入新语法,而是以DSL形式实现,其处理过程如下:

Groovy DSL --解析--> Groovy AST --转换--> GINQ AST --优化--> 优化后的GINQ AST --生成--> 目标Groovy AST

解析阶段

借助Groovy既有的解析器Parrot,将Groovy DSL源码解析为Groovy AST,该AST对于Groovy而言是通用的但缺乏“个性”,难以方便地描述GINQ的形态。

转换阶段

为了方便地描述GINQ,我设计了GINQ AST。在获得了Groovy AST之后,比对GINQ AST规范,将Groovy AST转换为GINQ AST。

优化阶段

原始GINQ AST的结构可能不是最优,需要做进一步的转换优化,比如存在先关联再筛选的情况,需要将筛选节点下沉,作用于数据源节点。

生成阶段

无论是操作集合还是操作数据库,都生成可实际执行的代码。为了这个目标,先将优化后的GINQ AST转换为目标代码对应的Groovy AST,然后借助Groovy既有的编译特性生成bytecode。

特性预览

GINQ的代码需要放置在GQ {...}块中,这样Groovy便可将其识别为GINQ DSL。

例子1:筛选出List实例中大于1并且小于等于5的元素

1
2
3
4
5
6
def result = GQ {
from n in [1, 2, 3, 4, 5, 6]
where n > 1 && n <= 5
select n
}
assert [2, 3, 4, 5] == result.toList()

注:任何Groovy的操作符均可在where中使用,比如in, !in等,而数据源可以是Iterable, Stream, 数组以及GINQ执行结果。

例子2:将List实例中的元素先按长度再按内容进行排序

1
2
3
4
5
6
def result = GQ {
from s in ['ab', 'bc', 'abc', 'bcd']
orderby s.length() in desc, s
select s
}
assert ['abc', 'bcd', 'ab', 'bc'] == result.toList()

注:默认升序asc。另外,null值的排序规则可通过nullslast以及nullsfirst指定,比如:asc(nullslast), asc(nullsfirst), desc(nullslast), desc(nullsfirst),而ascasc(nullslast)的缩写,descdesc(nullslast)的缩写。

例子3:在List实例中选取序号为1开始的2个元素

1
2
3
4
5
6
def result = GQ {
from n in [3, 4, 5, 6]
limit 1, 2
select n
}
assert [4, 5] == result.toList()

注:序号从0开始

例子4:关联2个List实例并筛选满足条件的元素

1
2
3
4
5
6
7
def result = GQ {
from n in [1, 2, 3, 4, 5, 6]
join m in [3, 4, 5, 6, 7, 8] on m == n
where n > 1 && n <= 5 && m > 3 && m <= 8
select n
}
assert [4, 5] == result.toList()

例子5:按模2的结果对List实例中的元素进行分组并计算

1
2
3
4
5
6
def result = GQ {
from n in [1, 2, 3, 4, 5, 6, 7, 8, 9]
groupby n % 2 as k // 取个别名k
select k, count(), min(n), max(n)
}
assert [[1, 5, 1, 9], [0, 4, 2, 8]] == result.toList()

注:目前已支持的聚合函数有:count, min, max, sum, avg, median, stdev, stdevp, var, varp, agg

例子6:将List实例中的元素按元素长度分区再按内容降序排列并选取元素的前一个元素

1
2
3
4
5
def result = GQ {
from s in ['a', 'ab', 'b', 'bc']
select s, (lead(s) over(partitionby s.length() orderby s in desc))
}
assert [['a', null], ['ab', null], ['b', 'a'], ['bc', 'ab']] == result.toList()

注:目前已支持的窗口函数有:rowNumber, rank, denseRank, percentRank, cumeDist, ntile, lead, lag, firstValue, lastValue, nthValue, count, min, max, sum, avg, median, stdev, stdevp, var, varp

例子7:查询JSON内容

先通过JsonSlurper将json内容解析为集合,然后利用GINQ对集合进行查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def json = new groovy.json.JsonSlurper().parseText('''
{
"fruits": [
{"name": "Orange", "price": 11},
{"name": "Apple", "price": 6},
{"name": "Banana", "price": 4},
{"name": "Mongo", "price": 29},
{"name": "Durian", "price": 32}
]
}
''')
def result = GQ {
from f in json.fruits
where f.price < 32
orderby f.price in desc
select f.name, f.price
}
assert [['Mongo', 29], ['Orange', 11], ['Apple', 6], ['Banana', 4]] == result.toList()

例子8:列表推导式(List Comprehension)

构造一个List实例,其元素是1至10中偶数的平方

1
2
def result = GQL {from n in 1..<11 where n % 2 == 0 select n ** 2}
assert [4, 16, 36, 64, 100] == result

注:GQL {...} 等价于 GQ {...} as List

例子N:……

点击链接查看更多例子

相关文章

1. GINQ官方文档

2. 《让我们介绍Groovy的最新趋势》