简体中文 | English
编辑本页

运算符

在前面的章节中,我们已经对运算符有了一定的了解,现在,我们来对它进行详细的介绍。

内置运算符

Ktorm 的每个运算符实际上都是一个返回 SqlExpression 的 Kotlin 函数,下面是目前我们支持的所有运算符的列表及使用示例:

Kotlin 函数名SQL 关键字/符号使用示例
isNullis nullKtorm: Employees.name.isNull()
SQL: t_employee.name is null
isNotNullis not nullKtorm: Employees.name.isNotNull()
SQL: t_employee.name is not null
unaryMinus(-)-Ktorm: -Employees.salary
SQL: -t_employee.salary
unaryPlus(+)+Ktorm: +Employees.salary
SQL: +t_employee.salary
not(!)notKtorm: !Employees.name.isNull()
SQL: not (t_employee.name is null)
plus(+)+Ktorm: Employees.salary + Employees.salary
SQL: t_employee.salary + t_employee.salary
minus(-)-Ktorm: Employees.salary - Employees.salary
SQL: t_employee.salary - t_employee.salary
times(*)*Ktorm: Employees.salary * 2
SQL: t_employee.salary * 2
div(/)/Ktorm: Employees.salary / 2
SQL: t_employee.salary / 2
rem(%)%Ktorm: Employees.id % 2
SQL: t_employee.id % 2
likelikeKtorm: Employees.name like “vince”
SQL: t_employee.name like ‘vince’
notLikenot likeKtorm: Employees.name notLike “vince”
SQL: t_employee.name not like ‘vince’
andandKtorm: Employees.name.isNotNull() and (Employees.name like “vince”)
SQL: t_employee.name is not null and t_employee.name like ‘vince’
ororKtorm: Employees.name.isNull() or (Employees.name notLike “vince”)
SQL: t_employee.name is null or t_employee.name not like ‘vince’
xorxorKtorm: Employees.name.isNotNull() xor (Employees.name notLike “vince”)
SQL: t_employee.name is not null xor t_employee.name not like ‘vince’
less<Ktorm: Employees.salary less 1000
SQL: t_employee.salary < 1000
lessEq<=Ktorm: Employees.salary lessEq 1000
SQL: t_employee.salary <= 1000
greater>Ktorm: Employees.salary greater 1000
SQL: t_employee.salary > 1000
greaterEq>=Ktorm: Employees.salary greaterEq 1000
SQL: t_employee.salary >= 1000
eq=Ktorm: Employees.id eq 1
SQL: t_employee.id = 1
notEq<>Ktorm: Employees.id notEq 1
SQL: t_employee.id <> 1
betweenbetweenKtorm: Employees.id between 1..3
SQL: t_employee.id between 1 and 3
notBetweennot betweenKtorm: Employees.id notBetween 1..3
SQL: t_employee.id not between 1 and 3
inListinKtorm: Employees.departmentId inList listOf(1, 2, 3)
SQL: t_employee.department_id in (1, 2, 3)
notInListnot inKtorm: Employees.departmentId notInList Departments.selectDistinct(Departments.id)
SQL: t_employee.department_id not in (select distinct t_department.id from t_department)
existsexistsKtorm: exists(Employees.select())
SQL: exists (select * from t_employee)
notExistsnot existsKtorm: notExists(Employees.select())
SQL: not exists (select * from t_employee)

这些运算符按照实现方式大概可以分为两类:

使用 operator 关键字重载的 Kotlin 内置运算符:这类运算符一般用于实现加减乘除等基本的运算,由于重载了 Kotlin 的内置运算符,它们使用起来就像是真的执行了运算一样,比如 Employees.salary + 1000。但实际上并没有,它们只是创建了一个 SQL 表达式,这个表达式会被 SqlFormatter 翻译为 SQL 中的对应符号。下面是加号运算符的代码实现,可以看到,它只是创建了一个 BinaryExpression<T> 而已:

1
2
3
infix operator fun <T : Number> ColumnDeclaring<T>.plus(expr: ColumnDeclaring<T>): BinaryExpression<T> {
return BinaryExpression(BinaryExpressionType.PLUS, asExpression(), expr.asExpression(), sqlType)
}

普通的运算符函数:然而,Kotlin 重载运算符还有许多限制,比如 equals 方法要求必须返回 Boolean,然而 Ktorm 的运算符需要返回 SQL 表达式,因此,Ktorm 提供了另外一个 eq 函数用于相等比较。除此之外,还有许多 SQL 中的运算符在 Kotlin 中并不存在,比如 like,Ktorm 就提供了一个 like 函数用于字符串匹配。下面是 like 函数的实现,这类函数一般都具有 infix 关键字修饰:

