跳到主要内容

DTO查询

之前我们介绍了,利用对象抓取器灵活控制被查询数据结构的形状。

现在,我们介绍一个与之等价的功能:查询DTO对象。

Jimmer提供了DTO语言

提示

该语言本质上是对象抓取器的另外一种表达方式

利用该语言,开发人员可以快速以某个实体类型为聚合根定义多种数据结构的形状,编译器会为每种形状定义生成相应的Java/Kotlin DTO类。每个DTO类型都包含和原动态类型之间的彼此转化逻辑,以及一个和自身形状匹配的对象抓取器。

某些情况下,服务端查询出某种形状的数据后,并不是为了作为HTTP请求的返回,而是自己用,用来驱动后续的复杂的业务逻辑,这是采用这种方式的理想场合。

警告

注意,如果服务端查询某种形状的数据不是为了自己用,而是为了直接作为HTTP请求的返回值,则更推荐直接返回动态实体对象,并利用客户端篇中的方案自动生成开发体验很高的客户端代码。

定义DTO的形状

本文侧重于讲解如何查询静态DTO类型,并非系统性介绍DTO语言,请参考对象篇/DTO转换/DTO语言以了解完整的DTO语言。

假如Book类的全名为com.yourcompany.yourproject.model.Book,你可以

  1. 在实体定义所在项目中,建立目录src/main/dto

  2. src/main/dto下,建立文件Book.dto

  3. 编辑此文件,利用DTO语言,定义Book实体的各种DTO形状

    Book.dto
    export com.yourcompany.yourproject.model.Book 
    -> package com.yourcompany.yourproject.model.dto

    BookDetailView {

    #allScalars

    store {
    #allScalars
    }

    authors {
    #allScalars
    }
    }

    SimpleBookView { ...... }

    ...省略其他DTO形状定义...

自动生成DTO类型

Jimmer负责编译dto文件,自动生成符合这些形状的DTO类型。

警告

如果除了dto文件外还有其他Java/Kotlin原代码文件被修改了,直接点击IDE中运行按钮可以导致dto文件的重新编译

但是,如果除了dto文件外没有其他Java/Kotlin文件被修改,简单地点击IDE中运行按钮并不会导致dto文件被重新编译,除非显式地rebuild!

如果你使用的构建工具是Gradle,也可以使用社区提供的第三方Gradle插件来解决这个问题: jimmer-gradle

以上面代码中的BookDetailView为例,此dto文件被Jimmer成功编译后,会自动生成如下DTO类型

BookDetailView.java
package com.yourcompany.yourproject.model.dto;

import com.yourcompany.yourproject.model.Book;
import org.babyfish.jimmer.View;

@GeneratedBy(file = "<your_project>/src/main/dto/Book.dto")
public class BookDetailView implements Input<Book> {

private long id;

private String name;

private int edition;

private BigDecimal price;

private TargetOf_store store;

private List<TargetOf_authors> authors;

public static class TargetOf_store implements Input<BookStore> {

private long id;

private String name;

@Nullable
private String website;

...省略其他成员...
}

public static class TargetOf_authors implements Input<Author> {

private long id;

private String firstName;

private String lastName;

private Gender gender;

...省略其他成员...
}

...省略其他成员...
}
信息
  • 生成的DTO类所在的包并非实体所处的包,而是其dto子包

  • 对于Java而言,假设用户已经使用了lombok

查询DTO对象

使用findById

BookDetailView view = sqlClient.findNullable(
BookDetailView.class,
1L
);
System.out.println(view);

打印结果如下 (为方便阅读,人为进行了格式化)

BookDetailView(
id=1,
name=Learning GraphQL,
edition=1,
price=50.00,
store=BookDetailView.TargetOf_store(
id=1,
name=O'REILLY,
website=null,
version=0
),
authors=[
BookDetailView.TargetOf_authors(
id=2,
firstName=Alex,
lastName=Banks,
gender=MALE
),
BookDetailView.TargetOf_authors(
id=1,
firstName=Eve,
lastName=Procello,
gender=FEMALE
)
]
)

不难发现,虽然现在查询不再返回动态实体对象,但功能和却和对象抓取器完全一样。这什么为什么呢?

由DTO语言编译而来的DTO类型,都会包含与其形状匹配的对象抓取器,如下

BookDetailView.java
@Data
public class BookDetailView implements View<Book> {

public static final ViewMetadata<Book, BookDetailView> METADATA =
new ViewMetadata<Book, BookDetailView>(
Fetchers.BOOK_FETCHER
.name()
.edition()
.price()
.store(TargetOf_store.METADATA.getFetcher())
.authors(TargetOf_authors.METADATA.getFetcher()),
BookDetailView::new
);

@Data
public static class TargetOf_store implements View<BookStore> {

public static final ViewMetadata<BookStore, TargetOf_store> METADATA =
new ViewMetadata<BookStore, TargetOf_store>(
Fetchers.BOOK_STORE_FETCHER
.name()
.website()
.version(),
TargetOf_store::new
);

...省略其他成员...
}

@Data
public static class TargetOf_authors implements View<Author> {

public static final ViewMetadata<Author, TargetOf_authors> METADATA =
new ViewMetadata<Author, TargetOf_authors>(
Fetchers.AUTHOR_FETCHER
.firstName()
.lastName()
.gender(),
TargetOf_authors::new
);

...省略其他成员...
}

...省略其他成员...
}
提示

这就是本文开头说DTO语言本质上是对象抓取器的另外一种表达方式的原因

使用自定义查询

BookTable table = Tables.BOOK_TABLE;

