简体中文 | English
编辑本页

快速上手

Ktorm 已经发布到 maven 中央仓库,因此,如果你使用 maven 的话,只需要在 pom.xml 文件里面添加一个依赖:

1
2
3
4
5
<dependency>
<groupId>org.ktorm</groupId>
<artifactId>ktorm-core</artifactId>
<version>${ktorm.version}</version>
</dependency>

或者 gradle:

1
compile "org.ktorm:ktorm-core:${ktorm.version}"

首先,创建 Kotlin object,描述你的表结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object Departments : Table<Nothing>("t_department") {
val id = int("id").primaryKey()
val name = varchar("name")
val location = varchar("location")
}

object Employees : Table<Nothing>("t_employee") {
val id = int("id").primaryKey()
val name = varchar("name")
val job = varchar("job")
val managerId = int("manager_id")
val hireDate = date("hire_date")
val salary = long("salary")
val departmentId = int("department_id")
}

然后,连接到数据库,执行一个简单的查询:

1
2
3
4
5
6
7
fun main() {
val database = Database.connect("jdbc:mysql://localhost:3306/ktorm", user = "root", password = "***")

for (row in database.from(Employees).select()) {
println(row[Employees.name])
}
}

现在,你可以执行这个程序了,Ktorm 会生成一条 SQL select * from t_employee,查询表中所有的员工记录,然后打印出他们的名字。 因为 select 函数返回的查询对象重载了迭代运算符,所以你可以在这里使用 for-each 循环的语法。

SQL DSL

让我们在上面的查询里再增加一点筛选条件:

1
2
3
4
5
6
7
database
.from(Employees)
.select(Employees.name)
.where { (Employees.departmentId eq 1) and (Employees.name like "%vince%") }
.forEach { row ->
println(row[Employees.name])
}

生成的 SQL 如下:

1
2
3
select t_employee.name as t_employee_name 
from t_employee
where (t_employee.department_id = ?) and (t_employee.name like ?)

这就是 Kotlin 的魔法,使用 Ktorm 写查询十分地简单和自然,所生成的 SQL 几乎和 Kotlin 代码一一对应。并且,Ktorm 是强类型的,编译器会在你的代码运行之前对它进行检查,IDE 也能对你的代码进行智能提示和自动补全。

动态查询,根据不同的情况在 where 子句中增加不同的筛选条件:

1
2
3
4
5
6
7
8
9
10
11
val query = database
.from(Employees)
.select(Employees.name)
.whereWithConditions {
if (someCondition) {
it += Employees.managerId.isNull()
}
if (otherCondition) {
it += Employees.departmentId eq 1
}
}

聚合查询:

1
2
3
4
5
6
7
8
9
val t = Employees.aliased("t")
database
.from(t)
.select(t.departmentId, avg(t.salary))
.groupBy(t.departmentId)
.having { avg(t.salary) gt 100.0 }
.forEach { row ->
println("${row.getInt(1)}:${row.getDouble(2)}")
}

Union:

1
2
3
4
5
6
7
8
9
10
val query = database
.from(Employees)
.select(Employees.id)
.unionAll(
database.from(Departments).select(Departments.id)
)
.unionAll(
database.from(Departments).select(Departments.id)
)
.orderBy(Employees.id.desc())

多表连接查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
data class Names(val name: String?, val managerName: String?, val departmentName: String?)

val emp = Employees.aliased("emp")
val mgr = Employees.aliased("mgr")
val dept = Departments.aliased("dept")

val results = database
.from(emp)
.leftJoin(dept, on = emp.departmentId eq dept.id)
.leftJoin(mgr, on = emp.managerId eq mgr.id)
.select(emp.name, mgr.name, dept.name)
.orderBy(emp.id.asc())
.map { row ->
Names(
name = row[emp.name],
managerName = row[mgr.name],
departmentName = row[dept.name]
)
}

插入:

1
2
3
4
5
6
7
8
database.insert(Employees) {
set(it.name, "jerry")
set(it.job, "trainee")
set(it.managerId, 1)
set(it.hireDate, LocalDate.now())
set(it.salary, 50)
set(it.departmentId, 1)
}

更新:

1
2
3
4
5
6
7
8
database.update(Employees) {
set(it.job, "engineer")
set(it.managerId, null)
set(it.salary, 100)
where {
it.id eq 2
}
}

删除:

1
database.delete(Employees) { it.id eq 4 }

更多 SQL DSL 的用法,请参考具体文档

实体类与列绑定

