本文使用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();
@.NEToMany(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所在的類.