环境
- 编辑器:IntelliJ IDEA 2024.3 (Ultimate Edition)
- Kotlin 版本:2.0.21
- Exposed 版本:0.56.0,利用 DSL 操作数据库
问题描述
最近在使用 Exposed DSL 中的 deleteWhere
或是 deleteIgnoreWhere
方法的时候,遇到了 eq
操作符无法在 IDEA 中解析的问题,参考下图情况:
但是在 where
方法当中,eq
操作符是能够正常使用的:
对此感到有些疑惑,因为它应当是能够正常工作的。
分析问题
我们来仔细看一下它们的区别。
where
的定义
public final fun where(
predicate: SqlExpressionBuilder.() -> Op<Boolean>
): Query
我们可以发现,where
函数接受了一个参数:
- 参数名为
predicate
- 参数类型为一个函数(Function)
- 这个函数拓展自
SqlExpressionBuilder
,使得 lambda 表达式可以使用其成员 - 函数的返回值是类型为
Boolean
的 SQL 条件表达式对象Op
deleteWhere
的定义
inline fun <T : Table> T.deleteWhere(
limit: Int? = null,
op: T.(ISqlExpressionBuilder) -> Op<Boolean>
): Int =
DeleteStatement.where(TransactionManager.current(), this@deleteWhere, op(SqlExpressionBuilder), false, limit)
这是一个泛型内联拓展函数:
-
拓展自泛型
T
,该泛型支持任何继承自Table
的类型 -
第二参数
op
接受一个函数:- 扩展自泛型
T
- 参数类型为
ISqlExpressionBuilder
- 返回值为
Boolean
的 SQL 条件表达式对象Op
并且不难发现,这个函数传入的是一个
SqlExpressionBuilder
对象。 - 扩展自泛型
看到这我们可以发现,它们虽然在官方文档中用起来几乎一样,但本身的细节还是有差异的。
eq
运算符的定义
通过 Exposed 的源代码找到 org.jetbrains.exposed.sql.SQLExpressionBuilder
中,对于 eq
运算符的定义:
interface ISqlExpressionBuilder {
/** Checks if this expression is equal to some [t] value. */
@LowPriorityInOverloadResolution
infix fun <T> ExpressionWithColumnType<T>.eq(t: T): Op<Boolean> = when {
t == null -> isNull()
(this as? Column<*>)?.isEntityIdentifier() == true -> table.mapIdComparison(t, ::EqOp)
else -> EqOp(this, wrap(t))
}
...
}
可以看到,这个函数是 ISqlExpressionBuilder
接口的一个成员,用于检查当前的表达式是否等于某个值 T
,这里的 T
可以为任意类型。经过比对后,函数返回一个布尔值 true
或者 false
。
infix
关键字用于表示该函数为 中缀函数,可以自然语言的形式调用,比如:column eq value
。它是 Kotlin 提供的一种语法糖,可以让咱们的开发更便捷。
SqlExpressionBuilder
的定义
在 org.jetbrains.exposed.sql.SQLExpressionBuilder
源码中:
object SqlExpressionBuilder : ISqlExpressionBuilder
也就是说,SqlExpressionBuilder
这个对象继承自 ISqlExpressionBuilder
这一接口。自然,SqlExpressionBuilder
可以访问 ISqlExpressionBuilder
中的所有成员。
问题根本
不难发现,问题的根本原因就在于其传入的函数的 作用域不相同。
- 对于
where
函数,默认传入了SqlExpressionBuilder
的环境。 - 对于
deleteWhere
,默认提供目标数据表T: Table
环境,以及ISqlExpressionBuilder
作为参数传入。
此时,给 where
传入一个 lambda 函数,其 this
自然会指向一个 SqlExpressionBuilder
对象。
但是如果给 deleteWhere
传入一个 lambda 函数,其 this
指向的是 T: Table
,我们想要的的 SqlExpressionBuilder
对象被放在了参数 it
当中。自然,通过 this
是无法访问到 ISqlExpressionBuilder
的成员函数 eq
的。
解决办法
既然发现了问题所在,就可以解决问题了。
方法一:利用 Op.build()
方法
最简单的办法就是利用 Op
提供的 build
方法:
XXTable.deleteWhere {
Op.build { XXTable.id eq id }
}
让其直接返回一个通过 SqlExpressionBuilder
环境构造的 SQL 条件表达式对象 Op
即可。
方法二:导入对应的操作符(推荐)
这个办法是我在 Issue#1698 当中发现的,应当是出现在 Exposed 0.40.X 版本之后。
因为 IDEA 默认只会根据上下文决定要引入的包。一般情况下,deleteWhere
传入的匿名函数并不是扩展自 SqlExpressionBuilder
的,因此我们只需要重新导入一下 org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
:
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
XXTable.deleteWhere {
XXTable.id eq id
}
此时,你的上下文环境中就存在 eq
的定义,而通过 eq
操作符构造的表达式,最终返回的结果就是 Op<Boolean>
对象,正是 deleteWhere
所需的参数函数的返回值,所以在此就可以正常使用。
之所以推荐这个办法,是因为它是最省时省力的。你只需要在文件头部进行对应的 Import,就可以在整个文件的上下文中使用 deleteWhere
了。
方法三:利用参数 it: ISqlExpressionBuilder
这也是一个简单的办法,只需要利用其提供的参数:
XXTable.deleteWhere { it: ISqlExpressionBuilder ->
with(it) {
XXTable.id eq id
}
}
将其利用 with
方法,把参数 it
转换为 this
就可以了。
总结
其实这并不是很复杂的问题,但是如果是第一次碰到的话,肯定会有疑问。我也反反复复地咨询了 AI,上述地解决方法一就是由 AI 提供地。但是 AI 提供的答案并不令我满意,在通过查看官方仓库的 Issue 才真正找到解决的办法。
值得注意的是,当你利用方法二解决问题之后,你会发现不论是 deleteWhere
还是 where
,里面使用的都是来自 SqlExpressionBuilder
提供的运算符,都是出自 ISqlExpressionBuilder
这个接口。可能经验丰富的朋友一眼就能看出原因,但我可能造诣没那么深,所以我还是想要就此分析一下,最后发现解决问题的办法非常之简单。
第三种办法是我自己发现的,因为我们可以看到传入的参数正是我们想要的内容,所以我们只需要将其利用起来就好。
后记
在我看来,官方的这个 API 写的有点问题。根据官方的文档中的写法,我们应当是能够正常使用操作表达式来进行删除操作的。
仔细观察源代码后,我们真正要想利用它,还必须进行其他的步骤,这是在官方文档中没有说明的。
其实有的朋友可能已经发现,如果对 deleteWhere
的定义修改为:
inline fun <T : Table> T.deleteWhere(
limit: Int? = null,
op: ISqlExpressionBuilder.(T) -> Op<Boolean>
): Int =
DeleteStatement.where(
TransactionManager.current(),
this@deleteWhere,
SqlExpressionBuilder.op(this@deleteWhere),
false,
limit
)
也就是说,只需要把 T
和 SqlExpressionBuilder
的位置换一下,就能完美解决这个情况了。我们只需要:
XXTable.deleteWhere { // this: ISqlExpressionBuilder, it: XXTable
it.id eq id
}
就可以完美解决这个问题。
当然,可能官方不选择这样子设计,也可能是由他们自身的考虑,是我没有注意到的,也欢迎大家就此进行讨论。