1
2
3
4
5
6
7
8
infix fun ColumnDeclaring<*>.like(argument: String): BinaryExpression<Boolean> {
return BinaryExpression(
type = BinaryExpressionType.LIKE,
left = asExpression(),
right = ArgumentExpression(argument, VarcharSqlType),
sqlType = BooleanSqlType
)
}

运算符优先级

运算符可以连续使用,但是,当我们一次使用多个运算符时,它们的优先级就成了一个问题。在一个表达式中可能包含多个运算符,不同的运算顺序可能得出不同的结果甚至出现运算错误,因为当表达式中含多种运算时,必须按一定顺序进行结合,才能保证运算的合理性和结果的正确性、唯一性。

例如 1 + 2 * 3,乘号的优先级比较高,则 2 * 3 优先结合,运算结果为 7;若不考虑运算符的优先级,从前往后结合,那么运算结果为 9,这是完全错误的。一般来说,乘除的优先级高于加减,与的优先级高于或,但是,在 Ktorm 中,情况却有些不同。

对于重载的 Kotlin 内置运算符,其优先级遵循 Kotlin 语言自己的规范。例如表达式 Employees.salary + 1000 * 2,由于乘号的优先级较高,最终翻译出来的 SQL 是 t_employee.salary + 2000

但是对于普通的运算符函数,却并没有优先级一说。在 Kotlin 语言的层面,它们实际上都只是普通的函数调用,因此只需要遵循从前往后结合的原则,尽管这有时可能会违反我们的直觉。比如 a or b and c,这里的 orand 都是运算符函数,直觉上,and 的优先级应该比 or 高,因此应该优先结合,但实际上,它们只是普通的 Kotlin 函数而已。如果对这一点没有清楚的认识,可能导致一些意料之外的 bug,为了解决这个问题,我们可以在需要的地方使用括号,比如 a or (b and c)

关于表达式优先级的具体顺序,请参考 Kotlin 语言规范中的相关规定。

自定义运算符

前面已经介绍过 Ktorm 核心模块的内置运算符,这些运算符为标准 SQL 中的运算符提供了支持,但如果我们想使用一些数据库方言中特有的运算符呢?下面我们以 PostgreSQL 中的 ilike 运算符为例,了解如何增加自己的运算符。

ilike 是 PostgreSQL 中特有的运算符,它的功能与 like 一样,也是进行字符串匹配,但是忽略大小写。我们首先创建一个表达式类型,它继承于 ScalarExpression<Boolean>,表示一个 ilike 操作:

1
2
3
4
5
6
data class ILikeExpression(
val left: ScalarExpression<*>,
val right: ScalarExpression<*>,
override val sqlType: SqlType<Boolean> = BooleanSqlType,
override val isLeafNode: Boolean = false
) : ScalarExpression<Boolean>()

有了表达式类型之后,我们只需要再增加一个扩展函数,这就是运算符函数,为了函数使用起来真的像一个运算符,我们需要添加 infix 关键字:

1
2
3
infix fun ColumnDeclaring<*>.ilike(argument: String): ILikeExpression {
return ILikeExpression(asExpression(), ArgumentExpression(argument, VarcharSqlType)
}

这样我们就能使用这个运算符函数了,就像使用其他运算符一样。不过现在 Ktorm 还无法识别我们自己创建的 ILikeExpression,无法为我们生成正确的 SQL,跟之前一样,我们需要扩展 SqlFormatter 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class PostgreSqlFormatter(database: Database, beautifySql: Boolean, indentSize: Int)
: SqlFormatter(database, beautifySql, indentSize) {

override fun visitUnknown(expr: SqlExpression): SqlExpression {
if (expr is ILikeExpression) {
if (expr.left.removeBrackets) {
visit(expr.left)
} else {
write("(")
visit(expr.left)
removeLastBlank()
write(") ")
}

write("ilike ")

if (expr.right.removeBrackets) {
visit(expr.right)
} else {
write("(")
visit(expr.right)
removeLastBlank()
write(") ")
}

return expr
} else {
super.visitUnknown(expr)
}
}
}

接下来的事情就是使用方言(Dialect)支持将这个自定义的 SqlFormatter 注册到 Ktorm 中了,关于如何启用方言,可参考后面的章节。