实体序列
在前一节中,我们简单了解了如何使用序列 API 获取实体对象,现在我们来对它进行更详细的介绍。
序列简介
要使用序列 API,首先要创建实体序列的对象。一般来说,我们会给 Database
定义一些扩展属性,它们使用 sequenceOf
函数创建序列对象并返回。这些属性可以帮助我们提高代码的可读性:
1 | val Database.departments get() = this.sequenceOf(Departments) |
sequenceOf
函数会返回一个默认的序列,它可以获得表中的所有实体对象。但是请放心,Ktorm 并不会马上执行查询,序列对象提供了一个迭代器 Iterator<E>
,当我们使用它迭代序列中的数据时,查询才会执行。下面我们使用 for-each 循环打印出序列中所有的员工:
1 | for (employee in database.employees) { |
生成的 SQL 如下:
1 | select * |
调用
sequenceOf
函数时,我们可以把withReferences
参数设置为 false,这样就不会自动 left join 关联表,如:database.sequenceOf(Employees, withReferences = false)
除了使用 for-each 循环外,我们还能用 toList
扩展函数将序列中的元素保存为一个列表:
1 | val employees = database.employees.toList() |
我们还能在 toList
之前,使用 filter
扩展函数添加一个筛选条件:
1 | val employees = database.employees.filter { it.departmentId eq 1 }.toList() |
此时生成的 SQL 会变成:
1 | select * |
我们再来看看最核心的 EntitySequence
类的定义:
1 | data class EntitySequence<E : Any, T : BaseTable<E>>( |
可以看出,每个实体序列中都包含了一个查询,而序列的迭代器正是包装了它内部的查询的迭代器。当序列被迭代时,会执行内部的查询,然后使用 entityExtractor
为每行创建一个实体对象。至于序列中的其他属性,比如 sql
、rowSet
、totalRecords
等,也都是直接来自它内部的查询对象,其功能与 Query
类中的同名属性完全相同。
Ktorm 的实体序列 API,大部分都是以扩展函数的方式提供的,这些扩展函数大致可以分为两类:
- 中间操作:这类函数并不会执行序列中的查询,而是修改并创建一个新的序列对象,比如
filter
函数会使用指定的筛选条件创建一个新的序列对象。中间函数的返回值类型通常都是EntitySequence
,以便我们继续链式调用其他序列函数。 - 终止操作:这类函数的返回值通常是一个集合或者是某个计算的结果,他们会马上执行一个查询,然后获取它的结果并执行一定的运算,比如
toList
、reduce
等。
中间操作
就像 kotlin.sequences
一样,EntitySequence
的中间操作并不会迭代序列执行查询,它们都返回一个新的序列对象。EntitySequence
的中间操作主要有如下几个。
filter
1 | inline fun <E : Any, T : BaseTable<E>> EntitySequence<E, T>.filter( |
与 kotlin.sequences
的 filter
函数类似,EntitySequence
的 filter
函数也接受一个闭包作为参数,使用闭包中指定的筛选条件对序列进行过滤。不同的是,我们的闭包接受当前表对象 T
作为参数,因此我们在闭包中使用 it
访问到的并不是实体对象,而是表对象,另外,闭包的返回值也是 ColumnDeclaring<Boolean>
,而不是 Boolean
。下面使用 filter
获取部门 1 中的所有员工:
1 | val employees = database.employees.filter { it.departmentId eq 1 }.toList() |
可以看到,用法几乎与 kotlin.sequences
完全一样,不同的仅仅是在 lambda 表达式中的等号 ==
被这里的 eq
函数代替了而已。filter
函数还可以连续使用,此时所有的筛选条件将使用 and
运算符进行连接,比如:
1 | val employees = database.employees |
生成 SQL:
1 | select * |
其实,Ktorm 还提供了一个 filterNot
函数,它的用法与 filter
一样,但是会将闭包中的筛选条件取反。比如上面例子中的第二个 filter
调用就可以改写为 filterNot { it.managerId.isNull() }
。除此之外,Ktorm 还提供了 filterTo
和 filterNotTo
,但这两个函数其实是终止操作,它们会在添加筛选条件之后马上迭代这个序列,将里面的元素添加到给定的集合中,其效果相当于连续调用 filter
和 toCollection
两个函数。
filterColumns
1 | inline fun <E : Any, T : BaseTable<E>> EntitySequence<E, T>.filterColumns( |
实体序列默认会查询当前表对象和关联表对象(如果启用的话)中的的所有列,这有时会造成一定的性能损失,如果你对这些损失比较敏感的话,可以使用 filterColumns
函数。这个函数支持我们定制查询中的列,比如我们需要获取公司的部门列表,但是不需要部门的地址数据,代码可以这样写:
1 | val departments = database.departments |
这时,返回的实体对象中将不再有 location
字段,生成的 SQL 如下:
1 | select t_department.id as t_department_id, t_department.name as t_department_name |
sortedBy
1 | inline fun <E : Any, T : BaseTable<E>> EntitySequence<E, T>.sortedBy( |
sortedBy
函数用于指定查询结果的排序方式,我们在闭包中返回一个字段或一个表达式,然后 Ktorm 就会使用它对结果进行排序。下面的代码按工资从低到高对员工进行排序:
1 | val employees = database.employees.sortedBy { it.salary }.toList() |
生成 SQL:
1 | select * |
sortedBy
函数默认按升序进行排序,如果你希望使用降序,可以改用 sortedByDescending
函数,它的用法是一样的。
有时候,我们的排序需要考虑多个不同的字段,这时我们可以给 sortedBy
函数传入多个 lambda 表达式。下面是一个使用示例,它将员工按工资从高到低排序,在工资相等的情况下,再按入职时间从远到近排序:
1 | val employees = database.employees |
生成 SQL:
1 | select * |
drop/take
1 | fun <E : Any, T : BaseTable<E>> EntitySequence<E, T>.drop(n: Int): EntitySequence<E, T> |
drop
和 take
函数用于实现分页的功能,drop
函数会丢弃序列中的前 n 个元素,take
函数会保留前 n 个元素丢弃后面的元素。下面是一个例子:
1 | val employees = database.employees.drop(1).take(1).toList() |
如果我们使用 MySQL 数据库,会生成如下 SQL:
1 | select * |
需要注意的是,这两个函数依赖于数据库本身的分页功能,然而 SQL 标准中并没有规定如何进行分页查询的语法,每种数据库提供商对其都有不同的实现。因此,使用这两个函数,我们必须开启某个方言的支持,具体请参考 查询 - limit 一节的相关描述。
终止操作
实体序列的终止操作会马上执行一个查询,获取查询的结果,然后执行一定的计算,下面介绍 Ktorm 为 EntitySequence
提供的一些终止操作,他们其实与 kotlin.sequences
的终止操作几乎一样。
toCollection
1 | fun <E : Any, C : MutableCollection<in E>> EntitySequence<E, *>.toCollection(destination: C): C |
toCollection
函数用于获取序列中的所有元素,它会马上执行查询,迭代查询结果中的元素,把它们添加到 destination
集合中:
1 | val employees = database.employees.toCollection(ArrayList()) |
除此之外,Ktorm 还提供了一些简便的 toXxx
系列函数,用于将序列中的元素保存为特定类型的集合,它们分别是:toList
、toMutableList
、toSet
、toMutableSet
、toHashSet
、toSortedSet
。
map/flatMap
1 | inline fun <E : Any, R> EntitySequence<E, *>.map(transform: (E) -> R): List<R> |
根据以往函数式编程的经验,你很可能会认为 map
和 flatMap
是中间操作,但是很遗憾,在 Ktorm 中,它们是终止操作,这是我们在设计上的一个妥协。
map
函数会马上执行查询,迭代查询结果中的元素,对每一个元素都应用参数 transform
所指定的转换,然后把转换的结果保存到一个列表中返回。flatMap
也会马上执行查询,它与 map
的区别,熟悉函数式编程的同学都能一眼看出来,在此不赘述。
下面的代码可以获取所有员工的名字:
1 | val names = database.employees.map { it.name } |
生成 SQL:
1 | select * |
请注意,虽然在这里我们只需要获取员工的名字,但是生成的 SQL 仍然查询了所有的字段,这是因为 Ktorm 无法通过我们传入的 transform
函数识别出所需的具体字段。如果你对这点性能的损失比较敏感,可以把 map
函数与 filterColumns
函数配合使用,也可以使用下面将要介绍的 mapColumns
函数代替。
除了基本的 map
函数,Ktorm 还提供了 mapTo
、mapIndexed
、mapIndexedTo
等,他们的功能与 kotlin.sequences
中的同名函数是一样的,在此也不再赘述。
mapColumns
1 | inline fun <E : Any, T : BaseTable<E>, C : Any> EntitySequence<E, T>.mapColumns( |
mapColumns
函数的功能与 map
类似,不同的是,它的闭包函数接受当前表对象 T
作为参数,因此我们在闭包中使用 it
访问到的并不是实体对象,而是表对象,另外,闭包的返回值也是 ColumnDeclaring<C>
,我们需要在闭包中返回希望从数据库中查询的列或表达式。还是前面的例子,使用 mapColumns
获取所有员工的名字:
1 | val names = database.employees.mapColumns { it.name } |
可以看到,这时生成的 SQL 中就只包含了我们需要的字段:
1 | select t_employee.name |
如果你希望 mapColumns
能一次查询多个字段,可以在闭包中使用 tupleOf
包装我们的这些字段,函数的返回值就相应变成了 List<TupleN<C1?, C2?, .. Cn?>>
。下面的例子会打印出部门 1 中所有员工的 ID,姓名和入职天数:
1 | database.employees |
运行上面的代码,会产生如下输出:
1 | 1:vince:473 |
生成 SQL:
1 | select t_employee.id, t_employee.name, datediff(?, t_employee.hire_date) |
tupleOf
函数的功能是创建一个元组对象,根据参数个数的不同,它的返回值可以是Tuple2
到Tuple9
,也就是说,我们最多可以使用mapColumns
系列函数一次查询九个字段。但如果我们希望超过九个字段呢?很遗憾,Ktorm 认为这并不是一个常用的功能,如果你确实有这种特殊的需求,可以使用filterColumns
函数或查询 DSL 代替。
除了基本的 mapColumns
函数,Ktorm 还提供了 mapColumnsTo
、mapColumnsNotNull
、mapColumnsNotNullTo
,通过名字你应该也猜到了它们的用法,在此就不重复说明了。
associate
associate
系列函数会马上执行查询,然后迭代查询的结果集,把序列转换为 Map
。它们的用法与 kotlin.sequences
的同名函数一模一样,具体可以参考 Kotlin 标准库的相关文档。
除了基本的 associate
函数以外,Ktorm 还提供了其他的一些变体,它们分别是:associateBy
、associateWith
、associateTo
、associateByTo
、associateWithTo
。
elementAt/first/last/find/findLast/single
这一系列函数用于获取序列中指定位置的元素,它们的用法也与 kotlin.sequences
的同名函数一模一样,具体可以参考 Kotlin 标准库的相关文档。
特别的是,如果我们启用了方言支持的话,这些函数会使用分页功能,尽量只查询一条数据。假如我们使用 MySQL,并且使用 elementAt(10)
获取下标为 10 的记录的话,会生成 limit 10, 1
这样的 SQL。但如果分页功能不可用,则会查出所有的记录,然后再根据下标获取指定元素。
另外,除了基本的形式外,这些函数还具有许多的变体,这里就不一一列举了。
fold/reduce/forEach
这一系列函数及其变体为序列提供了迭代、折叠等功能,它们的用法也与 kotlin.sequences
的同名函数一模一样,具体可以参考 Kotlin 标准库的相关文档。下面使用 fold
计算所有员工的工资总和:
1 | val totalSalary = database.employees.fold(0L) { acc, employee -> acc + employee.salary } |
当然,如果仅仅为了获得工资总和,我们没必要这样做。这是性能低下的写法,它会查询出所有员工的数据,然后对它们进行迭代,这里仅用作示范,更好的写法是使用 sumBy
函数:
1 | val totalSalary = database.employees.sumBy { it.salary } |
joinTo/joinToString
这两个函数提供了将序列中的元素组装为字符串的功能,它们的用法也与 kotlin.sequences
的同名函数一模一样,具体可以参考 Kotlin 标准库的相关文档。
下面使用 joinToString
把所有员工的名字拼成一个字符串:
1 | val names = database.employees.joinToString(separator = ":") { it.name } |