直接查询中间表
被对象模型隐藏的中间表
让我们回顾一下这段实体接口定义代码
- Java
- Kotlin
@Entity
public interface Book {
@ManyToMany
@JoinTable(
name = "BOOK_AUTHOR_MAPPING",
joinColumnName = "BOOK_ID",
inverseJoinColumnName = "AUTHOR_ID"
)
List<Author> authors();
...omit other code...
}
@Entity
interface Book {
@ManyToMany
@JoinTable(
name = "BOOK_AUTHOR_MAPPING",
joinColumnName = "BOOK_ID",
inverseJoinColumnName = "AUTHOR_ID"
)
val authors: List<Author>
...omit other code...
}
上述代码中,BOOK_AUTHOR_MAPPING
表作为中间表被使用。
-
数据库的BOOK表,Java代码有与之对应的实体接口Book。
-
数据库的AUTHOR表,Java代码有与之对应的实体接口Author。
-
但是,数据库中的BOOK_AUTHOR_MAPPING表,在Java代码中没有对应的实体接口。
即,中间表被对象模型隐藏了。
直接查询中间表
Jimmer提供了一个有趣的功能,即便中间表被隐藏没有对应实体,也可以对其直接查询。
- Java
- Kotlin
AssociationTable<Book, BookTableEx, Author, AuthorTableEx> association =
AssociationTable.of(BookTableEx.class, BookTableEx::authors);
List<Association<Book, Author>> associations =
sqlClient
.createAssociationQuery(association)
.where(association.source().id().eq(3L))
.select(association)
.execute();
associations.forEach(System.out::println);
val associations = sqlClient
.queries
.forList(Book::authors) {
where(table.source.id eq 3L)
select(table)
}
.execute()
associations.forEach(::println)
这里,Java的createAssociationQuery
或Kotlin的queries.forList
表示基于中间表创建查询,而非基于实体表。
这里的Java代码示范为了兼容Java8,第一行的变量association
的类型比较复杂。建议提高Java的版本,采用var
关键字。
生成的SQL如下
select
tb_1_.BOOK_ID,
tb_1_.AUTHOR_ID
/* highlight-next-line */
from BOOK_AUTHOR_MAPPING as tb_1_
where tb_1_.BOOK_ID = ? /* 3 */
果然,这是一个基于中间表的查询。
最终打印结果如下(原输出是紧凑的,为了方便阅读,这里进行了格式化):
Association{
source={
"id":3
}, target={
"id":1
}
}
Association{
source={
"id":3
},
target={
"id":2
}
}
返回数据是一系列Association对象
package org.babyfish.jimmer.sql.association;
public class Association<S, T> {
public Association(S source, T target) {
this.source = source;
this.target = target;
}
public S source() {
return source;
}
public T target() {
return target;
}
}
Association<S, T>
表示从S
类型指向T
类型关联的中间表实体。中间表实体是伪实体,没有id。它只有两个属性:
source
: 中间表指向己方的外键所对应的对象(在这个例子中,就是Book
对象)。target
: 中间表指向对方的外键所对应的对象(在这个例子中,就是Author
对象)。
-
在这个例子中,并未使用对象抓取器定义Association的对象格式(事实上Association也不支持对象抓取器),因此对象的
source
和target
关联属性仅包含对象id。 -
Author
也有一个从动的多对多关联Author.books
, 它是Book.authors
的镜像。- Java
- Kotlin
@Entity
public interface Author {
@ManyToMany(mappedBy = "authors")
List<Book> books();
...
}@Entity
interface Author {
@ManyToMany(mappedBy = "authors")
val books: List<Book>
...
}基于
Author.books
也可以创建中间表查询,但是source
代表Author,而target
代表Book。和当前例子相反。
这个例子中,我们只查询了中间表本身。所以,source
和target
对象中只有id。
要获取完整的source
和target
对象,可以表连接,然后利用元组进行返回。
代码如下
- Java
- Kotlin
AssociationTable<Book, BookTableEx, Author, AuthorTableEx> association =
AssociationTable.of(BookTableEx.class, BookTableEx::authors);
List<Tuple2<Book, Author>> tuples =
sqlClient
.createAssociationQuery(association)
.where(association.source().id().eq(3L))
.select(
association.source(),
association.target()
)
.execute();
tuples.forEach(System.out::println);
val associations = sqlClient
.queries
.forList(Book::authors) {
where(table.source.id eq 3L)
select(
table.source,
table.target
)
}
.execute()
associations.forEach(::println)
生成的SQL如下:
select
/* source() */
tb_1_.BOOK_ID,
tb_2_.NAME,
tb_2_.EDITION,
tb_2_.PRICE,
tb_2_.STORE_ID,
/* target() */
tb_1_.AUTHOR_ID,
tb_3_.FIRST_NAME,
tb_3_.LAST_NAME,
tb_3_.GENDER
from BOOK_AUTHOR_MAPPING as tb_1_
inner join BOOK as tb_2_
on tb_1_.BOOK_ID = tb_2_.ID
inner join AUTHOR as tb_3_
on tb_1_.AUTHOR_ID = tb_3_.ID
where tb_1_.BOOK_ID = ? /* 3 */
最终打印结果如下(原输出是紧凑的,为了方便阅读,这里进行了格式化):
Tuple2{
_1={
"id":3,
"name":"Learning GraphQL",
"edition":3,
"price":51.00,
"store":{
"id":1
}
},
_2={
"id":1,
"firstName":"Alex",
"lastName":"Banks",
"gender":"MALE"
}
}
Tuple2{
_1={
"id":3,
"name":"Learning GraphQL",
"edition":3,
"price":51.00,
"store":{
"id":1
}
},
_2={
"id":2,
"firstName":"Eve",
"lastName":"Procello",
"gender":"MALE"
}
}
和非中间表查询的对比
读者可能会认为,基于中间表查询的查询存在的价值,是为了让开发人员写出性能更高的查询。
但其实不是这样的。由于幻连接和半连接这两个优化手段的存在,无论是否使用基于中间表的查询,都能达到很好的性能。是否选择使用基于中间表的查询,完全看用户自己喜好。
1. 基于中间表子查询实现一个功能
之前的代码,我们演示基于中间表的顶级查询;而这个例子演示基于中间表的子查询。
- Java
- Kotlin
BookTable table = Tables.BOOK_TABLE;
AssociationTable<Book, BookTableEx, Author, AuthorTableEx> association =
AssociationTable.of(BookTableEx.class, BookTableEx::authors);
List<Book> books = sqlClient
.createQuery(table)
.where(
table.id().in(
sqlClient
.createAssociationSubQuery(association)
.where(
association
.target() ❶
.firstName().eq("Alex")
)
.select(
association
.source() ❷
.id()
)
)
)
.select(table)
.execute();
val books = sqlClient
.createQuery(Book::class) {
where(
table.id valueIn
subQueries.forList(Book::authors) {
where(
table
.target ❶
.firstName eq "Alex"
)
select(
table
.source ❷
.id
)
}
)
select(table)
}
.execute()
其中
-
Java的
createAssociationSubQuery
和Kotlin的subQueries.forList
用于创建一个基于中间表的子查询。该查询用户寻找所有包含firstName
为"Alex"的作者的书籍。 -
❶处
association.target
是真正的表连接,会生成SQL JOIN连接AUTHOR
表进行条件判断。 -
❷处
association.source
是由于幻连接,并不会生成SQL join。
最终生成的SQL如下:
select
tb_1_.ID,
tb_1_.NAME,
tb_1_.EDITION,
tb_1_.PRICE,
tb_1_.STORE_ID
from BOOK as tb_1_
where
tb_1_.ID in (
/* highlight-next-line */
select
tb_2_.BOOK_ID
from BOOK_AUTHOR_MAPPING as tb_2_
inner join AUTHOR as tb_3_
on tb_2_.AUTHOR_ID = tb_3_.ID
where tb_3_.FIRST_NAME = ?
)