Base-Query
所谓Base-Query,即SQL中的Derived Table
,CTE
和Recursive CTE
的统称。
Jimmer使用强类型DSL对Derived Table
,CTE
和Recursive CTE
进行了统一支持。
和SQL语言的这些概念不同,Jimmer额外提供了一个全新颠覆性特性:投影反向传播。
这是一套全新的设计哲学,也是本文要讨论的重点。
初识Derived Table
所谓Derived Table就是利用一个查询作为其他查询的from
或join
的基表。例如
select a, b, c
from (
select a, b, c, d from t where ....
) derived_table
where d between 1 and 10
Jimmer实现derived table的方式如下
- Java
- Kotlin
BookTable book = BookTable.$;
AuthorTableEx author = AuthorTableEx.$;
BaseTable2<BookTable, NumericExpression<Integer>> baseTable =
sqlClient
.createBaseQuery(book)
.addSelect(book)
.addSelect(
Expression.numeric().sql(
Integer.class,
"dense_rank() over(" +
"order by %e desc" +
")",
sqlClient.createSubQuery(author)
.where(author.books().id().eq(book.id()))
.selectCount()
)
)
.asBaseTable();
List<Book> books =
sqlClient
.createQuery(baseTable)
.where(baseTable.get_2().eq(1))
.select(baseTable.get_1())
.execute();
val baseTable =
baseTableSymbol {
sqlClient.createBaseQuery(Book::class) {
selections
.add(table)
.add(
sql(Int::class, "dense_rank() over(order by %e desc)") {
expression(
subQuery(Author::class) {
where(table.books.id eq parentTable.id)
select(rowCount())
}
)
}
)
}
}
val books =
sqlClient.createQuery(baseTable) {
where(table._2 eq 1)
select(table._1)
}.execute()
对于这个例子
-
和
createQuery
创建真实查询不同,createBaseQuery
创建的base-query不会被直接执行。 其目的仅仅在于构建derived table,供后续createQuery
创建真正的查询使用 (作为from或join的基表使用) -
内部的base-query返回了两列 (第一列是表对象,第二列是简单表达式)。外部查询使用
get_1()
/_1
和get_2()
/_2
来使用他们base-query支持返回1~9列
-
和
createQuery
以及createSubQuery
(截止目前为止暂时没有被介绍,后学章节出现) 不同,createBaseQuery
不使用select(selection1, selection2, ..., selectionN)
方法指定投影,而是-
Java使用
.addSelect(...).addSelect(...)...addSelect(...)
风格的代码指定投影 -
Kotlin使用
selections.add(...).add(...)...add(...)
风格的代码指定投影
信息- 须采用链式编程风格,语法层面不可中断,才能保证成功的编译
- base-query采用特殊的投影指定方法是为了实现稍后即将介绍的重要特性:投影反向传播,这里请读者先行忽略此设 计的意图。
-
此查询会生成如下SQL
select
tb_1_.c1,
tb_1_.c2,
tb_1_.c3,
tb_1_.c4,
tb_1_.c5
from (
select
tb_2_.ID c1,
tb_2_.NAME c2,
tb_2_.EDITION c3,
tb_2_.PRICE c4,
tb_2_.STORE_ID c5,
dense_rank() over(order by (
select
count(1)
from AUTHOR tb_3_
inner join BOOK_AUTHOR_MAPPING tb_4_
on tb_3_.ID = tb_4_.AUTHOR_ID
where
tb_4_.BOOK_ID = tb_2_.ID
) desc) c6
from BOOK tb_2_
) tb_1_
where
tb_1_.c6 = ? /* 1 */
得到如下数据
[
{
"id": 1,
"name": "Learning GraphQL",
"edition": 1,
"price": 50,
"store": {
"id": 1
}
},
{
"id": 2,
"name": "Learning GraphQL",
"edition": 2,
"price": 55,
"store": {
"id": 1
}
},
{
"id": 3,
"name": "Learning GraphQL",
"edition": 3,
"price": 51,
"store": {
"id": 1
}
}
]
投影反向传播
BaseQuery独有的投影列设置
上文提及
和
createQuery
以及createSubQuery
(截止目前为止暂时没有被介绍,后学章节出现) 不同,createBaseQuery
不使用select(selection1, selection2, ..., selectionN)
方法制定投影,而是
Java使用
.addSelect(...).addSelect(...)...addSelect(...)
风格的代码指定投影Kotlin使用
selections.add(...).add(...)...add(...)
风格的代码指定投影
须采用链式编程风格,语法层面不可中断,才能保证成功的编译
createBaseQuery
如此设计的,是为了只允许如下两种操作
-
查询简表达式
- Java
- Kotlin
.addSelect(
Expression.concat(
table.firstName(),
Expression.constant(" "),
table.lastName()
)
)selections.add(
concat(
table.firstName,
constant(" "),
table.lastName
)
)
-
查询表对象
- Java
- Kotlin
.addSelect(table)
.addSelect(table.store())
.addSelect(table.asTableEx().authors())selections
.add(table)
.add(table.store)
.add(table.asTableEx().authors)信息即
-
要么返回简单表达式
-
要么返回对象 (形状未知)
其中,返回形状未知的表对象 (2),才是投影反向传播的所关心的话题,是本章节的重点。
注意:不能对返回的表对象施加对象抓取器,或返回Output DTO
而如下方式是不被允许的
-
使用对象抓取器的对象查询
- Java
- Kotlin
// 注意,这里示范的是非法代码
.addSelect(
table.fetch(
BookFetcher.$
.allScalarFields()
.store(
BookStoreFetcher.$.name()
)
.authors(
AuthorFetcher.$.name()
)
)
)// 注意,这里示范的是非法代码
selections.add(
table.fetchBy {
allScalarFields()
store {
name()
}
authors {
authors()
}
}
) -
- Java
- Kotlin
// 注意,这里示范的是非法代码
.addSelect(table.fetch(BookView.class))// 注意,这 里示范的是非法代码
selections.add(table.fetch(BookView::class))
基于对象抓取器/Output DTO的对象查询是Jimmer最核心的功能之一, BaseQuery投影查询的的API却不允许这样查询。
这绝非为了禁止使用此能力;恰恰相反,这是为了给予更好的抽象, 更好地使用对象抓取器和Output DTO。
投影反向传播机制介绍
在原生SQL中,基于dervided table的查询往往是这样的。
select
c1,
c2,
c3,
...,
cM
from (
select
c1,
c2,
c3,
...,
cM,
...,
CN
from real_table
) derived_table
其中,M <= N
我们先决定内部base query的返回列集合 (c1, c2, ..., cN)
,然后在选择其中的一个子集合 (c1, c2, ..., cM)
(M <= N),让外部真实查询返回。
很明显,这是一个自内向外的投影传播过程,内部查询的投影和外部查询的投影包含大量的重复列,这是一件非常繁琐且容易出错的事。 在列的数量较多的时候,这对开发、重构和维护极为不利。不幸的是,实际项目几乎总是如此。
因此, Jimmer采用 自外向内 的投影反向传播机制
-
内部的base-query如果如果返回对象,必须返回原始的table对象,不能使用对象抓取器或Output DTO,表示返回的对象的形状是未知的
-
最终,内部base-query应该返回的哪些具体的列,而是根据外部查询的需要自动决定的,无需在base-query中显式指定。
外部查询控制内部base-query应该返回哪些具体列的方法有:
- 对象抓取器
- Output DTO类型
- DSL中被使用的列属性
投影反向传播不仅能让大幅答复简化,还带了原始SQL不具备的智能行实用性。
现在以对象抓取器为例,编写一个函数
- Java
- Kotlin
private List<Book> findBooks(
Fetcher<Book> fetcher
) {
BookTable table = BookTable.$;
AuthorTableEx author = AuthorTableEx.$;
BaseTable2<BookTable, NumericExpression<Integer>> baseTable =
sqlClient
.createBaseQuery(table)
.addSelect(table)
.addSelect(
Expression.numeric().sql(
Integer.class,
"dense_rank() over(" +
"order by %e desc" +
")",
sqlClient.createSubQuery(author)
.where(author.books().id().eq(table.id()))
.selectCount()
)
)
.asBaseTable();
return sqlClient
.createQuery(baseTable)
.where(baseTable.get_2().eq(1))
.select(
baseTable.get_1().fetch(
fetcher
)
)
.execute();
}
private fun findBooks(
fetcher: Fetcher<Book>
): List<Book> {
val baseTable = baseTableSymbol {
sqlClient.createBaseQuery(Book::class) {
selections
.add(table)
.add(
sql(Int::class, "dense_rank() over(order by %e desc)") {
expression(
subQuery(Author::class) {
where(table.books.id eq parentTable.id)
select(rowCount())
}
)
}
)
}
}
return sqlClient.createQuery(baseTable) {
where(table._2 eq 1)
select(
table._1.fetch(
fetcher
)
)
}.execute()
}
现在让我们来看3个案例
-
简单的对象抓取器
- Java
- Kotlin
List<Book> books = findBooks(
BookFetcher.$
.name()
);val books = findBooks(
newFetcher(Book::class).by {
name()
}
)由于对象抓取器很简单,内外查询返回的列都较少,生成如下SQL
select
tb_1_.c1,
tb_1_.c2
from (
select
tb_2_.ID c1,
tb_2_.NAME c2,
dense_rank() over(order by (
select
count(1)
from AUTHOR tb_3_
inner join BOOK_AUTHOR_MAPPING tb_4_
on tb_3_.ID = tb_4_.AUTHOR_ID
where
tb_4_.BOOK_ID = tb_2_.ID
) desc) c3
from BOOK tb_2_
) tb_1_
where
tb_1_.c3 = ? /* 1 */查询得到如下数据
[
{
"id": 1,
"name": "Learning GraphQL"
},
{
"id": 2,
"name": "Learning GraphQL"
},
{
"id": 3,
"name": "Learning GraphQL"
}
] -
中等复杂的对象抓取器
- Java
- Kotlin
List<Book> books = findBooks(
BookFetcher.$
.allScalarFields()
);val books = findBooks(
newFetcher(Book::class).by {
allScalarFields()
}
)由于对象抓取器相对复杂一些,内外查询返回的列都变多,生成如下SQL
select
tb_1_.c1,
tb_1_.c2,
tb_1_.c3,
tb_1_.c4
from (
select
tb_2_.ID c1,
tb_2_.NAME c2,
tb_2_.EDITION c3,
tb_2_.PRICE c4,
dense_rank() over(order by (
select
count(1)
from AUTHOR tb_3_
inner join BOOK_AUTHOR_MAPPING tb_4_
on tb_3_.ID = tb_4_.AUTHOR_ID
where
tb_4_.BOOK_ID = tb_2_.ID
) desc) c5
from BOOK tb_2_
) tb_1_
where
tb_1_.c5 = ? /* 1 */查询得到如下数据
[
{
"id": 1,
"name": "Learning GraphQL",
"edition": 1,
"price": 50
},
{
"id": 2,
"name": "Learning GraphQL",
"edition": 2,
"price": 55
},
{
"id": 3,
"name": "Learning GraphQL",
"edition": 3,
"price": 51
}
] -
复杂的对象抓取器
- Java
- Kotlin
List<Book> books = findBooks(
BookFetcher.$
.name()
.store(
ReferenceFetchType.JOIN_ALWAYS,
BookStoreFetcher.$.name()
)
);val books = findBooks(
newFetcher(Book::class).by {
name()
store(ReferenceFetchType.JOIN_ALWAYS) {
name()
}
}
)由于对象抓取器包含join fetch操作,查询语句自然也会通过join抓取关联对象,生成如下SQL
select
tb_1_.c1,
tb_1_.c2,
tb_6_.ID,
tb_6_.NAME
from (
select
tb_2_.ID c1,
tb_2_.NAME c2,
tb_2_.STORE_ID c3,
dense_rank() over(order by (
select
count(1)
from AUTHOR tb_3_
inner join BOOK_AUTHOR_MAPPING tb_4_
on tb_3_.ID = tb_4_.AUTHOR_ID
where
tb_4_.BOOK_ID = tb_2_.ID
) desc) c4
from BOOK tb_2_
) tb_1_
left join BOOK_STORE tb_6_
on tb_1_.c3 = tb_6_.ID
where
tb_1_.c4 >= ? /* 1 */查询得到如下数据
[
{
"id": 1,
"name": "Learning GraphQL",
"store": {
"id": 1,
"name": "O'REILLY"
}
},
{
"id": 2,
"name": "Learning GraphQL",
"store": {
"id": 1,
"name": "O'REILLY"
}
},
{
"id": 3,
"name": "Learning GraphQL",
"store": {
"id": 1,
"name": "O'REILLY"
}
}
]