首发于Enaium的个人博客


本文运用Jimmer的官方用例来介绍Jimmer的运用办法,Jimmer同时支持JavaKotlin,本文运用Java来介绍,实际上KotlinJava运用起来更便利,这儿为了便利咱们了解,运用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便是运用JimmerSql目标,这个目标是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
  }
]

指定查询字段

怎样需求需求查询指定字段就能够这样,这儿的nameBookStoreTable的一个字段,但这儿的Controller回来的是BookStore目标,所以只如同上面的那样查询一切字段.

sql.createQuery(bookStore).select(bookStore.name()).execute();

像上面的比如中假如咱们非要查询指定字段又不想界说新的DTO目标,那么这种在Jimmer中也能够非常简略的完结,那便是运用Jimmer中的Fetchr

运用BookStoreFetchr来指定查询的字段

sql.createQuery(bookStore).select(bookStore.fetch(BookStoreFetcher.$.name())).execute();

查询成果如下:

[
  {
    "id": 2,
    "name": "MANNING"
  },
  {
    "id": 1,
    "name": "O'REILLY"
  }
]

惊奇的发现,Controller的回来类型是BookStore,可是查询成果中只要idname字段.

这儿我把完整的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()便是查询一切标量字段

在查询一切标量字段的基础上不查询BookStorename字段

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,这儿的JoinTypeLEFT,表明左衔接,假如不指定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办法回来的Mapkey便是书店的 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之核算特点