实体查询
Ktorm 提供了一套名为”实体序列”的 API,用来从数据库中获取实体对象。正如其名字所示,它的风格和使用方式与 Kotlin 标准库中的序列 API 极其类似,它提供了许多同名的扩展函数,比如 filter、map、reduce 等。
使用序列 API 获取实体
要使用序列 API,首先要创建实体序列的对象。一般来说,我们会给 Database 定义一些扩展属性,它们使用 sequenceOf 函数创建序列对象并返回。这些属性可以帮助我们提高代码的可读性:
1 | val Database.departments get() = this.sequenceOf(Departments) |
下面的代码使用 find 函数从序列中根据名字获取一个 Employee 对象:
1 | val employee = database.employees.find { it.name eq "vince" } |
这种写法十分自然,就像使用 Kotlin 标准库中的函数从一个集合中筛选符合条件的元素一样,不同的仅仅是在 lambda 表达式中的等号 == 被这里的 eq 函数代替了而已。
find 函数接收一个类型为 (T) -> ColumnDeclaring<Boolean> 的参数,这是一个闭包函数,其中的返回值会作为查询的筛选条件,SQL 执行完毕后,返回结果集中的第一条记录。作为参数的闭包函数本身也接收一个参数 T,这正是当前表对象,因此我们能在闭包中使用 it 引用它。
上述代码生成的 SQL 如下:
1 | select t_employee.id as t_employee_id, t_employee.name as t_employee_name, t_employee.job as t_employee_job, t_employee.manager_id as t_employee_manager_id, t_employee.hire_date as t_employee_hire_date, t_employee.salary as t_employee_salary, t_employee.department_id as t_employee_department_id, _ref0.id as _ref0_id, _ref0.name as _ref0_name, _ref0.location as _ref0_location |
生成 SQL 中包含了十分长的字段列表,这是有必要的,Ktorm 会尽量避免使用 select *,但是为了展示的方便,以后在文档中出现的 SQL,太长的字段列表会使用 select * 代替。
观察生成的 SQL,我们发现 Ktorm 自动使用外键 left join 了 t_employee 的关联表 t_department。这是因为我们在表对象中声明 departmentId 这一列时使用 references 函数将此列绑定到了 Departments 表。在使用序列 API 的时候,Ktorm 会自动递归地 left join 所有关联的表,将部门表的数据一并查询出来,填充到 Employee.department 属性中。
在使用
references绑定时请注意避免循环的引用,比如Employees表引用了Departments,则Departments不能再直接或间接地引用Employees,否则会导致 Ktorm 在自动 left join 的过程中出现栈溢出。
既然 Ktorm 会自动 left join 关联表,我们当然也能在筛选条件中使用关联表中的列。下面的代码可以获取一个在广州工作的员工对象,这里我们通过列的 referenceTable 属性获取 departmentId 所引用的表对象:
1 | val employee = database.employees.find { |
为了让代码看起来优雅一点,我们可以在 Employees 表对象中添加一个 get 属性。下面的代码和上面是完全等价的,但是阅读起来却更加自然:
1 | open class Employees(alias: String?) : Table<Employee>("t_employee", alias) { |
生成的 SQL 如下:
1 | select * |
注意:我们在通过
it.departmentId.referenceTable获取到引用的表对象后,把它转型成了Departments,因此这要求我们必须使用 class 而不是 object 定义表对象,并且重写aliased函数,具体参见表别名的相关介绍。
除了 find 函数外,实体序列 API 还给我们提供了许多方便的函数,如使用 filter 对元素进行筛选、使用 groupingBy 进行分组聚合等。与 SQL DSL 相比,实体序列 API 更具有函数式风格,其使用方式就像在操作内存中的集合一样,因此我们建议大家优先使用。更多用法请参见实体序列和序列聚合的文档。
使用查询 DSL 获取实体
序列 API 会自动 left join 引用表,有时这可能会造成一点浪费。如果你希望对查询进行更细粒度的控制,你可以使用前面章节中介绍的查询 DSL,Ktorm 提供了从查询 DSL 中获取实体对象的方法。
下面的例子使用 createEntity 函数从查询 DSL 中获取了实体对象的列表:
1 | val employees = database |
在这里,我们使用 map 函数对查询进行迭代,在迭代中使用 createEntity 为每一行返回的记录创建一个实体对象。createEntity 是 Table 类的函数,它会根据表对象中的列绑定配置,自动创建实体对象,从结果集中读取数据填充到实体对象的各个属性中。如果该表使用 references 引用绑定了其它表,也会递归地对所引用的表调用 createEntity 创建关联的实体对象。
然而,查询 DSL 返回的列是可自定义的,里面不一定包含引用表中的列。针对这种情况,createEntity 函数提供了一个名为 withReferences 的参数,它的值默认是 true。但当我们把它设为 false 时,createEntity 将不再获取引用表关联的实体对象的数据,它会把引用绑定视为到其所引用的实体对象的主键的嵌套绑定,例如 c.references(Departments) { it.department },在它眼里相当于 c.bindTo { it.department.id },使用这个参数可避免一些不必要的对象创建。
1 | Employees.createEntity(row, withReferences = false) |
但是在上面的查询中,因为我们并没有联表,因此不管我们把 withReferences 参数设置成什么,都会生成一句简单的 SQL select * from t_employee order by t_employee.id,打印出同样的结果:
1 | Employee{id=1, name=vince, job=engineer, hireDate=2018-01-01, salary=100, department=Department{id=1}} |
joinReferencesAndSelect
joinReferencesAndSelect 是 QuerySource 的扩展函数,它创建一个 Query 对象,这个查询递归地 left join 当前表对象的所有关联表,并且 select 出它们的所有列。你可以直接从这个返回的 Query 对象中获取所有的记录,也可以紧接着调用 Query 类的其他扩展方法修改这个查询。实际上,实体序列 API 就是基于这个函数实现自动联表的。
下面是一个使用示例,这个查询获取所有的员工及其所在的部门的信息,并按员工的 ID 进行排序:
1 | val employees = database |
生成的 SQL 如下:
1 | select * |
从生成的 SQL 中可以看出,上面的查询实际上相当于手动调用 leftJoin 函数,例如下面的代码与上面例子中的查询就是完全等价的,但是使用 joinReferencesAndSelect 可以为我们减少一些样板代码。
1 | val emp = Employees |

