跳到主要内容

简单计算

简单计算属性是使用@org.babyfish.jimmer.sql.Formula声明的属性,有两者用法

  • 基于Java/Kotlin的计算属性
  • 基于SQL的计算属性
备注

简单计算属性为实现简单而快速的计算而设计,如果需要复杂的计算,请采用复杂计算属性

定义实体一文中,我们为Author定义了两个字段:firstNamelastName

接下来,让我们为Author添加一个叫新属性fullName

fullName = firstName + ' ' + lastName

接下来,我们用两者不同的方式,即基于Java/Kotlin的计算和基于SQL的计算,来实现Author.fullName

1. 基于Java/Kotlin的计算

依赖普通属性

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

...省略其他属性...
}

不难发现,基于Java/Kotlin的简单计算属性有以下特征:

  • 属性不是抽象的(Java下需要使用default关键字),直接给出计算逻辑实现。

  • @Formuladependencies被指定,表示当前属性依赖于Author.firstNameAuthor.lastName

    即,动态实体必须确保同时具备firstNamelastName属性才可以计算fullName

用法如下

Author author = authorRepository.findNullable(
1L,
Fetchers.AUTHOR_FETCHER
//查询id(隐含+强制)和fullName
.fullName()
);
System.out.println(author);

执行的SQL为

select 
tb_1_.ID,
/* highlight-start */
tb_1_.FIRST_NAME,
tb_1_.LAST_NAME
/* highlight-end */
from AUTHOR as tb_1_
where tb_1_.ID = ?

fullName是计算属性,在数据库中无对应字段,但其依赖于firstNamelastName, 所以此SQL查询FIRST_NAMELAST_NAME,让其依赖的属性存在。

接下来,我们看看代码中打印会输出什么

{"id":1,"fullName":"Eve Procello"}

我们看到,Jackson序列化(实体对象的toString方法是序列化的一种快捷方式)后只有fullName,但没有firstNamelastName

这是,因为对象抓取器因抓取fullName而导致firstNamelastName被间接抓取,他们并未被直接抓取。

在这种情况下,虽然动态对象具备firstNamelastName,但它们被标记成对Jackson不可见的状态,不会出现在Jackson序列化结果中。

备注

如果让对象抓取器直接抓取firstNamelastName,那么它们一定会出现在序列化结果中。读者可以自行试验,这里不再赘述。

依赖Embbeddable

假设有一个Embeddable类型

NameInfo.java
@Embeddable
public interface NameInfo {
String firstName();
String lastName();
}

如果某实体使用了此Embeddable类型,那么实体属性可以依赖其内部属性,例如

Author.java
@Entity
public interface Author {

NameInfo nameInfo();

@Formula(dependencies = {"nameInfo.firstName", "nameInfo.lastName"})
// 也可以写成:@Formula(dependencies = "nameInfo")
default String fullName() {
return nameInfo().firstName() + ' ' + nameInfo().lastName();
}

...省略其他属性...
}
信息

使用方法和执行效果完全同上,无需重复。

依赖关联属性

Book.java
@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());
}

...省略其他属性...
}

执行如下代码

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

执行,生成如下两条SQL语句

  1. 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
  2. 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的计算

Author.java
package com.example.model;

import org.babyfish.jimmer.sql.*;

@Entity
public interface Author {

@Formula(sql = "concat(%alias.FIRST_NAME, ' ', %alias.LAST_NAME)")
String fullName();

...省略其他属性...
}

不难发现,基于SQL的简单计算属性有以下特征

  • 属性是抽象的。

  • @Formulasql被指定为一个SQL表达式,内部有一个特殊的符号%alias

    用户无法事先知道当前表在最终SQL中的别名,所以,Jimmer在这里约定%alias表示实际的表列名

用法如下

Author author = authorRepository.findNullable(
1L,
Fetchers.AUTHOR_FETCHER
//查询id(隐含+强制)和fullName
.fullName()
);
System.out.println(author);

生成SQL如下:

select 
tb_1_.ID,
/* 这里,@Formula.sql中的`alias`被替换成了`tb_1_` */
/* highlight-next-line */
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的简单计算有一个缺点和一个优点。

  • 缺点:如果对象抓取器同时抓取firstNamelastNamefullName,会导致最终SQL查询三列:

    tb_1_.FIRST_NAME, tb_1_.LAST_NAMEconcat(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计算。