首发于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之核算特点