超级QBE查询
Super QBE是一个非常强大的功能,利用DTO语言生成复杂查询的参数类型,并自动实现查询逻辑。
创建文件
在任何可以访问实体类型的项目中 ,创建src/main/dto
目录,在这个目录下创建文件Book.dto
。
对于Java项目而言,如果当前项目并非定义实体类型的项目,需要在为当前项目中任何一个类添加@EnableDtoGeneration
注解。
在文件头部添加如下代码
export com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.dto
以上这些步骤,以及如何编译DTO文件,在DTO语言#2. 创建文件中有详细的讨论,本文不作重复性阐述。
定义Specification类型
...省略export语句...
specification BookSpecification {
}
编译后的代码
编译后,将会生成这样的Java/Kotlin代码
- Java
- Kotlin
package com.yourcompany.yourproject.dto;
import org.babyfish.jimmer.sql.ast.query.specification.JSpecification;
import org.babyfish.jimmer.sql.ast.query.specification.SpecificationArgs;
...省略其他import语句...
public class BookSpecification implements JSpecification<Book, BookTable> {
@Override
public void applyTo(SpecificationArgs<Book, BookTable> args) {}
}
package com.yourcompany.yourproject.dto;
import org.babyfish.jimmer.sql.kt.ast.query.specification.KSpecification
import org.babyfish.jimmer.sql.kt.ast.query.specification.KSpecificationArgs
...省略其他import语句...
data class BookSpecification(
// 现在data class暂时没有字段,会导致编译错误
) : KSpecification<Book> {
override fun applyTo(args: KSpecificationArgs<Book>) {}
}
applyTo
是specification编译后生成的代码中的特有方法,按照当前对象的信息动态地为Jimmer查询添加where条件。
此方法无需用户调用 (被Jimmer内部行为调用) ,用户也无需关心其内部代码实现。这里只需要知道此方法有什么用即可。
在后续的讨论中,我们会陆续为DTO文件中的BookSpecification
添加属性。
相应地,对于自动生成的BookSpecification
类而言,一方面属性会同步增加,另外一方面,applyTo
方法中的代码也会变 多。
用法
-
在查询中使用
- Java
- Kotlin
public List<Book> find(
Specification<Book> specification ❶
) {
BookTable table = Tables.BOOK_TABLE;
return sqlClient
.createQuery(table)
.where(specification) ❷
.select(table)
.execute();
}fun find(
specification: Specification<Book> ❶
): List<Book> =
sqlClient.createQuery(Book::class) {
where(specification) ❷
select(table)
}.execute()-
❶
org.babyfish.jimmer.Specification<Book>
类型的参数,用于生产各种动态SQL条件。 -
❷ 无论
specification
格式简单还是复杂,只需一个简单的where
语句就可以使用。
-
在Spring Data Repository中使用
Jimmer整合了Spring Data,所以能定义Spring Data Repository,请参 考SpringData风格以了解更多。
Jimmer的Spring Data Repository有两种查询风格,抽象方法和default方法。default方法使用specification的代码和上面雷同, 所以,我们看看抽象查询方法使用specification的例子
- Java
- Kotlin
public interface BookRepository : JRepository<Book, Long> {
public List<Book> find(
Specification<Book> specification
)
}interface BookRepository : KRepository<Book, Long> {
fun find(
specification: Specification<Book>
): List<Book>
}
属性映射
映射属性
...省略export语句...
specification BookSpecification {
name
}
这样,就将实体属性映射到了DTO中,生成的代码为
- Java
- Kotlin
public class BookSpecification implements JSpecification<Book, BookTable> {
@Nullable
private String name;
...省略getters和setters...
@Override
public void applyTo(SpecificationArgs<Book, BookTable> args) {
...略...
}
}
package com.yourcompany.yourproject.dto;
import org.babyfish.jimmer.sql.kt.ast.query.specification.KSpecification
import org.babyfish.jimmer.sql.kt.ast.query.specification.KSpecificationArgs
...省略其他import语句...
data class BookSpecification(
val name: String? = null
) : KSpecification<Book> {
override fun applyTo(args: KSpecificationArgs<Book>) {
...略...
}
}
可空性
我们发现,在生成的代码中name
字段时可以为null的,这是specification的特殊性所在。
specification用作查询参数,为了支持动态查询,默认情况下,所有属性都是可以为null的。除非显式地使用!
规定属性不能为null*(请参见DTO语言#7. 可空性)*。
用法
-
让BookSpecification的
name
字段为null- Java
- Kotlin
BookSpecification specification = new BookSpecification();
List<Book> books = bookRepository.find(specification);val specification = BookSpecification()
val books = bookRepository.find(specification)由于
specification.name
为null,生成的SQL不会带任何where
条件生成的SQL如下
select tb_1_.ID, tb_1_.NAME, tb_1_.EDITION, tb_1_.PRICE, tb_1_.STORE_ID
from BOOK tb_1_信息这种不指定specification相关属性的查询,必然导致查询没有任何条件。
这种例子本文仅仅示范一次。
-
让BookSpecification的
name
字段不为null- Java
- Kotlin
BookSpecification specification = new BookSpecification();
specification.setName("GraphQL in Action");
List<Book> books = bookRepository.find(specification);val specification = BookSpecification(
name = "GraphQL in Action"
)
val books = bookRepository.find(specification)生成的SQL如下
select tb_1_.ID, tb_1_.NAME, tb_1_.EDITION, tb_1_.PRICE, tb_1_.STORE_ID
from BOOK tb_1_
/* highlight-next-line */
where tb_1_.NAME = ? /* GraphQL in Action */
QBE函数
初识QBE函数
上面的代码中,当指定了specification.name
后,生成的where
条件是等于。
等于不一定是我们需要的,可以为映射属性施加QBE函数,改变运算符,以like
为例
...省略export语句...
specification BookSpecification {
like(name)
}
like函数不会影响生成的BookSpecification
类的属性,但是会影响其applyTo
方法,该方法的内部实现是where
条件的添加逻辑,无需用户关心。
执行
- Java
- Kotlin
BookSpecification specification = new BookSpecification();
specification.setName("GraphQL");
List<Book> books = bookRepository.find(specification);
val specification = BookSpecification(
name = "GraphQL"
)
val books = bookRepository.find(specification)
生成的SQL如下
select tb_1_.ID, tb_1_.NAME, tb_1_.EDITION, tb_1_.PRICE, tb_1_.STORE_ID
from BOOK tb_1_
/* highlight-next-line */
where tb_1_.NAME like ? /* %GraphQL% */
like函数的选项
like
是所有QBE函数中比较特殊的一个,支持3个选项
-
i: 大小写不敏感
-
^: 精确匹配开头 (Jimmer不会自动在参数值前面加
%
) -
$: 精确匹配结尾 (Jimmer不会自动在参数值后面加
%
)
如果需要选项,可以在like
后面加/
,再加上需要的选项。比如:like/i
、like/^
、like/$
、like/i^
、like/i$
和like/i^$
。
虽然i
、^
和$
都是可选的,但是彼此先后顺序是固定的。
让我们来试试大小写不敏感的like
,修改DTO代码如下
...省略export语句...
specification BookSpecification {
like/i(name)
}
再次执行上面的代码,执行如下SQL
-
不支持
ilike
的数据库select tb_1_.ID, tb_1_.NAME, tb_1_.EDITION, tb_1_.PRICE, tb_1_.STORE_ID
from BOOK tb_1_
/* highlight-next-line */
where lower(tb_1_.NAME) like ? /* %graphql% */ -
支持
ilike
的数据库select tb_1_.ID, tb_1_.NAME, tb_1_.EDITION, tb_1_.PRICE, tb_1_.STORE_ID
from BOOK tb_1_
/* highlight-next-line */
where tb_1_.NAME ilike ? /* %graphql% */
所有QBE函数
事实上,除了like
外,speciation支持大量的QBE函数,涵盖了常见的SQL判断,如下表
QBE函数 | 原实体属性类型 (或要求) | 生成的DTO类属性的类型 | 备注 |
---|---|---|---|
eq | 任何非关联属性 | 原类型 | 等于,和不使用任何QBE函数等价 |
ne | 任何非关联属性 | 原类型 | 不等于 |
gt | 任何非关联属性 | 原类型 | 大于 |
ge | 任何非关联属性 | 原类型 | 大于或等于 |
lt | 任何非关联属性 | 原类型 | 小于 |
le | 任何非关联属性 | 原类型 | 小于或等于 |
like | String | String | 模糊匹配 |
notLike | String | String | 模糊不匹配 |
null | 任何属性 | boolean | 如果DTO属性为true,is null判断 |
notNull | 任何属性 | boolean | 如果DTO属性为true,is not null判断 |
valueIn | 任何非关联属性 | List<原类型> | in(...) |
valueNotIn | 任何非关联属性 | List<原类型> | not in(...) |
associatedIdEq | 任何关联属性 | 关联实体的id属性的类型 | 关联id = ?。注意,和id (DTO语言固有函数) 等价 |
associatedIdNe | 任何关联属性 | 关联实体的id属性的类型 | 关联id <> ? |
associatedIdIn | 任何关联属性 | List<关联实体的id属性的类型> | 关联id in(...) |
associatedIdNotIn | 任何关联属性 | List<关联实体的id属性的类型> | 关联id not in(...) |
综合示范
修改DTO代码
...省略export语句...
specification BookSpecification {
like/i(name)
ge(price)
le(price)
}
ge(price)
会指定默认的别名minPrice
,le(price)
会指定默认的别名maxPrice
,因此,上述代码也可以写为
...省略export语句...
specification BookSpecification {
like/i(name)
ge(price) as minPrice
le(price) as maxPrice
}
由此可见,某些QBE函数自带默认alias的功能。
假设原属性名称为Prop
,所有QBE函数的默认alias行为如下
QBE函数 | 默认alias |
---|---|
eq (或不指定QBE函数) | prop |
ne | 不支持,不指定alias会导致编译错 |
gt | minPropExclusive |
ge | minProp |
lt | maxPropExclusive |
le | maxProp |
like | prop |
notLike | 不支持,不指定alias会导致编译错 |
valueIn | 不支持,不指定alias会导致编译错 |
valueNotIn | 不支持,不指定alias会导致编译错 |
associatedIdEq或id | 如果关联是引用 (非集合),propId ;否则,编译报错 |
associatedIdNe | 如果关联是引用 (非集合),excludedPropId ;否则,编译报错 |
associatedIdIn | 如果关联是引用 (非集合),propIds ;否则,编译报错 |
associatedIdNotNull | 如果关联是引 用 (非集合),excludedPropIds ;否则,编译报错 |
编译后生成的代码为
- Java
- Kotlin
public class BookSpecification implements JSpecification<Book, BookTable> {
@Nullable
private String name;
@Nullable
private BigDecimal minPrice;
@Nullable
private BigDecimal maxPrice;
...省略getters和setters...
@Override
public void applyTo(SpecificationArgs<Book, BookTable> args) {
...略...
}
}
package com.yourcompany.yourproject.dto;
import org.babyfish.jimmer.sql.kt.ast.query.specification.KSpecification
import org.babyfish.jimmer.sql.kt.ast.query.specification.KSpecificationArgs
...省略其他import语句...
data class BookSpecification(
val name: String? = null,
val minPrice: BigDecimal? = null,
val maxPrice: BigDecimal? = null
) : KSpecification<Book> {
override fun applyTo(args: KSpecificationArgs<Book>) {
...略...
}
}
执行
- Java
- Kotlin
BookSpecification specification = new BookSpecification();
specification.setName("GraphQL");
specification.setMinPrice(new BigDecimal(40));
specification.setMaxPrice(new BigDecimal(40));
List<Book> books = bookRepository.find(specification);
val specification = BookSpecification(
name = "GraphQL",
minPrice = BigDecimal(40),
maxPrice = BigDecimal(60)
)
val books = bookRepository.find(specification)
生成的SQL如下
select tb_1_.ID, tb_1_.NAME, tb_1_.EDITION, tb_1_.PRICE, tb_1_.STORE_ID
from BOOK tb_1_
where
lower(tb_1_.NAME) like ? /* %graphql% */
and
tb_1_.PRICE >= ? /* 40 */
and
tb_1_.PRICE <= ? /* 60 */
order by
tb_1_.NAME asc,
tb_1_.EDITION desc
关联对象
之前例子,所有过滤规则都是针对当前表字段的。现在,让我们对关联对象进行过滤。修改DTO文件如下
...省略export语句...
specification BookSpecification {
like/i(name)
ge(price)
le(price)
flat(store) { ❶
like/i(name) as storeName
}
flat(authors) { ❷
like/i(firstName) as authorName
}
}
我们发现,上面的代码使用了flat
函数。flat函数在DTO语言#10.4-flat函数中有详细讨论,本文不作重复性阐述。
由于specification
作为复杂查询的参数,很有可能HTTP GET参数。采用flat
函数消除关联,生成平坦的DTO对象,更有利于基于Spring MVC开发HTTP GET API。
-
❶
Book.store
是引用关联 -
❷
Book.authors
是集合关联
编译后生成的代码为
- Java
- Kotlin
public class BookSpecification implements JSpecification<Book, BookTable> {
@Nullable
private String name;
@Nullable
private BigDecimal minPrice;
@Nullable
private BigDecimal maxPrice;
@Nullable
private String storeName;
@Nullable
private String authorName;
...省略getters和setters...
@Override
public void applyTo(SpecificationArgs<Book, BookTable> args) {
...略...
}
}
package com.yourcompany.yourproject.dto;
import org.babyfish.jimmer.sql.kt.ast.query.specification.KSpecification
import org.babyfish.jimmer.sql.kt.ast.query.specification.KSpecificationArgs
...省略其他import语句...
data class BookSpecification(
val name: String? = null,
val minPrice: BigDecimal? = null,
val maxPrice: BigDecimal? = null,
val storeName: String? = null,
val authorName: String? = null
) : KSpecification<Book> {
override fun applyTo(args: KSpecificationArgs<Book>) {
...略...
}
}
执行
- Java
- Kotlin
BookSpecification specification = new BookSpecification();
specification.setStoreName("MANNING");
specification.setAuthorName("a");
List<Book> books = bookRepository.find(specification);
val specification = BookSpecification(
storeName = "MANNING",
authorName = "a"
)
val books = bookRepository.find(specification)
生成的SQL如下
select tb_1_.ID, tb_1_.NAME, tb_1_.EDITION, tb_1_.PRICE, tb_1_.STORE_ID
from BOOK tb_1_
/* highlight-next-line */
inner join BOOK_STORE tb_2_ /* ❶ */
on tb_1_.STORE_ID = tb_2_.ID
where
tb_2_.NAME ilike ? /* %manning% */
and
/* highlight-next-line */
exists( /* ❷ */
select
1
from AUTHOR tb_3_
inner join BOOK_AUTHOR_MAPPING tb_4_
on tb_3_.ID = tb_4_.AUTHOR_ID
where
tb_1_.ID = tb_4_.BOOK_ID
and
tb_3_.FIRST_NAME ilike ? /* %a% */
)
-
❶
Book.store
是引用关联,不会破坏分页安全性。因此,仅使用动态JOIN即可应用DTO对象
storeName
属性所表示的过滤条件。 -
❷
Book.authors
是集合关联,会破坏分页安全性。
逻辑或
之前的例子中,Jimmer会按照DTO对象的每个属性来生成若干个where
条件,这些条件之间的关系是逻辑与,但能否支持逻辑或呢?
另外,实体类型Author
除了firstName
属性外还有lastName
,上一个例子中的authorName
只映射了firstName
显得不太合理,有没有更好的办法?
以上两个问题是同一个问题。为解决这个问题,Super QBE支持逻辑或,也称呼多属性映射。
修改DTO代码,如下
...省略export语句...
specification BookSpecification {
like/i(name)
ge(price)
le(price)
flat(store) {
like/i(name) as storeName
}
flat(authors) {
like/i(firstName, lastName) as authorName
}
}
这里,DTO属性authorName
映射了两个属性,firstName
和lastName
,这就叫做多属性映射。
-
多属性映射只能在
specification
中使用,无法在DTO语言所描述的其他类型中使用。 -
被QBE函数映射的多个属性的类型必须完全一致 (但可空性允许不同)。例如,这里的
firstName
和lastName
都是String
类型`。 -
多属性映射的DTO属性必须通过
as
指定别名,否则会导致编译错。 -
并非所有QBE函数都支持多属性映射,支持多属性映射的QBE函数如下:
- eq
- ne
- null
- notNull
- valueIn
- associatedIdEq
- associatedIdIn
盲目地让所有QBE函数都支持多属性映射会带来理解歧义。所以,才会有此限制。
重复执行上个例子中的查询,生成的SQL如下
select tb_1_.ID, tb_1_.NAME, tb_1_.EDITION, tb_1_.PRICE, tb_1_.STORE_ID
from BOOK tb_1_
inner join BOOK_STORE tb_2_
on tb_1_.STORE_ID = tb_2_.ID
where
tb_2_.NAME ilike ? /* %manning% */
and
exists(
select
1
from AUTHOR tb_3_
inner join BOOK_AUTHOR_MAPPING tb_4_
on tb_3_.ID = tb_4_.AUTHOR_ID
where
tb_1_.ID = tb_4_.BOOK_ID
and
(
tb_3_.FIRST_NAME ilike ? /* %a% */
/* highlight-next-line */
or
tb_3_.LAST_NAME ilike ? /* %a% */
)
)
不难发现,多属性映射就是逻辑或。