List<Book> books = sqlClient
.createQuery(table)
.where(table.name().eq("GraphQL in Action"))
.orderBy(table.name())
.orderBy(table.edition().desc())
.select(
table.fetch(BookDetailView.class)
)
.execute()

这里,我们看到了,以前代码中的table.fetch(fetcher)被替换成了table.fetch(BookDetailView.class)

提示

所有底层查询API都可以用viewType替换fetcher,再次印证了本文开头所说DTO语言本质上是对象抓取器的另外一种表达方式。

关联属性特有配置

备注

Jimmer DTO分为三种

  1. Output DTO
  2. Input DTO
  3. Specification DTO

本文所讨论的DTO,用于定义对象抓取器并将其查询结果类型映射为静态对象,属于Output DTO。

因此,本节所介绍的内容,是Output DTO特有的配置。

对象抓取器/关联属性中,我们介绍了关联属性的一些特有配置。

  1. BatchSize
  2. 关联级分页
  3. 属性过滤器
  4. 引用抓取方式

对象抓取器/递归查询中,我们介绍了

  1. 限制递归深度
  2. 控制每个节点是否递归

同理,作为类型抓取器更加类型化的体现,Output DTO也同样具备这些配置

BatchSize

Book.dto
export com.yourcompany.yourproject.model.Book 
-> package com.yourcompany.yourproject.model.dto

BookDetailView {

#allScalars

!batch(2)
authors {
#allScalars
}
}

关联级分页

Book.dto
export com.yourcompany.yourproject.model.Book 
-> package com.yourcompany.yourproject.model.dto

BookDetailView {

#allScalars

!limit(10, 90) // limit: 10, offset: 90
authors {
#allScalars
}
}

Book.dto
export com.yourcompany.yourproject.model.Book 
-> package com.yourcompany.yourproject.model.dto

BookDetailView {

#allScalars

!limit(10) // limit: 10, offset: 0
authors {
#allScalars
}
}

属性过滤器

Book.dto
export com.yourcompany.yourproject.model.Book 
-> package com.yourcompany.yourproject.model.dto

BookDetailView {

#allScalars

!where(firstName ilike '%a%' or lastName ilike '%a%')
!orderBy(firstName asc, lastName asc)
authors {
#allScalars
}
}

为了限制DTO语言的复杂度,!where!orderBy故意限制了语法复杂度,以!where中的条件表达式为例

  1. 支持andor,以及通过()改变运算符优先级。
  2. 变量必须是关联实体 (本例中为Author) 中能映射到数据库列的属性,即,标量属性和关联id。

    如果属性是复合字段,可以实现用.链路引用内部属性,例如fullName.firstName

  3. 支持二元运算符=<>!=<<=>>=like、和ilike,其中<>!=等价。
  4. 支持一元云运算is nullis not null

显然,设计目的在于控制DTO语言的复杂性。如果要编写任意复杂的筛选和排序,请使用!filter,如下:

  1. 首先,新建一个类。

    • 对于Java而言,请实现org.babyfish.jimmer.sql.fetcher.FieldFilter<T>接口
    • 对于Kotlin而言,请实现org.babyfish.jimmer.sql.kt.fetcher.KFieldFilter<E>接口
    AuthorFilter.java
    package com.yourcompany.yourpackage.strategy;

    ...省略import语句...

    public class AuthorsPropFilter implements FieldFilter<AuthorTable> {

    @Override
    public void apply(FieldFilterArgs<AuthorTable> args) {
    AuthorTable table = args.getTable();
    args
    .where(
    Predicate.or(
    table.firstName().ilike("a"),
    table.lastName().ilke("b")
    )
    )
    .orderBy(
    table.firstName(),
    table.lastName()
    );
    }
    }

    可见,此类型允许我们使用完整的Jimmer DSL的能力,实现任意复杂的筛选和排序逻辑。

  2. 在DTO声明中使用上述Java/Kotlin类

    Book.dto
    export com.yourcompany.yourproject.model.Book 
    -> package com.yourcompany.yourproject.model.dto

    import com.yourcompany.yourpackage.strategy.AuthorsPropFilter

    BookDetailView {

    #allScalars

    !filter(AuthorsPropFilter)
    authors {
    #allScalars
    }
    }

引用抓取方式

Book.dto
BookDetailView {

#allScalars

!fetchType(JOIN_ALWASY)
store {
#allScalars
}
}
备注

注意:和对象抓取器一样,只能为引用关联 (非集合关联,@ManyToOne@OneToOne) 属性指定抓取方式

限制递归深度

Book.dto
export com.yourcompany.yourproject.model.Book 
-> package com.yourcompany.yourproject.model.dto

BookDetailView {

#allScalars

!depth(2)
childNodes*
}

控制每个节点是否递归

  1. 首先,新建一个类,实现org.babyfish.jimmer.sql.fetcher.RecursionStrategy<E>接口

    ChildNodesRecursionStrategy.java
    package com.yourcompany.yourpackage.strategy;

    ...省略import语句...

    public class ChildNodesRecursionStrategy implements RecursionStrategy<TreeNode> {

    @Override
    public boolean isRecursive(Args<TreeNode> args) {
    return !args.getEntity().name().equals("Clothing");
    }
    }
  2. 在DTO声明中使用上述Java/Kotlin类

    Book.dto
    export com.yourcompany.yourproject.model.Book 
    -> package com.yourcompany.yourproject.model.dto

    import com.yourcompany.yourpackage.strategy.ChildNodesRecursionStrategy

    BookDetailView {

    #allScalars

    !recursion(ChildNodesRecursionStrategy)
    childNodes*
    }