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

41次阅读

共计 9750 个字符,预计需要花费 25 分钟才能阅读完成。

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

正文完
 0