跳到主要内容

DSL表达式

字面量表达式

先来看个例子 (这个查询没有实际业务意义,仅为演示)

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();

生成的SQL如下

select ?, ?, ?, ?, null from BOOK as tb_1_

除了null以外,其余各种类型的字面量都变成了JDBC参数。

警告

这段示范中,可以看出,value()方法接受了很多种类型的参数。

需要注意的是,无论value()方法参数类型是什么,都不能为null,否则将导致异常。

要为null创建字面量表达式,必须使用nullValue()方法,该方法需要指定表达式类型。

信息

对于Java代码,我们使用了类型引导方法:

  1. Expression.string(),针对字符串类型。
  2. Expression.numeric(),针对数字类型。
  3. Expression.comparable(),针对可比较类型,即继承自java.lang.Comparable的类型。
  4. Expression.any(),其它类型。

kotlin没有这个问题,其API是统一的。

提示

大部分情况下,开发人员无需手动创建字面量表达式。

以下文即将讨论的相等判断为例,下面这种相对繁琐的写法

where(
book.name().eq(
Expression.string().value("Java in Action")
)
);

这是不是比较繁琐?别担心,可以用更便捷的写法代替

where(book.name().eq("Java in Action"));

不难发现,其它API会提供重载版本,以避免让开发人员亲自构建字面量表达式。

但是,这无法100%做到。极少数情况下,仍然需要开发人员亲自构建字面量表达式。

常量表达式

常量表达式和字面量表达式高度类似,先看例子 (这个查询没有实际业务意义,仅为演示)

BookTable book = Tables.BOOK_TABLE;

List<Integer> constants = sqlClient
.createQuery(book)
.select(
Expression.constant(1)
)
.execute();

生成的SQL如下

select 
/* highlight-next-line */
1
from BOOK as tb_1_

不难看出,和字面量表达式总是使用JDBC参数不同,常量表达式直接把值硬编码进SQL语句。

为了杜绝注入方式攻击的问题,常量表达式只支持数字类型,这是一个硬性限制。

信息

虽然有常量表达式只支持数字类型这个硬性限制,不用担心注入方式攻击问题,但实际项目仍需要严格限制其使用。

常量表达式之所以存在的唯一理由:某些数据库支持函数索引,如果定义函数索引的SQL表达式内部存在数字常量,这时,为了匹配这样的函数索引,常量表达式会非常有用。

如果你的项目没有这种场景,请永远不要使用常量表达式,而应该全部使用字面量表达式。

错误地使用常量表达式会带来严重的后果。错误地把善变的数字类型参数作为常量表达式植入SQL,将会破坏SQL字符串的稳定性,最终导致数据库内部执行计划缓存命中率极低,影响性能。

静态谓词 vs 动态谓词

早期,Jimmer通过whereIf来支持动态查询

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();
}

虽然whereIf可读性很好,但是代码相对麻烦。后来,Jimmer提供了动态谓词,上面的代码可以简化为

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();
}

其中,Java的ilikeIfeqIf和Kotlin的ilike?eq?叫做动态谓词。

在右操作数为null或""*(长度为0的字符串)*时,动态谓词不创建表达式,直接返回null。where调用能自动忽略null,从而实现了等价的动态查询。

信息

相比于最初的whereIf,动态谓词价值并非简化代码,而是可以适应更复杂的场景,比如or

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();
}

这里,or中所有参数是动态谓词,即,or的每个参数都可能为nul,而or也能忽略null参数。当然,如果所有参数都为null,那么or本身也拒绝创建任何表达方式,直接返回null。

以下是Jimmer的所有静态谓词和动态谓词。

静态谓词动态谓词(Java)动态谓词(Kotlin)
eqeqIf`eq?`
neneIf`ne?`
ltltIf`lt?`
leleIf`le?`
gtgtIf`gt?`
gegeIf`ge?`
likelikeIf`like?`
ilikeilikeIf`ilike?`
betweenbetweenIf`between?`

Q: 为什么Jimmer提供两套谓词,而非只提供动态谓词?

A: 因为静态谓词对null也有特殊处理。

  • eq: 自动转化为isNull()
  • ne: 自动转化为isNotNull()
  • 其他静态谓词
    • Java: 将这视为用户代码的BUG,要求用户要么提供非空值,要么换动态谓词
    • Kotlin: 编译时禁止用户提供可能为null的值
