首发于Enaium的个人博客
本文使用Jimmer
的[官方用例]来介绍Jimmer
的使用方法,Jimmer
同时支持Java
和Kotlin
,本文使用Java
来介绍,实际上Kotlin
比Java
使用起来更方便,这里为了方便大家理解,使用Java
来介绍,本篇文章只是对Jimmer
的一个简单介绍,更多的内容请参考[官方文档]
这里开始就不从实体类开始介绍了,这里简单的把用到的三张表之间的关系介绍一下:
BookStore
书店 可以拥有多个Book
Book
书 可以属于多个BookStore
,可以有多个Author
Author
作者 可以拥有多个Book
,多对多书与作者的关系.
查询
Jimmer
可以配合SpringData
(不是SpringDataJPA
),但这里先介绍脱离SpringData
的使用方法,但还是在SpringBoot
环境下,这里使用H2
内存数据库,Jimmer
支持H2
,MySQL
,PostgreSQL
,Oracle
等数据库,这里使用H2
内存数据库.
这里的查询都使用Controller
来演示.
查询所有书店
createQuery
就是创建一个查询,select
就是选择要查询的字段,这里直接传入了BookStoreTable
表示查询所有字段.
这里用到的sql
就是使用Jimmer
的Sql
对象,这个对象是Jimmer
的核心对象,所有的查询都是通过这个对象来实现的,使用Spring
的注入方式注入JSqlClient
对象.
final BookStoreTable bookStore = BookStoreTable.$;//这里的$是一个静态方法,返回一个BookStoreTable对象
sql.createQuery(bookStore).select(bookStore).execute();
查询结果如下:
[
{
"createdTime": "2023-05-27 11:00:37",
"modifiedTime": "2023-05-27 11:00:37",
"id": 1,
"name": "O'REILLY",
"website": null
},
{
"createdTime": "2023-05-27 11:00:37",
"modifiedTime": "2023-05-27 11:00:37",
"id": 2,
"name": "MANNING",
"website": null
}
]
指定查询字段
如何需要需要查询指定字段就可以这样,这里的name
是BookStoreTable
的一个字段,但这里的Controller
返回的是BookStore
对象,所以只好像上面的那样查询所有字段.
sql.createQuery(bookStore).select(bookStore.name()).execute();
像上面的例子中如果我们非要查询指定字段又不想定义新的DTO
对象,那么这种在Jimmer
中也可以非常简单的实现,那就是使用Jimmer
中的Fetchr
使用BookStore
的Fetchr
来指定查询的字段
sql.createQuery(bookStore).select(bookStore.fetch(BookStoreFetcher.$.name())).execute();
查询结果如下:
[
{
"id": 2,
"name": "MANNING"
},
{
"id": 1,
"name": "O'REILLY"
}
]
惊奇的发现,Controller
的返回类型是BookStore
,但是查询结果中只有id
和name
字段.
这里我把完整的Controller
代码贴出来,List
的类型就是BookStore
的实体类,这就是Jimmer
的强大之处,不需要定义DTO
对象,就可以实现查询指定字段的功能.
@GetMapping("/simpleList")
public List<BookStore> findSimpleStores() {
final BookStoreTable bookStore = BookStoreTable.$;//这里的$是一个静态方法,返回一个BookStoreTable对象
return sql.createQuery(bookStore).select(bookStore.fetch(BookStoreFetcher.$.name())).execute();
}
和实体类的Table
一样,Fetcher
也可以声明一个静态常量.
private static final Fetcher<BookStore> SIMPLE_FETCHER = BookStoreFetcher.$.name();
这样就可以这样使用了.
sql.createQuery(bookStore).select(bookStore.fetch(SIMPLE_FETCHER)).execute();
接下来详细介绍Fetcher
的使用
查询所有标量字段,也就是非关联字段.
private static final Fetcher<BookStore> DEFAULT_FETCHER = BookStoreFetcher.$.allScalarFields();//这里的allScalarFields()就是查询所有标量字段
在查询所有标量字段的基础上不查询BookStore
的name
字段
private static final Fetcher<BookStore> DEFAULT_FETCHER = BookStoreFetcher.$.allScalarFields().name(false);//这里的name(false)就是不查询name字段
指定查询关联字段
像这样查询所有书店的所有书籍,并且查询书籍的所有作者,这样就可以使用Fetcher
来实现,如果在使用传统ORM
框架时,这里就需要定义一个DTO
对象来接收查询结果,但是在Jimmer
中,不需要定义DTO
对象,就可以实现查询指定字段的功能,可能有读者会问了,没有DTO
前端怎么接收数据呢,这里先剧透一下,Jimmer
会根据后端写的Fetcher
来生成前端的DTO
,这里就不多说了,后面会详细介绍.
private static final Fetcher<BookStore> WITH_ALL_BOOKS_FETCHER =
BookStoreFetcher.$
.allScalarFields()//查询所有标量字段
.books(//查询关联字段
BookFetcher.$//书籍的Fetcher
.allScalarFields()//查询所有标量字段
.authors(//查询关联字段
AuthorFetcher.$//作者的Fetcher
.allScalarFields()//查询所有标量字段
)
);
稍剧透一点,这里如果使用Kotlin
来编写会更加简洁,因为Kotlin
中的DSL
特性
private val WITH_ALL_BOOKS_FETCHER = newFetcher(BookStore::class).by {
allScalarFields()//查询所有标量字段
books {//查询关联字段
allScalarFields()//查询所有标量字段
authors {//查询关联字段
allScalarFields()//查询所有标量字段
}
}
}
这么一看Kotlin
确实比Java
简洁很多,但本篇文章还是介绍的是Java
的使用方法.
指定查询条件和计算结果字段
如果需要查询书店中所有书籍的平均价格,那么就要查询书店中所有书籍的价格,然后计算平均值,这里先把查询的代码写出来,然后在介绍如何把计算结果字段添加到Fetcher
中.
sql.createQuery(bookStore)//这里的bookStore是一个BookStoreTable对象
.where(bookStore.id().in(ids))//要查询的书店的id集合,也可以直接指定id,比如.eq(1L)
.groupBy(bookStore.id())//按照书店的id分组
.select(
bookStore.id(),//查询书店的id
bookStore.asTableEx().books(JoinType.LEFT).price().avg().coalesce(BigDecimal.ZERO)//查询书店中所有书籍的平均价格
)
.execute();//这样执行查询后,返回的结果就是书店的id和书店中所有书籍的平均价格,在Jimmer中会返回一个List<Tuple2<...>>类型的结果,其中Tuple元组的数量和查询的字段数量一致,这里就是2个字段,所以就是Tuple2
这里最后的select
是查出了书店的 id 和书店中所有书籍的平均价格,asTableEx()
是为了突破Jimmer
的限制,Jimmer
中的Table
只能查询标量字段,而不能查询关联字段,这里的asTableEx()
就是为了查询关联字段,asTableEx()
的参数是JoinType
,这里的JoinType
是LEFT
,表示左连接,如果不指定JoinType
,默认是INNER
,表示内连接.
这里的avg()
是计算平均值的意思,coalesce(BigDecimal.ZERO)
是为了防止计算结果为null
,如果计算结果为null
,那么就返回BigDecimal.ZERO
.
这里介绍如何把计算结果字段添加到Fetcher
中,这样就又引出了一个Jimmer
的功能计算属性
计算属性
在Jimmer
中如果要添加计算属性,那么就要实现TransientResolver
接口,这里先把代码贴出来,然后再详细介绍.
@Component
public class BookStoreAvgPriceResolver implements TransientResolver<Long, BigDecimal> {
@Override
public Map<Long, BigDecimal> resolve(Collection<Long> ids) {
return null;
}
}
这里的ids
就是书店的 id 集合,这里的resolve
方法就是计算书店中所有书籍的平均价格,这里的Long
是书店的 id,BigDecimal
是书店中所有书籍的平均价格,这里的resolve
方法返回的Map
的key
就是书店的 id,value
就是书店中所有书籍的平均价格.
接着配合上面写的查询代码,完成计算的代码
BookStoreTable bookStore = BookStoreTable.$;
return sql.createQuery(bookStore)
.where(bookStore.id().in(ids))
.groupBy(bookStore.id())
.select(
bookStore.id(),
bookStore.asTableEx().books(JoinType.LEFT).price().avg().coalesce(BigDecimal.ZERO)
)
.execute()//这里的execute()返回的结果是List<Tuple2<Long, BigDecimal>>类型的
.stream()//这里把List转换成Stream
.collect(
Collectors.toMap(Tuple2::get_1, Tuple2::get_2)//这里把List<Tuple2<Long, BigDecimal>>转换成Map<Long, BigDecimal>
);
这样一个TransientResolver
的实现就完成了,接着就是把TransientResolver
添加到实体类中
Jimmer
中定义实体类是在接口中定义的
@Transient(BookStoreAvgPriceResolver.class)//这里的BookStoreAvgPriceResolver.class就是上面写的计算属性的实现
BigDecimal avgPrice();//这里的avgPrice()就是计算属性,这里的BigDecimal就是计算属性的类型
这样就可以直接在Fetcher
中查询计算属性了
private static final Fetcher<BookStore> WITH_ALL_BOOKS_FETCHER =
BookStoreFetcher.$
.allScalarFields()
.avgPrice()//这里就是查询计算属性
//...省略
接着看戏生成的SQL
代码和查询结果,这里照样省略其他查询只关注标量字段和计算属性
select
tb_1_.ID,
coalesce(
avg(tb_2_.PRICE), ? /* 0 */
)
from BOOK_STORE tb_1_
left join BOOK tb_2_
on tb_1_.ID = tb_2_.STORE_ID
where
tb_1_.ID in (
? /* 1 */
)
group by
tb_1_.ID
{
"createdTime": "2023-05-27 12:04:39",
"modifiedTime": "2023-05-27 12:04:39",
"id": 1,
"name": "O'REILLY",
"website": null,
"avgPrice": 58.5
}
定义实体类
在Jimmer
中定义实体类是在接口中定义的,这里先把代码贴出来,然后再详细介绍.
BookStore
@Entity//这里的@Entity就是实体类
public interface BookStore extends BaseEntity {
@Id//这里的@Id就是主键
@GeneratedValue(strategy = GenerationType.IDENTITY)//这里的strategy = GenerationType.IDENTITY就是自增长
long id();//这里的id()就是实体类的id
@Key
String name();//业务主键
@Null//这里的@Null就是可以为null,建议使用Jetbrains的@Nullable
String website();
@OneToMany(mappedBy = "store", orderedProps = {
@OrderedProp("name"),
@OrderedProp(value = "edition", desc = true)
})//这里的@OneToMany就是一对多,这里的mappedBy = "store"就是Book中的store字段,这里的orderedProps就是排序字段
List<Book> books();
@Transient(BookStoreAvgPriceResolver.class)//这里的BookStoreAvgPriceResolver.class就是上面写的计算属性的实现
BigDecimal avgPrice();//这里的avgPrice()就是计算属性,这里的BigDecimal就是计算属性的类型
}
Book
@Entity
public interface Book extends TenantAware {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();
@Key//这里的@Key就是业务主键
String name();
@Key//和上面的name()一样,这里的@Key就是业务主键,表示name和edition的组合是唯一的
int edition();
BigDecimal price();
@Nullable
@ManyToOne
BookStore store();
@ManyToMany(orderedProps = {
@OrderedProp("firstName"),
@OrderedProp("lastName")
})//这里的@ManyToMany就是多对多,这里的orderedProps就是排序字段
@JoinTable(
name = "BOOK_AUTHOR_MAPPING",//这里的name就是中间表的表名
joinColumnName = "BOOK_ID",//这里的joinColumnName就是中间表的外键
inverseJoinColumnName = "AUTHOR_ID"//这里的inverseJoinColumnName就是中间表的外键
)
List<Author> authors();
}
Author
@Entity
public interface Author extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();
@Key
String firstName();
@Key
String lastName();
Gender gender();//这里的Gender就是枚举类型
@ManyToMany(mappedBy = "authors", orderedProps = {
@OrderedProp("name"),
@OrderedProp(value = "edition", desc = true)
})//这里的@ManyToMany就是多对多,这里的mappedBy = "authors"就是Book中的authors字段,这里的orderedProps就是排序字段
List<Book> books();
}
public enum Gender {
@EnumItem(name = "M")//这里的name表示在数据库中存储的值
MALE,
@EnumItem(name = "F")
FEMALE
}
如果使用过Spring Data JPA
的话,这里的代码应该很熟悉,Jimmer
中的实体类的关联关系和Spring Data JPA
中的关联关系是一样的.
生成前端代码
还记得前面的剧透吗,现在开始正式介绍如何生成前端代码,这里先把生成的代码贴出来,然后再详细介绍.
DTO
这里生成了好多根据Controller
的返回类型的Fetcher
生成的DTO
,这里就不贴出来了,只贴一个BookStoreDto
的代码.
export type BookStoreDto = {
//只有查询书店的name
"BookStoreService/SIMPLE_FETCHER": {
readonly id: number
readonly name: string
}
//查询书店的所有字段
"BookStoreService/DEFAULT_FETCHER": {
readonly id: number
readonly createdTime: string
readonly modifiedTime: string
readonly name: string
readonly website?: string
}
//查询书店的所有字段和书店中所有书籍的所有字段还有书籍的所有作者的所有字段
"BookStoreService/WITH_ALL_BOOKS_FETCHER": {
readonly id: number
readonly createdTime: string
readonly modifiedTime: string
readonly name: string
readonly website?: string
readonly avgPrice: number //这里的avgPrice就是计算属性
readonly books: ReadonlyArray<{
readonly id: number
readonly createdTime: string
readonly modifiedTime: string
readonly name: string
readonly edition: number
readonly price: number
readonly authors: ReadonlyArray<{
readonly id: number
readonly createdTime: string
readonly modifiedTime: string
readonly firstName: string
readonly lastName: string
readonly gender: Gender
}>
}>
}
}
Controller
这里只看BookStoreController
的主要请求
这里Jimmer
把所有的Controller
的请求都放在了一个Controller
中,这里的Controller
就是BookStoreController
,这里的BookStoreController
就是BookStore
实体类的Controller
,这里的BookStoreController
的代码如下
async findComplexStoreWithAllBooks(options: BookStoreServiceOptions['findComplexStoreWithAllBooks']): Promise<
BookStoreDto['BookStoreService/WITH_ALL_BOOKS_FETCHER'] | undefined
> {
let _uri = '/bookStore/';
_uri += encodeURIComponent(options.id);
_uri += '/withAllBooks';
return (await this.executor({uri: _uri, method: 'GET'})) as BookStoreDto['BookStoreService/WITH_ALL_BOOKS_FETCHER'] | undefined
}
async findSimpleStores(): Promise<
ReadonlyArray<BookStoreDto['BookStoreService/SIMPLE_FETCHER']>
> {
let _uri = '/bookStore/simpleList';
return (await this.executor({uri: _uri, method: 'GET'})) as ReadonlyArray<BookStoreDto['BookStoreService/SIMPLE_FETCHER']>
}
async findStores(): Promise<
ReadonlyArray<BookStoreDto['BookStoreService/DEFAULT_FETCHER']>
> {
let _uri = '/bookStore/list';
return (await this.executor({uri: _uri, method: 'GET'})) as ReadonlyArray<BookStoreDto['BookStoreService/DEFAULT_FETCHER']>
}
配置代码生成
需要再配置中指定生成代码的访问地址,因为Jimmer
生成的前端代码是一个压缩包,访问这个地址就可以下载生成的源码了
jimmer:
client:
ts:
path: /ts.zip #这里的path就是访问地址
接着配置Controller
的返回类型
@GetMapping("/simpleList")
public List<@FetchBy("SIMPLE_FETCHER") BookStore> findSimpleStores() {
final BookStoreTable bookStore = BookStoreTable.$;
return sql.createQuery(bookStore).select(bookStore.fetch(SIMPLE_FETCHER)).execute();
}
这里使用了FetchBy
注解,其中的值就是当前类的Fetcher
常量,如果Fetcher
不在当前的类下,可以指定注解中的ownerType
来指定Fetcher
所在的类.
好了,Jimmer
的基本使用就介绍完了,如果想了解更多的使用方法,可以查看Jimmer
的[文档],也可以观看Jimmer
作者录制的视频教程Jimmer0.6x: 前后端免对接+spring starter,让REST媲美GraphQL,Jimmer-0.7之计算属性