简单计算
简单计算属性是使用@org.babyfish.jimmer.sql.Formula声明的属性,有两者用法
- 基于Java/Kotlin的计算属性
- 基于SQL的计算属性
简单计算属性为实现简单而快速的计算而设计,如果需要复杂的计算,请采用复杂计算属性
在定义实体一文中,我们为Author定义了两个字段:firstName和lastName。
接下来,让我们为Author添加一个叫新属性fullName:
fullName = firstName + ' ' + lastName
接下来,我们用两者不同的方式,即基于Java/Kotlin的计算和基于SQL的计算,来实现Author.fullName
1. 基于Java/Kotlin的计算
依赖普通属性
- Java
- Kotlin
package com.example.model;
import org.babyfish.jimmer.sql.*;
@Entity
public interface Author {
String firstName();
String lastName();
@Formula(dependencies = {"firstName", "lastName"})
default String fullName() {
return firstName() + ' ' + lastName();
}
...省略其他属性...
}
package com.example.model
import org.babyfish.jimmer.sql.*
@Entity
interface Author {
val firstName: String
val lastName: String
@Formula(dependencies = ["firstName", "lastName"])
val fullName: String
get() = "$firstName $lastName"
...省略其他属性...
}
不难发现,基于Java/Kotlin的简单计算属性有以下特征:
-
属性不是抽象的(Java下需要使用
default关键字),直接给出计算逻辑实现。 -
@Formula的dependencies被指定,表示当前属性依赖于Author.firstName和Author.lastName。即,动态实体必须确保同时具备
firstName和lastName属性才可以计算fullName
用法如下
- Java
- Kotlin
Author author = authorRepository.findNullable(
1L,
Fetchers.AUTHOR_FETCHER
//查询id(隐含+强制)和fullName
.fullName()
);
System.out.println(author);
val author = authorRepository.findNullable(
1L,
newFetcher(Author::class).by {
//查询id(隐含+强制)和fullName
fullName()
}
);
println(author)
执行的SQL为
select
tb_1_.ID,
tb_1_.FIRST_NAME,
tb_1_.LAST_NAME
from AUTHOR as tb_1_
where tb_1_.ID = ?
fullName是计算属性,在数据库中无对应字段,但其依赖于firstName和lastName,
所以此SQL查询FIRST_NAME和LAST_NAME,让其依赖的属性存在。
接下来,我们看看代码中打印会输出什么
{"id":1,"fullName":"Eve Procello"}
我们看到,Jackson序列化(实体对象的toString方法是序列化的一种快捷方式)后只有fullName,但没有firstName和lastName。
这是,因为对象抓取器因抓取fullName而导致firstName和lastName被间接抓取,他们并未被直接抓取。
在这种情况下,虽然动态对象具备firstName和lastName,但它们被标记成对Jackson不可见的状态,不会出现在Jackson序列化结果中。
如果让对象抓取器直接抓取firstName和lastName,那么它们一定会出现在序列化结果中。读者可以自行试验,这里不再赘述。
依赖Embbeddable
假设有一个Embeddable类型
- Java
- Kotlin
@Embeddable
public interface NameInfo {
String firstName();
String lastName();
}
@Embeddable
interface NameInfo {
val firstName: String
val lastName: String
}
如果某实体使用了此Embeddable类型,那么实体属性可以依赖其内部属性,例如
- Java
- Kotlin
@Entity
public interface Author {
NameInfo nameInfo();
@Formula(dependencies = {"nameInfo.firstName", "nameInfo.lastName"})
// 也可以写成:@Formula(dependencies = "nameInfo")
default String fullName() {
return nameInfo().firstName() + ' ' + nameInfo().lastName();
}
...省略其他属性...
}
@Entity
interface Author {
val nameInfo: NameInfo
@Formula(dependencies = ["nameInfo.firstName", "nameInfo.lastName"])
// 也可以写成:@Formula(dependencies = ["nameInfo"])
...省略其他属性...
val fullName: String
get() = "${nameInfo.firstName} ${nameInfo.lastName}"
}
使用方法和执行效果完全同上,无需重复。
依赖关联属性
- Java
- Kotlin
@Entity
public interface Book {
@ManyToMany
List<Author> authors();
@Formula(dependencies = "authors")
default int authorCount() {
return authors().size();
}
@Formula(dependencies = {"authors.firstName", "authors.lastName"})
default List<String> authorNames() {
return authors()
.stream()
.map(author -> author.firstName() + ' ' + author.lastName())
.collect(Collectors.toList());
}
...省略其他属性...
}
@Entity
public interface Book {
@ManyToMany
val authors: List<Author>
@Formula(dependencies = "authors")
val authorCount: Int
get() = authors.size
@Formula(dependencies = ["authors.firstName", "authors.lastName"])
val authorNames: List<String>
get() = authors.map { "${it.firstName} ${it.lastName}" }
...省略其他属性...
}
执行如下代码
- Java
- Kotlin
BookTable table = BookTable.$;
List<Book> books = sqlClient
.createQuery(table)
.where(table.name().eq("Learning GraphQL"))
.orderBy(table.edition().desc())
.select(
table.fetch(
BookFetcher.$
.name()
.edition()
.authorCount()
.authorNames()
)
)
.execute();
val books = sqlClient
.createQuery(Book::class) {
where(table.name eq "Learning GraphQL")
orderBy(table.edition().desc())
select(
table.fetchBy {
name()
edition()
authorCount()
authorNames()
}
)
}
.execute()
执行,生成如下两条SQL语句
-
select
tb_1_.ID,
tb_1_.NAME,
tb_1_.EDITION,
from BOOK tb_1_
where
tb_1_.NAME = ? /* Learning GraphQL */
order by
tb_1_.EDITION desc -
select
tb_2_.BOOK_ID,
tb_1_.ID,
tb_1_.FIRST_NAME,
tb_1_.LAST_NAME
from AUTHOR tb_1_
inner join BOOK_AUTHOR_MAPPING tb_2_
on tb_1_.ID = tb_2_.AUTHOR_ID
where
tb_2_.BOOK_ID in (
? /* 3 */, ? /* 2 */, ? /* 1 */
)
order by
tb_1_.FIRST_NAME asc,
tb_1_.LAST_NAME asc
得到的数据如下
[
{
"id":3,
"name":"Learning GraphQL",
"edition":3,
"authorCount":2,
"authorNames":["Alex Banks","Eve Procello"]
},
{
"id":2,
"name":"Learning GraphQL",
"edition":2,
"authorCount":2,
"authorNames":["Alex Banks","Eve Procello"]
},
{
"id":1,
"name":"Learning GraphQL",
"edition":1,
"authorCount":2,
"authorNames":["Alex Banks","Eve Procello"]
}
]
2. 基于SQL的计算
- Java
- Kotlin
package com.example.model;
import org.babyfish.jimmer.sql.*;
@Entity
public interface Author {
@Formula(sql = "concat(%alias.FIRST_NAME, ' ', %alias.LAST_NAME)")
String fullName();
...省略其他属性...
}
package com.example.model
import org.babyfish.jimmer.sql.*
@Entity
interface Author {
@Formula(sql = "concat(%alias.FIRST_NAME, ' ', %alias.LAST_NAME)")
val fullName: String
...省略其他属性...
}
不难发现,基于SQL的简单计算属性有以下特征
-
属性是抽象的。
-
@Formula的sql被指定为一个SQL表达式,内部有一个特殊的符号%alias用户无法事先知道当前表在最终SQL中的别名,所以,Jimmer在这里约定
%alias表示实际的表列名
用法如下
- Java
- Kotlin
Author author = authorRepository.findNullable(
1L,
Fetchers.AUTHOR_FETCHER
//查询id(隐含+强制)和fullName
.fullName()
);
System.out.println(author);
val author = authorRepository.findNullable(
1L,
newFetcher(Author::class).by {
//查询id(隐含+强制)和fullName
fullName()
}
);
println(author)
生成SQL如下:
select
tb_1_.ID,
/* 这里,@Formula.sql中的`alias`被替换成了`tb_1_` */
concat(tb_1_.FIRST_NAME, ' ', tb_1_.LAST_NAME)
from AUTHOR as tb_1_
where tb_1_.ID = ?
最终打印结果
{"id":1,"fullName":"Eve Procello"}
比较
相比于基于Java/Kotlin的简单计算,基于SQL的简单计算有一个缺点和一个优点。
-
缺点:如果对象抓取器同时抓取
firstName、lastName和fullName,会导致最终SQL查询三列:tb_1_.FIRST_NAME,tb_1_.LAST_NAME和concat(tb_1_.FIRST_NAME, ' ', tb_1_.LAST_NAME)。很明显,返回数据包含冗余,是一种浪费。
-
优点:基于Java/Kotlin的计算属性只能作为对象抓取器的被抓取字段,无法为SQL DSL所用。
而基于SQL的计算属性会被代码生成器生成到强类型SQL DSL的API中,为SQL DSL所用,如
Java的
where(table.fullName().eq("Eve Procello"))或Kotlin的where(table.fullName eq "Eve Procello")提示所有计算属性中,只有基于SQL的简单计算属性可以被SQL DSL使用。
建议使用支持函数索引的数据库,并和函数索引结合使用。
因此,建议认真考虑@Formula计算属性应该基于Java/Kotlin计算还是基于SQL计算。