信息

我们已经清晰地讨论动态谓词的这个概念。为了让简化后续讨论,本文后续内容仅示范静态谓词。

比较

  1. 等于

    where(book.name().eq("SQL in Action"));
  2. 不等

    where(book.name().ne("SQL in Action"));
  3. 大于

    where(book.price().gt(new BigDecimal(50)));
  4. 大于或等于

    where(book.price().ge(new BigDecimal(50)));
  5. 小于

    where(book.price().lt(new BigDecimal(50)));
  6. 小于或等于

    where(book.price().le(new BigDecimal(50)));
  7. Between

    where(
    book.price().between(
    new BigDecimal(40),
    new BigDecimal(50)
    )
    );
  8. Not Between

    where(
    book.price().notBetween(
    new BigDecimal(40),
    new BigDecimal(50)
    )
    );

模糊匹配

大小写

  1. like: 大小写敏感

    where(book.name().like("Ab"));

    最终生成的SQL条件

    where tb_1_.NAME like ?

    相应的JDBC参数: %Ab%

  2. ilike: 大小写不敏感

    where(book.name().ilike("Ab"));

    最终生成的SQL条件

    • 如果数据库方言支持ilike

      where tb_1_.NAME ilike ? 
    • 否则

      where lower(tb_1_.NAME) like ? 

    相应的JDBC参数: %ab%

匹配模式

  1. LikeMode.ANYWHERE(不指定时的默认行为):出现在任何位置

    where(book.name().like("Ab", LikeMode.ANYWHERE));

    相应的JDBC参数: '%Ab%'

  2. LikeMode.START:作为开头

    where(book.name().like("Ab", LikeMode.START));

    相应的JDBC参数: 'Ab%'

  3. LikeMode.END:作为结尾

    where(book.name().like("Ab", LikeMode.END));

    相应的JDBC参数: '%Ab'

  4. LikeMode.EXACT:精确匹配

    where(book.name().like("Ab", LikeMode.EXACT));

    相应的JDBC参数: 'Ab'

空判断

where(book.store().isNull());
where(book.store().isNotNull());

IN LIST

单列

where(
book.name().in(
Arrays.asList(
"SQL in Action",
"Java in Action"
)
)
);

生成的SQL条件

where tb_1_.NAME in (?, ?)

多列

where(
Expression.tuple(
book.name(),
book.edition()
).in(
Arrays.asList(
new Tuple2<>("SQL in Action", 1),
new Tuple2<>("SQL in Action", 2),
new Tuple2<>("Java in Action", 1),
new Tuple2<>("Java in Action", 2)
)
)
);

生成的SQL条件

where (tb_1_.NAME, tb_1_.EDITION) in (
(? /* SQL in Action */, ? /* 1 */),
(? /* SQL in Action */, ? /* 2 */),
(? /* Java in Action */, ? /* 1 */),
(? /* Java in Action */, ? /* 2 */)
)
备注

除了和数据集合配合使用外,in还可以与子查询配合一起使用。

这部分内容会在子查询相关文档中详细介绍,本文不做重复介绍。

优化

