首发于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
接口,这里先把代码贴出来,而后再具体介绍.
@Componentpublic 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_IDwhere 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
@Entitypublic 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
@Entitypublic 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之计算属性