Java의 혁신적인 ORM 프레임워크 Jimmer에 대한 간략한 소개

에나이움 개인 블로그 에 최초 공개


이 글 에 Jimmer사용된 공식 사용 사례는Jimmer 사용 방법을 소개하는 데 사용되며 Jimmer동시에 지원 JavaKotlin, 이 글은 Java실제로 사용하는 것 Kotlin보다 더 편리하다는 것을 소개하는 데 사용됩니다. Java, 소개에 사용 , 이 글은 간략한 소개 Java일 뿐 , 자세한 내용은 공식 문서를 참고하세요.Jimmer

엔터티 클래스 소개로 시작하지 말고 사용된 세 테이블 간의 관계에 대한 간략한 소개는 다음과 같습니다.

  • BookStore서점은 여러 개를 가질 수 있습니다Book
  • Book책은 여러 개에 속할 수 있고 BookStore여러 개를 가질 수 있습니다.Author
  • AuthorBook저자는 다수의 다대다 책과 저자 관계를 가질 수 있습니다 .

문의

JimmerSpringData(아님 ) 사용할 수 있지만 여기서는 먼저 분리 사용 방법을 SpringDataJPA소개 하지만 여전히 환경에서 여기서는 메모리 데이터베이스, 지원 , , 및 기타 데이터베이스를 사용하고 여기서는 메모리 데이터베이스를 사용합니다 .SpringDataSpringBootH2JimmerH2MySQLPostgreSQLOracleH2

여기에 있는 쿼리는 모두 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오브젝트 이므로 위와 같이 모든 필드를 쿼리하면 됩니다 .BookStoreTableControllerBookStore

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

위의 예에서와 같이 지정된 필드에 대한 쿼리를 고집하고 새 개체를 정의하지 않으려는 경우 DTO이 역시 매우 간단 Jimmer하게 구현할 수 있습니다.JimmerFetchr

쿼리를 지정하는 데 사용되는 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엔터티 클래스입니다 .JimmerDTO

@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,这里的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

Controllerreturn type here 기반으로 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
      }>
    }>
  }
}

제어 장치

여기에서 BookStoreController주요 요청 만 확인하십시오.

여기에 Jimmer모든 Controller요청이 하나에 입력됩니다 Controller. 여기에는 엔터티 클래스가 Controller있습니다 . 여기의 코드는 다음과 같습니다.BookStoreControllerBookStoreControllerBookStoreControllerBookStoreController

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짐머의 기본 사용법은 끝났습니다. 사용법에 대해 더 알고 싶다면 설명서를 확인하거나 저자가 녹화한 비디오 자습서를 볼 수 있습니다 속성계산-0.7Jimmer.Jimmer

추천

출처juejin.im/post/7237663395057811512