Jimmer为in list表达式提供了3个优化:

  • 单值优化
  • Padding优化
  • Any等式优化
  1. 单值优化

    当列表长度为1时,in(?)会被自动改写为= ?,而not in(?)自动改写为<> ?,例如

    where(
    Expression.tuple(
    book.name(),
    book.edition()
    ).in(
    Arrays.asList(
    new Tuple2<>("SQL in Action", 1)
    )
    )
    );

    生成的SQL条件

    where 
    (tb_1_.NAME, tb_1_.EDITION) =
    (? /* SQL in Action */, ? /* 1 */)
  2. Padding优化

    为了支持padding优化,Jimmer提供了开关,有如下两种开启方式

    • 如果使用了Jimmer的Spring Boot Starter,修改application.yml *(或application.properties)*如下

      application.yml
      jimmer:
      in-list-padding-enabled: true
    • 如果没有使用了Jimmer的Spring Boot Starter,则需要在创建sqlClient时调用如下API

      JSqlClient sqlClient = JSqlClient
      .newBuilder()
      .setInListPaddingEnabled(true)
      // ...省略其他配置...
      .build();

    一旦开启了此开关,Jimmer会保证列表的长度为2的幂次方 (2,4,6,8,16,32,...),以此大幅降低SQL的多样性,提高数据库执行计划缓存的多样性。例如

    where(
    Expression.tuple(
    book.name(),
    book.edition()
    ).in(
    Arrays.asList(
    new Tuple2<>("SQL in Action", 1),
    new Tuple2<>("SQL in Action", 2),
    new Tuple2<>("Java in Action", 1)
    )
    )
    );

    此例子中,列表长度为3,并非2的幂次方。离3最近的2的幂次方是4,所以,Jimmer会重复列表中最后一项,最终让列表长度为4。生成的如下SQL条件

    where (tb_1_.NAME, tb_1_.EDITION) in (

    // 原始列表
    (? /* SQL in Action */, ? /* 1 */),
    (? /* SQL in Action */, ? /* 2 */),
    (? /* Java in Action */, ? /* 1 */),

    // 重复原列表最后一项,让列表的长度为2的幂次方
    // highlight-next-line
    (? /* Java in Action */, ? /* 1 */)
    )
  3. Any等式优化

    该优化被前面讨论过的padding优化更为测试,但是存在两个限制

    • 目前,只能优化单列in

    • 数据库需支持= any(?)表达式,即org.babyfish.jimmer.sql.dialect.Dialect的实现类的isAnyEqualityOfArraySupported()方法需要返回true

      当前,支持该行为的方言有

      • org.babyfish.jimmer.sql.dialect.H2Dialect
      • org.babyfish.jimmer.sql.dialect.PostgresDialect

    对此,Jimmer提供了开关,有如下两种开启方式

    • 如果使用了Jimmer的Spring Boot Starter,修改application.yml *(或application.properties)*如下

      application.yml
      jimmer:
      in-list-to-any-equality-enabled: true
    • 如果没有使用了Jimmer的Spring Boot Starter,则需要在创建sqlClient时调用如下API

      JSqlClient sqlClient = JSqlClient
      .newBuilder()
      .setInListToAnyEqualityEnabled(true)
      // ...省略其他配置...
      .build();
    注意

    如果当前org.babyfish.jimmer.sql.dialect.Dialect实现类的isAnyEqualityOfArraySupported()方法返回false (不支持此特性), 此配置将会导致异常,sqlClient无法创建,应用无法启动。

    where(
    book.name().in(
    Arrays.asList(
    "SQL in Action",
    "Java in Action"
    )
    )
    );

    生成的SQL条件

    where tb_1_.NAME = any(
    ? /* [SQL in Action, Java in Action] */
    )

    这里,整个list被视为一个整体,作为一个array类型的参数。

与、或、非

where(
Predicate.and(
book.name().like("Ab"),
book.price().ge(new BigDecimal(40)),
book.price().lt(new BigDecimal(60))
)
);
备注

注意,如果逻辑与表达式直接作为where方法的参数,以下两种等价写法更值得推荐.

  1. 使用where方法的变参版本

    where(
    book.name().like("Ab"),
    book.price().ge(new BigDecimal(40)),
    book.price().lt(new BigDecimal(60))
    );
  2. 多次调用where方法

    q.where(book.name().like("Ab"));
    q.where(book.price().ge(new BigDecimal(40)));
    q.where(book.price().lt(new BigDecimal(60)));

所以,直接使用and的写法在实际项目中应该不常见。

where(
Predicate.or(
book.name().like("Ab"),
book.price().ge(new BigDecimal(40)),
book.price().le(new BigDecimal(60))
)
);

where(
book.name().like("Ab").not()
);

并不总是需要调用not()函数。很多时候有快捷方式可用,比如:

  1. .eq(value).not()可以简写成.ne(value)
  2. .isNull().not()可以简写成.isNotNull(value)
  3. .exists().not()可以简写成.notExists() (exists会在子查询中介绍,本文不会介绍)

甚至,即便开发人员明确使用了not(),最终SQL也不一定会出现not,比如

where(
book.price().ge(new BigDecimal(40)).not()
);

实际生成的SQL却是

where tb_1_1.PRICE < ?

jimmer-sql尽量避免在SQL中直接使用not,但无论如何,最终SQL逻辑和你想要的逻辑等价。