除了 SQL DSL 以外,Ktorm 也支持实体对象。首先,我们需要定义实体类,然后在表对象中使用 bindTo 函数将表与实体类进行绑定。在 Ktorm 里面,我们使用接口定义实体类,继承 Entity<E> 即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Department : Entity<Department> {
companion object : Entity.Factory<Department>()
val id: Int
var name: String
var location: String
}

interface Employee : Entity<Employee> {
companion object : Entity.Factory<Employee>()
val id: Int
var name: String
var job: String
var manager: Employee?
var hireDate: LocalDate
var salary: Long
var department: Department
}

修改前面的表对象,把数据库中的列绑定到实体类的属性上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object Departments : Table<Department>("t_department") {
val id = int("id").primaryKey().bindTo { it.id }
val name = varchar("name").bindTo { it.name }
val location = varchar("location").bindTo { it.location }
}

object Employees : Table<Employee>("t_employee") {
val id = int("id").primaryKey().bindTo { it.id }
val name = varchar("name").bindTo { it.name }
val job = varchar("job").bindTo { it.job }
val managerId = int("manager_id").bindTo { it.manager.id }
val hireDate = date("hire_date").bindTo { it.hireDate }
val salary = long("salary").bindTo { it.salary }
val departmentId = int("department_id").references(Departments) { it.department }
}

命名规约:强烈建议使用单数名词命名实体类,使用名词的复数形式命名表对象,如:Employee/Employees、Department/Departments。

完成列绑定后,我们就可以使用序列 API 对实体进行各种灵活的操作。我们先给 Database 定义两个扩展属性,它们使用 sequenceOf 函数创建序列对象并返回。这两个属性可以帮助我们提高代码的可读性:

1
2
val Database.departments get() = this.sequenceOf(Departments)
val Database.employees get() = this.sequenceOf(Employees)

下面的代码使用 find 函数从序列中根据名字获取一个 Employee 对象:

1
val employee = database.employees.find { it.name eq "vince" }

我们还能使用 filter 函数对序列进行筛选,比如获取所有名字为 vince 的员工:

1
val employees = database.employees.filter { it.name eq "vince" }.toList()

findfilter 函数都接受一个 lambda 表达式作为参数,使用该 lambda 的返回值作为条件,生成一条查询 SQL。可以看到,生成的 SQL 自动 left join 了关联表 t_department

1
2
3
4
select * 
from t_employee
left join t_department _ref0 on t_employee.department_id = _ref0.id
where t_employee.name = ?

将实体对象保存到数据库:

1
2
3
4
5
6
7
8
9
val employee = Employee {
name = "jerry"
job = "trainee"
hireDate = LocalDate.now()
salary = 50
department = database.departments.find { it.name eq "tech" }
}

database.employees.add(employee)

将内存中实体对象的变化更新到数据库:

1
2
3
4
val employee = database.employees.find { it.id eq 2 } ?: return
employee.job = "engineer"
employee.salary = 100
employee.flushChanges()

从数据库中删除实体对象:

1
2
val employee = database.employees.find { it.id eq 2 } ?: return
employee.delete()

更多实体 API 的用法,可参考列绑定实体查询相关的文档。

实体序列 API

Ktorm 提供了一套名为”实体序列”的 API,用来从数据库中获取实体对象。正如其名字所示,它的风格和使用方式与 Kotlin 标准库中的序列 API 极其类似,它提供了许多同名的扩展函数,比如 filtermapreduce 等。

Ktorm 的实体序列 API,大部分都是以扩展函数的方式提供的,这些扩展函数大致可以分为两类,它们分别是中间操作和终止操作。

中间操作

这类操作并不会执行序列中的查询,而是修改并创建一个新的序列对象,比如 filter 函数会使用指定的筛选条件创建一个新的序列对象。下面使用 filter 获取部门 1 中的所有员工:

1
val employees = database.employees.filter { it.departmentId eq 1 }.toList()

可以看到,用法几乎与 kotlin.sequences 完全一样,不同的仅仅是在 lambda 表达式中的等号 == 被这里的 eq 函数代替了而已。filter 函数还可以连续使用,此时所有的筛选条件将使用 and 运算符进行连接,比如:

1
2
3
4
val employees = database.employees
.filter { it.departmentId eq 1 }
.filter { it.managerId.isNotNull() }
.toList()

生成 SQL:

1
2
3
4
select * 
from t_employee
left join t_department _ref0 on t_employee.department_id = _ref0.id
where (t_employee.department_id = ?) and (t_employee.manager_id is not null)

使用 sortedBysortedByDescending 对序列中的元素进行排序:

