2
头图

[!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 还是有一些区别的,比如最为明显的就是 selectfrom 出现的顺序不一样。

GINQ 基础语法

话不多说,先看一个仅需 fromselect 的简单用例:

// 作为 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 后面跟随的是数据源,当前支持的数据源类型有 IterableStream、数组和 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]

当然也有 existsnot 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 的排序,我们可以通过在 ascdesc 中传入 nullsfirstnullslast 来指定:

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 也分为:

  • 内联:如 joininnerjoininnerhashjoin
  • 外联:如 lefthashjoinrighthashjoin
  • 其他:如 fulljoincrossjoin

使用方式和我们在 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」的版本,如 innerhashjoinlefthashjoin 等。不过对于内联的 innerjoininnerhashjoin,我们都可以统一使用 join,Groovy 也会为我们智能的选择使用 innerjoininnerhashjoin,还能让代码拥有更好的可读性。

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)

groupbyhaving 和聚合函数:分组、筛选和计算

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、查询和操作 XMLJSONTOML 类型文件等进阶用法就留待下次来总结吧。

参考资料


Sooxin
6 声望0 粉丝