首发于 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 之计算属性