1
val employees = database.employees.sortedBy { it.salary }.toList()

使用 droptake 函数进行分页:

1
val employees = database.employees.drop(1).take(1).toList()

终止操作

实体序列的终止操作会马上执行一个查询,获取查询的执行结果,然后执行一定的计算。for-each 循环就是一个典型的终止操作,下面我们使用 for-each 循环打印出序列中所有的员工:

1
2
3
for (employee in database.employees) {
println(employee)
}

生成的 SQL 如下:

1
2
3
select * 
from t_employee
left join t_department _ref0 on t_employee.department_id = _ref0.id

toCollectiontoList 等方法用于将序列中的元素保存为一个集合:

1
val employees = database.employees.toCollection(ArrayList())

mapColumns 函数用于获取指定列的结果:

1
val names = database.employees.mapColumns { it.name }

除此之外,mapColumns 还可以同时获取多个列的结果,这时我们只需要在闭包中使用 tupleOf 包装我们的这些字段,函数的返回值也相应变成了 List<TupleN<C1?, C2?, .. Cn?>>

1
2
3
4
5
6
database.employees
.filter { it.departmentId eq 1 }
.mapColumns { tupleOf(it.id, it.name) }
.forEach { (id, name) ->
println("$id:$name")
}

生成 SQL:

1
2
3
select t_employee.id, t_employee.name
from t_employee
where t_employee.department_id = ?

其他我们熟悉的序列函数也都支持,比如 foldreduceforEach 等,下面使用 fold 计算所有员工的工资总和:

1
val totalSalary = database.employees.fold(0L) { acc, employee -> acc + employee.salary }

序列聚合

实体序列 API 不仅可以让我们使用类似 kotlin.sequences 的方式获取数据库中的实体对象,它还支持丰富的聚合功能,让我们可以方便地对指定字段进行计数、求和、求平均值等操作。

下面使用 aggregateColumns 函数获取部门 1 中工资的最大值:

1
2
3
val max = database.employees
.filter { it.departmentId eq 1 }
.aggregateColumns { max(it.salary) }

如果你希望同时获取多个聚合结果,只需要在闭包中使用 tupleOf 包装我们的这些聚合表达式即可,此时函数的返回值就相应变成了 TupleN<C1?, C2?, .. Cn?>。下面的例子获取部门 1 中工资的平均值和极差:

1
2
3
val (avg, diff) = database.employees
.filter { it.departmentId eq 1 }
.aggregateColumns { tupleOf(avg(it.salary), max(it.salary) - min(it.salary)) }

生成 SQL:

1
2
3
select avg(t_employee.salary), max(t_employee.salary) - min(t_employee.salary) 
from t_employee
where t_employee.department_id = ?

除了直接使用 aggregateColumns 函数以外,Ktorm 还为序列提供了许多方便的辅助函数,他们都是基于 aggregateColumns 函数实现的,分别是 countanynoneallsumBymaxByminByaverageBy

下面改用 maxBy 函数获取部门 1 中工资的最大值:

1
2
3
val max = database.employees
.filter { it.departmentId eq 1 }
.maxBy { it.salary }

除此之外,Ktorm 还支持分组聚合,只需要先调用 groupingBy,再调用 aggregateColumns。下面的代码可以获取所有部门的平均工资,它的返回值类型是 Map<Int?, Double?>,其中键为部门 ID,值是各个部门工资的平均值:

1
2
3
val averageSalaries = database.employees
.groupingBy { it.departmentId }
.aggregateColumns { avg(it.salary) }

生成 SQL:

1
2
3
select t_employee.department_id, avg(t_employee.salary) 
from t_employee
group by t_employee.department_id

在分组聚合时,Ktorm 也提供了许多方便的辅助函数,它们是 eachCount(To)eachSumBy(To)eachMaxBy(To)eachMinBy(To)eachAverageBy(To)。有了这些辅助函数,上面获取所有部门平均工资的代码就可以改写成:

1
2
3
val averageSalaries = database.employees
.groupingBy { it.departmentId }
.eachAverageBy { it.salary }

除此之外,Ktorm 还提供了 aggregatefoldreduce 等函数,它们与 kotlin.collections.Grouping 的相应函数同名,功能也完全一样。下面的代码使用 fold 函数计算每个部门工资的总和:

1
2
3
4
5
val totalSalaries = database.employees
.groupingBy { it.departmentId }
.fold(0L) { acc, employee ->
acc + employee.salary
}

更多实体序列 API 的用法,可参考实体序列序列聚合相关的文档。