Ktorm Extension: Condition Table
作为 Ktorm 的一个简易扩展库,
为 Ktorm 提供简易扩展类 ConditionTable
,
提供支持通过实体类进行查询的api。
⚠️ 仍在建设中
此扩展库发布于Maven**仓库,你可以使用 Maven 或 Gradle 等任何支持的手段来使用本扩展。
Maven
<!-- 环境中必须存在 ktorm 依赖 -->
<dependency>
<groupId>org.ktorm</groupId>
<artifactId>ktorm-core</artifactId>
<version>${ktorm.version}</version>
</dependency>
<dependency>
<groupId>love.forte.ktorm-extension</groupId>
<artifactId>ktorm-extension-condition-table</artifactId>
<!-- TODO -->
<version>${ktorm-condition-table.version}</version>
</dependency>
Gradle Groovy
// 环境中必须存在 ktorm 依赖
compile "org.ktorm:ktorm-core:${ktorm.version}"
compile "love.forte.ktorm-extension:ktorm-extension-condition-table:${ktorm-condition-table.version}"
Gradle Kotlin DSL
// 环境中必须存在 ktorm 依赖
implementation("org.ktorm:ktorm-core:${ktorm.version}")
implementation("love.forte.ktorm-extension:ktorm-extension-condition-table:${ktorm-condition-table.version}")
首先,创建用于映射数据库表结构的 Kotlin Object。先看看在普通的 ktorm-core
中是如何使用的。
在定义表结构映射之前,先定义实体类(接口):
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
}
然后使用 kotlin object 实现 Table<E>
来提供对数据库表结构的映射:
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 }
}
摘自 ktorm README.md
那么,如果希望拥有一个支持实体类查询API的表结构,那么你需要这样定义你的映射对象:
object Departments : ConditionalTable<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 : ConditionalTable<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 }
}
大致的变化就是将 Table<E>
变化为 ConditionalTable<E>
。
ConditionalTable<E>
扩展了 Table<E>
,为字段提供了定义作为实体查询时的条件函数,如下示例:
object Departments : ConditionalTable<Department>("t_department") {
val id = int("id").primaryKey().bindTo { it.id }.conditionOn { column, value -> // this: Department
if (value != null) column eq value else column eq 1
}
val name = varchar("name").bindTo { it.name }.conditionNotNullOn { column, value -> // this: Department
column like "%$value%"
}
val location = varchar("location").bindTo { it.location } // No conditions will be generated for this field(column)
}
- 对于
id
字段,使用了conditionOn { ... }
。当使用实体类查询的时候, 会自动向条件中填充if (value != null) column eq value else column eq 1
的结果, 即如果id
不为null,则SQL中相当于value = ${id}
; 如果id
为null,则SQL中相当于value = 1
。
- 对于
name
字段,使用了conditionNotNullOn { ... }
。当使用实体类查询的时候,只有在实体类中的name
不为null的时候, 自动向条件中填充column like "%$value%"
的结果, 即如果name
不为null,则SQL中(类似的)相当于name LIKE CONCAT('%', ${name}, '%')
; 如果name
为null,则不生成。
- 对于
location
字段,由于没有定义条件,因此不会产生条件。
conditionOn { ... }
和conditionNotNullOn { ... }
理论上是可以重复使用的, 多次调用后,查询时会产生两次条件的并集。( condition1 and condition2 )但是一般情况下,为了更好的控制,还是建议只调用一次。
当需要使用查询的时候,通过 EntitySequence
或 Query
的扩展完成:
定义:
interface Department : Entity<Department> {
companion object : Entity.Factory<Department>()
val id: Int
var name: String
var location: LocationWrapper
var mixedCase: String?
}
object Departments : ConditionalTable<Department>("t_department") {
companion object : Departments(null)
override fun aliased(alias: String) = Departments(alias)
val id = int("id").primaryKey().bindTo { it.id }
val name = varchar("name")
.bindTo { it.name }
.conditionNotNullOn { c, v ->
c like "$v%"
}
val location = varchar("location").transform({ LocationWrapper(it) }, { it.underlying }).bindTo { it.location }
val mixedCase = varchar("mixedCase").bindTo { it.mixedCase }
}
val entity = Department {
name = "te"
}
val Database.departments get() = this.sequenceOf(Departments)
使用:
database.departments.filterBy(entity).forEach {
// ...
}
database.from(Departments).select().whereBy(Departments, entity).forEach {
// ...
}
database.departments.filterByOr(entity) { table, condition ->
val extraCondition = table.location eq locationWrapper
condition?.and(extraCondition) ?: extraCondition
}.forEach {
// ...
}
database.from(Departments).select().whereByOr(Departments, entity) {
it?.and(DepartmentsWithCondition.location eq locationWrapper) // or null.
}.forEach {
// ...
}
你可以看到主要涉及的扩展就是如下几个函数:
EntitySequence<E, T>.filterBy
Query.whereBy
EntitySequence<E, T>.filterByOr
Query.whereByOr
它们分别对应了原本的 EntitySequence<E, T>.filter
和 Query.where
。
在这其中,以 By
结尾的函数和 ByOr
结构的函数之间的主要区别在于它们的最后一个可选的 andThen
函数。
以下述代码为例:
// (1)
database.departments.filterBy(entity) { table, condition -> // condition: notnull
condition and (table.location eq locationWrapper) // return: notnull
}.forEach {
// ...
}
// (2)
database.departments.filterByOr(entity) { table, condition -> // condition: nullable
condition?.and(table.location eq locationWrapper) // return: nullable
}.forEach {
// ...
}
可以看出,以 By
结尾的函数中,最后的额外条件函数的参数 condition
和返回值 不可为null ,而 ByOr
接口的函数允许它们为null。