DSL表达式
字面量表达式
先来看个例子 (这个查询没有实际业务意义,仅为演示)
- Java
- Kotlin
BookTable book = Tables.BOOK_TABLE;
List<
Tuple5<
String,
Long,
OffsetDateTime,
String,
Boolean
>
> tuples = sqlClient
.createQuery(book)
.select(
Expression.string().value("String"),
Expression.numeric().value(3L),
Expression.comparable().value(OffsetDateTime.now()),
Expression.any().value("String"),
Expression.nullValue(Boolean.class)
)
.execute();
val tuples = sqlClient
.createQuery(Book::class) {
select(
value("String"),
value(3L),
value(OffsetDateTime.now()),
value("String"),
nullValue(Boolean::class)
)
}
.execute()
生成的SQL如下
select ?, ?, ?, ?, null from BOOK as tb_1_
除了null以外,其余各种类型的字面量都变成了JDBC参数。
这段示范中,可以看出,value()
方法接受了很多种类型的参数。
需要注意的是,无论value()
方法参数类型是什么,都不能为null,否则将导致异常。
要为null创建字面量表达式,必须使用nullValue()
方法,该方法需要指定表达式类型。
对于Java代码,我们使用了类型引导方法:
- Expression.string(),针对字符串类型。
- Expression.numeric(),针对数字类型。
- Expression.comparable(),针对可比较类型,即继承自java.lang.Comparable的类型。
- Expression.any(),其它类型。
kotlin没有这个问题,其API是统一的。
大部分情况下,开发人员无需手动创建字面量表达式。
以下文即将讨论的相等判断为例,下面这种相对繁琐的写法
- Java
- Kotlin
where(
book.name().eq(
Expression.string().value("Java in Action")
)
);
where(
table.name eq value("Java in Action")
)
这是不是比较繁琐?别担心,可以用更便捷的写法代替
- Java
- Kotlin
where(book.name().eq("Java in Action"));
where(table.name eq "Java in Action")
不难发现,其它API会提供重载版本,以避免让开发人员亲自构建字面量表达式。
但是,这无法100%做到。极少数情况下,仍然需要开 发人员亲自构建字面量表达式。
常量表达式
常量表达式和字面量表达式高度类似,先看例子 (这个查询没有实际业务意义,仅为演示)
- Java
- Kotlin
BookTable book = Tables.BOOK_TABLE;
List<Integer> constants = sqlClient
.createQuery(book)
.select(
Expression.constant(1)
)
.execute();
val constants = sqlClient
.createQuery(Book::class) {
select(
constant(1)
)
}
.execute()
生成的SQL如下
select
/* highlight-next-line */
1
from BOOK as tb_1_
不难看出,和字面量表达式总是使用JDBC参数不同,常量表达式直接把值硬编码进SQL语句。
为了杜绝注入方式攻击的问题,常量表达式只支持数字类型,这是一个硬性限制。
虽然有常量表达式只支持数字类型这个硬性限制,不用担心注入方式攻击问题,但实际项目仍需要严格限制其使用。
常量表达式之所以存在的唯一理由:某些数据库支持函数索引,如果定义函数索引的SQL表达式内部存在数字常量,这时,为了匹配这样的函数索引,常量表达式会非常有用。
如果你的项目没有这种场景,请永远不要使用常量表达式,而应该全部使用字面量表达式。
错误地使用常量表达式会带来严重的后果。错误地把善变的数字类型参数作为常量表达式植入SQL,将会破坏SQL字符串的稳定性,最终导致数据库内部执行计划缓存命中率极低,影响性能。
静态谓词 vs 动态谓词
早期,Jimmer通过whereIf
来支持动态查询
- Java
- Kotlin
public List<Book> findBooks(
@Nullable String name,
@Nullable Integer edition
) {
BookTable table = BookTable.$;
return sqlClient
.createQuery(table)
.whereIf(
name != null && !name.isEmpty(),
() -> table().name().ilike(name)
)
.whereIf(
edition != null,
table().edition().eq(edition)
)
.select(table)
.execute();
}
fun findBooks(
name: String? = null,
edition: Int? = null
): List<Book> =
sqlClient
.createQuery(Book::class) {
name?.takeIf { it.isNotEmpty() }?.let {
where(table.name ilike it)
}
edition?.let {
where(table.edition eq it)
}
select(table)
}
.execute()
虽然whereIf
可读性很好,但是代码相对麻烦。后来,Jimmer提供了动态谓词,上面的代码可以简化为
- Java
- Kotlin
public List<Book> findBooks(
@Nullable String name,
@Nullable Integer edition
) {
BookTable table = BookTable.$;
return sqlClient
.createQuery(table)
.where(table.name().ilikeIf(name))
.where(table.edition().eqIf(edition))
.select(table)
.execute();
}
fun findBooks(
name: String? = null,
edition: Int? = null
): List<Book> =
sqlClient
.createQuery(Book::class) {
where(table.name `ilike?` name)
where(table.name `eq?` edition)
select(table)
}
.execute()
其中,Java的ilikeIf
、eqIf
和Kotlin的ilike?
、eq?
叫做动态谓词。
在右操作数为null或""*(长度为0的字符串)*时,动态谓词不创建表达式,直接返回null。where调用能自动忽略null,从而实现了等价的动态查询。
相比于最初的whereIf
,动态谓词价值并非简化代码,而是可以适应更复杂的场景,比如or
- Java
- Kotlin
public List<Book> findBooksByNameOrPriceRange(
@Nullable String name,
@Nullable BigDecimal minPrice,
@Nullable BigDecimal maxPrice
) {
BookTable table = BookTable.$;
return sqlClient
.createQuery(table)
.where(
Predicate.or(
table.name().eqIf(name),
table.price().betweenIf(minPrice, maxPrice)
)
)
.select(table)
.execute();
}
fun findBooksByNameOrPriceRange(
name: String? = null,
minPrice: BigDecimal? = null,
maxPrice: BigDecimal? = null
): List<Book> =
sqlClient
.createQuery(Book::class) {
where(
or(
table.name `eq?` name,
table.price.`between?`(minPrice, maxPrice)
)
)
select(table)
}
.execute()
这里,or
中所有参数是 动态谓词,即,or
的每个参数都可能为nul,而or
也能忽略null参数。当然,如果所有参数都为null
,那么or
本身也拒绝创建任何表达方式,直接返回null。
以下是Jimmer的所有静态谓词和动态谓词。
静态谓词 | 动态谓词(Java) | 动态谓词(Kotlin) |
---|---|---|
eq | eqIf | `eq?` |
ne | neIf | `ne?` |
lt | ltIf | `lt?` |
le | leIf | `le?` |
gt | gtIf | `gt?` |
ge | geIf | `ge?` |
like | likeIf | `like?` |
ilike | ilikeIf | `ilike?` |
between | betweenIf | `between?` |
Q: 为什么Jimmer提供两套谓词,而非只提供动态谓词?
A: 因为静态谓词对null也有特殊处理。
- eq: 自动转化为isNull()
- ne: 自动转化为isNotNull()
- 其他静态谓词
- Java: 将这视为用户代码的BUG,要求用户要么提供非空值,要么换动态谓词
- Kotlin: 编译时禁止用户提供可能为null的值
我们已经清晰地讨论动态谓词的这个概念。为了让简化后续讨论,本文后续内容仅示范静态谓词。
比较
-
等于
- Java
- Kotlin
where(book.name().eq("SQL in Action"));
where(table.name eq "SQL in Action")
-
不等
- Java
- Kotlin
where(book.name().ne("SQL in Action"));
where(table.name ne "SQL in Action")
-
大于
- Java
- Kotlin
where(book.price().gt(new BigDecimal(50)));
where(table.price gt BigDecimal(50))
-
大于或等于
- Java
- Kotlin
where(book.price().ge(new BigDecimal(50)));
where(table.price ge BigDecimal(50))
-
小于
- Java
- Kotlin
where(book.price().lt(new BigDecimal(50)));
where(table.price lt BigDecimal(50))
-
小于或等于
- Java
- Kotlin
where(book.price().le(new BigDecimal(50)));
where(table.price le BigDecimal(50))
-
Between
- Java
- Kotlin
where(
book.price().between(
new BigDecimal(40),
new BigDecimal(50)
)
);where(
table.price.between(
BigDecimal(40),
BigDecimal(50)
)
) -
Not Between
- Java
- Kotlin
where(
book.price().notBetween(
new BigDecimal(40),
new BigDecimal(50)
)
);where(
table.price.notBetween(
BigDecimal(40),
BigDecimal(50)
)
)