数学运算

  1. +

    select(book.price().plus(BigDecimal.TWO));
  2. -

    select(book.price().minus(BigDecimal.TWO));
  3. *

    select(book.price().times(BigDecimal.TWO));
  4. /

    select(book.price().div(BigDecimal.TWO));
  5. %

    select(book.price().rem(BigDecimal.TWO));

聚合函数

BookTable book = Tables.BOOK_TABLE;

List<
Tuple6<
Long,
Long,
BigDecimal,
BigDecimal,
BigDecimal,
BigDecimal
>
> tuples = sqlClient
.createQuery(book)
.select(
book.count(),
book.id().count(/* distinct */ true),
book.price().sum(),
book.price().min(),
book.price().max(),
book.price().avg()
)
.execute();

最终生成的SQL如下

select 
count(tb_1_.ID),
count(distinct tb_1_.ID),
sum(tb_1_.PRICE),
min(tb_1_.PRICE),
max(tb_1_.PRICE),
avg(tb_1_.PRICE)
from BOOK as tb_1_

Coalesce

Coalesce语句指定一系列表达式,返回第一个非null值。

BookStoreTable store = Tables.BOOK_STORE_TABLE;

List<String> results = sqlClient
.createQuery(store)
.select(
store.website()
.coalesceBuilder()
.or(store.name())
.or("Default Value")
.build()
)
.execute();
提示

在这个例子中,kotlin 代码和 java 代码是不同的。 kotlin 代码中没有表达式❸。

Kotlin-API 引入了 kotlin 对 SQL 的 null-safety,table.name 是一个非 null 字段,所以 kotlin 强制我们编写 end() 来完成这个表达式。

这意味着java代码中的表达式❸不会被使用。

最终生成的SQL如下

select 
/* highlight-next-line */
coalesce(
tb_1_.WEBSITE,
tb_1_.NAME,
?
)
from BOOK_STORE as tb_1_

特别地,如果SQL的coalesce函数只有两个参数,即上述Java代码中or()方法只会被调用一次时,有一个快捷写法:

BookStoreTable store = Tables.BOOK_STORE_TABLE;

List<String> results = sqlClient
.createQuery(store)
.select(
store.website().coalesce(store.name())
)
.execute();

最终生成的SQL如下

select 
/* highlight-next-line */
coalesce(tb_1_.WEBSITE, tb_1_.NAME)
from BOOK_STORE as tb_1_

其实,这种写法才是最常见的。

Concat

Contact表达式用于字符串拼接。

这个例子以空格作为连接符,把作者的firstName和lastName拼起来

AuthorTable author = Tables.AUTHOR_TABLE;

List<String> results = sqlClient
.createQuery(author)
.select(
author
.firstName()
.concat(
Expression.string().value(" "),
author.lastName()
)
)
.execute();

最终生成的SQL

select 
/* highlight-next-line */
concat(
tb_1_.FIRST_NAME,
?,
tb_1_.LAST_NAME
)
from AUTHOR as tb_1_

Case

case表达式分为两种,简单的case和普通的case

简单的case

简单的case语句,需要在语句开头处指定一个表达式,后续每一个判断分支指定一个期望值,检查指定的表达式是否和某个期望值匹配。

BookTable book = Tables.BOOK_TABLE;

List<String> results = sqlClient
.createQuery(book)
.select(
Expression.string()
.caseBuilder(book.name())
.when("Java in Action", "Matched")
.when("SQL in Action", "Matched")
.otherwise("Not matched")
)
.execute();

最终生成的SQL如下

select 
/* highlight-next-line */
case tb_1_.NAME
when ? then ?
when ? then ?
else ?
end
from BOOK as tb_1_

普通的case

普通的case语句,语句开头处不需要指定任何参数,但后续每一个判断分支都可以指定一个任意复杂度的逻辑判断表达式,检查是否有分支的逻辑判断成立。

BookTable book = Tables.BOOK_TABLE;

List<String> results = sqlClient
.createQuery(book)
.select(
Expression.string()
.caseBuilder()
.when(
book.price().lt(new BigDecimal(30)),
"Cheap"
)
.when(
book.price().gt(new BigDecimal(70)),
"Expensive"
)
.otherwise("Appropriate")
)
.execute();

最终生成的SQL如下

select 
/* highlight-next-line */
case
when tb_1_.PRICE < ? then ?
when tb_1_.PRICE > ? then ?
else ?
end
from BOOK as tb_1_