关于 Exposed DSL 的 deleteWhere 无法识别 eq 操作符:Unresolved reference `eq`

环境

  • 编辑器:IntelliJ IDEA 2024.3 (Ultimate Edition)
  • Kotlin 版本:2.0.21
  • Exposed 版本:0.56.0,利用 DSL 操作数据库

问题描述

最近在使用 Exposed DSL 中的 deleteWhere 或是 deleteIgnoreWhere 方法的时候,遇到了 eq 操作符无法在 IDEA 中解析的问题,参考下图情况:
eq 操作符无法被解析

但是在 where 方法当中,eq 操作符是能够正常使用的:

在 where 方法中能够正常使用

对此感到有些疑惑,因为它应当是能够正常工作的。

分析问题

我们来仔细看一下它们的区别。

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
    )

也就是说,只需要把 TSqlExpressionBuilder的位置换一下,就能完美解决这个情况了。我们只需要:

XXTable.deleteWhere { // this: ISqlExpressionBuilder, it: XXTable
    it.id eq id
}

就可以完美解决这个问题。

当然,可能官方不选择这样子设计,也可能是由他们自身的考虑,是我没有注意到的,也欢迎大家就此进行讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Loyreau

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值