[!SUMMARY]
受到 C# 的 LINQ 语法启发,Groovy 4.x 带来了 GINQ 语法,这为我们提供了一种全新的方式查询和操作集合,本文将对这一语法的基础使用进行简短地介绍。
最近总算将手头的 Groovy 版本从 2.5.x 升级到了 4.x,这意味着新写的 Groovy 脚本不仅能和高版本 Java 有更好的兼容性(如箭头函数),还能使用更多的 Groovy 新特性。
而 GINQ (Groovy-Integrated Query)就是一个很有趣的新语法,该语法显然是参考自 C# 的 LINQ,该语法为我们带来了一种用类似 SQL 的语法来查询和操作集合的全新体验。
于是在阅读了官方文档 Using GINQ 一节后,在此简单地总结一下 GINQ 的入门用法。
GINQ 简介
从官网的文档里可以看到,GINQ 的语法结构如下:
GQ, i.e. abbreviation for GINQ
|__ from
| |__ <data_source_alias> in <data_source>
|__ [join/innerjoin/leftjoin/rightjoin/fulljoin/crossjoin]*
| |__ <data_source_alias> in <data_source>
| |__ on <condition> ((&& | ||) <condition>)* (NOTE: `crossjoin` does not need `on` clause)
|__ [where]
| |__ <condition> ((&& | ||) <condition>)*
|__ [groupby]
| |__ <expression> [as <alias>] (, <expression> [as <alias>])*
| |__ [having]
| |__ <condition> ((&& | ||) <condition>)*
|__ [orderby]
| |__ <expression> [in (asc|desc)] (, <expression> [in (asc|desc)])*
|__ [limit]
| |__ [<offset>,] <size>
|__ select
|__ <expression> [as <alias>] (, <expression> [as <alias>])*
Note
[]
表示相关从句是可选的,*
表示出现零次或多次,而+
表示出现一次或多次。另外,GINQ 语句是顺序敏感的,因此语句的顺序需要和上方的结构保持一致。
光是看这语法定义,可以说和 SQL 是很相似了。不过考虑到向下兼容,GINQ 并未引入新的关键字,而是作为一种 DSL 被引入到 Groovy,所以和 SQL 还是有一些区别的,比如最为明显的就是 select
和 from
出现的顺序不一样。
GINQ 基础语法
话不多说,先看一个仅需 from
和 select
的简单用例:
// 作为 DSL,GINQ 语句必须使用 `GQ` 包裹:
def query = GQ {
from w in ['hello', 'world']
select w
}
// 这里的 query 的类型被推导为 `Queryable<String>`,
// 并且它和 Java 的 Stream 一样,只有调用时才会执行,即它是懒加载的
def helloWorld = query.stream().collect(Collectors.joining(', '))
println helloWorld // 'hello, world'
也可以通过往 GQ
中传入相关的参数进行定制,比如下面启用了并行处理:
GQ(parallel: true) {
from i in [1, 2, 3]
select i
}.toList()
如果一个方法是使用 GINQ 实现的,那么需要使用 @groovy.ginq.transform.GQ
注解来标注这个方法:
import groovy.ginq.transform.GQ
@GQ
def getEvenNumbers(def numbers) {
from n in numbers
where n % 2 == 0
select n
}
assert [2, 4] == getEvenNumbers([1, 2, 3, 4]).toList()
也可以在 @GQ
里指定函数的返回值类型,配置是否开启并行处理(默认不开启)等,如:
import groovy.ginq.transform.GQ
@GQ(value = List<Integer>, parallel = true)
def ginq(def numbers) {
from n in numbers
where n > 2
select n
}
println ginq([1, 2, 3, 4]).class // class java.util.ArrayList
assert [3, 4] == ginq([1, 2, 3, 4])
Note
GINQ 支持多种返回结果类型,比如:
List
,Set
,Collection
,Iterable
,Iterator
,java.util.stream.Stream
以及数组类型等。
GINQ 语法详解
from
:指定数据源
从前面的语法结构可知,GINQ 语句都是从 from
开始的,from
后面跟随的是数据源,当前支持的数据源类型有 Iterable
、 Stream
、数组和 GINQ 结果集等。
Iterable
from n in [1, 2, 3] select n
Stream
from w in ['a', 'b', 'c', 'd'].stream() select w
数组
from i in new int[]{1, 2, 3, 4} select i
GINQ 结果集
def ginqResultSet = GQ { from i in [1, 2, 3, 4] select i } def filteredResultSet = GQ { from i in ginqResultSet where i >= 3 select i }
as
:为列起一个别名
import java.util.stream.Collectors
def result = GQ {
from n in [1, 2, 3]
// 用 as 为 column 列起一个别名
select n, n * 2 as doubleN, n * 3 as tripleN, n * 4 as quadrupleN
}
def list = result.stream()
// r 的类型为 org.apache.groovy.ginq.provider.collection.runtime.NamedRecord
// 可以看到这里我们甚至可以用 4 种方式来获取每一条查询结果中的列的值
.map { r -> [r?[0], r?['doubleN'], r?.get('tripleN'), r?.quadrupleN] }
.collect(Collectors.toList())
println(list) // [[1, 2, 3, 4], [2, 4, 6, 8], [3, 6, 9, 12]]
Note
对于语法
select new NamedRecord(P1, P2, …, Pn)
,当且仅当n >= 2
时,可以简写为select P1, P2, …, Pn
。并且在使用as
时也会创建NamedRecord
实例,而我们可以通过别名来引用存储在NamedRecord
实例中数据。
distinct
:去除重复记录
def distinct = GQ {
from pair in [[1, 1], [2, 2], [2, 2], [1, 3]]
select distinct(pair)
}.toList()
// [2, 2] 只出现了一次
print(distinct) // [[1, 1], [2, 2], [1, 3]]
where
:按条件过滤
def filteredList = GQ {
from i in [1, 2, 3, 4, 5]
where i >= 4 || i < 2
select i
}.toList()
println(filteredList) // [1, 4, 5]
也支持 in
和 !in
(not in)筛选:
def filteredList2 = GQ {
from i in [1, 2, 3, 4]
// 支持 `in`
where i in (
// 子查询
from n in [1, 2, 3]
// 支持 `!in` 即 not in
where n !in [3]
select n
)
select i
}
println(filteredList2) // [1, 2]
当然也有 exists
和 not exists
:
def filteredList3 = GQ {
from i in [1, 2, 3, 4]
// 支持 `where(...).exists()`,
// 即 SQL 里的 `exists` 语句
where(
from x in (
from n in [1, 2, 3]
// 支持 `where !(...).exists()`,
// 即 SQL 里的 `not exists` 语句
where !(
from m in [2, 3]
where m == n
select m
).exists()
select n
)
where x == i
select x
).exists()
select i
}.toList()
println(filteredList3) // [1]
orderby
:为返回的结果集进行排序
def list = GQ {
from n in [[1, 'a'], [1, 'bc'], [2, '3']]
orderby n[0] in asc, (n[1] as String).length() in desc
select n[0] as key, n[1] as value
}
// +-----+-------+
// | key | value |
// +-----+-------+
// | 1 | bc |
// | 1 | a |
// | 2 | 3 |
// +-----+-------+
print list
如果是顺序排序,可以省略 in asc
:
from n in [[1, 'a'], [2, 'b'], [3, '3']]
// `orderby n[0] in asc` 与 `orderby n[0]` 等价
orderby n[0]
select n[0] as key, n[1] as value
另外,对于 null
的排序,我们可以通过在 asc
或 desc
中传入 nullsfirst
或 nullslast
来指定:
def list3 = GQ {
from i in [1, 2, null, 3]
// 指定逆序,null 在前的排序顺序
orderby i in desc(nullsfirst)
select i
}
print(list3.toList()) // [null, 3, 2, 1]
limit
:分页
limit
的用法和 MySQL 里的 limit
用法一样,即第一个参数为可选参数 offset
,表示偏移量,第二个参数为 size
,表示返回的结果集数量。
def list = GQ {
from i in [1, 2, 3, 4, 5]
// 等价于 `limit 0,2`
limit 2
select i
}
print(list) // [1, 2]
def list2 = GQ {
from i in [1, 2, 3, 4, 5]
limit 2, 1
select i
}
print(list2) // [3]
join
:关联新的数据源
和 SQL 一样,GINQ 的 JOIN 也分为:
- 内联:如
join
、innerjoin
、innerhashjoin
等 - 外联:如
lefthashjoin
、righthashjoin
等 - 其他:如
fulljoin
、crossjoin
等
使用方式和我们在 SQL 中的用法相似。另外关联语句中的 on
语句中只能使用 ==
和 &&
表达式。
def list = GQ {
from i1 in [[1, 'A'], [2, 'B'], [3, 'C']]
innerjoin i2 in [1, 2] on i1[0] == i2
select i1[1]
}
println(list) // [A, B]
def list2 = GQ {
from i1 in [[1, 'A'], [2, 'B'], [3, 'C']]
leftjoin i2 in [[1, 'a'], [2, 'b']] on i1[0] == i2[0]
select i1[0], i2?[1]
}.toList()
println(list2) // [[1, a], [2, b], [3, null]]
如果列表里的对象较多,则推荐使用「hash」的版本,如 innerhashjoin
、lefthashjoin
等。不过对于内联的 innerjoin
和 innerhashjoin
,我们都可以统一使用 join
,Groovy 也会为我们智能的选择使用 innerjoin
或 innerhashjoin
,还能让代码拥有更好的可读性。
switch
:SQL 中 case...when
的替代
SQL 中的 case...when...
语法在 GINQ 中可由 switch
表达式来呈现:
def list = GQ {
from i in [1, 2, 3, 4, 5]
select switch (i) {
case 1 -> '==1'
case [2, 3] -> '<=3'
default -> '>3'
} as result
}
// +--------+
// | result |
// +--------+
// | ==1 |
// | <=3 |
// | <=3 |
// | >3 |
// | >3 |
// +--------+
print(list)
groupby
、having
和聚合函数:分组、筛选和计算
GINQ 也提供了许多实用的聚合函数,如 count()
、min(expression)
、sum(expression)
、avg(expression)
等,更多聚合函数详见此链接:The Apache Groovy programming language - SQL-like querying of collections。
def result = GQ {
from p in [['a', 1], ['a', 3], ['b', 2], ['c', 6]]
// `count()` 相当于 SQL 里的 `COUNT(*)`
select count() as size, max(p[1]) as max_number
}
// +------+------------+
// | size | max_number |
// +------+------------+
// | 3 | 6 |
// +------+------------+
println(result)
使用聚合函数,通常会结合 groupby
进行分组,以及 having
进行筛选:
def result2 = GQ {
from w in ['a', 'bc', 'de', 'fg', 'hik', 'klmn']
groupby w.size() as length
having length < 3
select length, count()
}
// +--------+--------------+
// | length | this.count() |
// +--------+--------------+
// | 1 | 1 |
// | 2 | 3 |
// +--------+--------------+
print(result2)
小结
GINQ 将 C# 的 LINQ 语法以 DSL 的形式带到了 Groovy 中,让我们能以 SQL-like 的方式方便快捷地查询和操作集合。
而本文作为官方文档 Using GINQ 的阅读笔记,只是浅浅涉猎了 GINQ 的一些基础用法,至于像 Window Functions、查询和操作 XML
、JSON
和 TOML
类型文件等进阶用法就留待下次来总结吧。
参考资料
- 官方文档「使用 GINQ」:https://groovy-lang.org/using-ginq.html
- 更多 GINQ 用例:groovy/GinqTest.groovy at master · apache/groovy
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。