关于java:Java革命性ORM框架Jimmer简单介绍

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理