Querying DTOs
Previously we introduced using object fetchers to flexibly control the shape of queried data structures.
Now we introduce an equivalent capability: querying DTO objects.
Jimmer provides a DTO language
This language is essentially another way to express object fetchers.
Using this language, developers can quickly define various data structure shapes with an entity type as the aggregate root. The compiler will generate corresponding Java/Kotlin DTO classes for each defined shape. Each DTO type contains mutual conversion logic with the original dynamic type, and an object fetcher matching its own shape.
In some cases, after the server queries data in a certain shape, it is not meant to be returned as an HTTP response, but rather used internally to drive subsequent complex business logic. This is an ideal scenario for this approach.
Note that if the server queries data in a certain shape not for its own use, but directly as the HTTP response, it is more recommended to directly return the dynamic entity object and use the solutions in Generate Client Code to automatically generate client code with a great developer experience.
Defining DTO Shapes
This article focuses on explaining how to query static DTO types, not a systematic introduction to the DTO language itself. Please refer to Object section/DTO Mapping/DTO Language for the complete DTO language.
Assuming the fully qualified name of the Book
class is com.yourcompany.yourproject.model.Book
, you can
-
In the project where the entity is defined, create the directory
src/main/dto
-
Under
src/main/dto
, create a fileBook.dto
. -
Edit this file, use the DTO language to define various DTO shapes for the Book entity
Book.dtoexport com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
BookDetailView {
#allScalars
store {
#allScalars
}
authors {
#allScalars
}
}
SimpleBookView { ...omitted... }
...Omit definitions of other DTO shapes...
Auto-generated DTO Types
Jimmer is responsible for compiling dto files and automatically generating DTO types matching these shapes.
If Java/Kotlin source code files other than dto files are modified, running the application directly from the IDE can trigger recompilation of dto files.
However, if no Java/Kotlin files other than dto files are modified, simply clicking the run button in the IDE does not cause dto files to be recompiled unless explicitly rebuilding!
If you are using Gradle as your build tool, you can also use the third-party Gradle plugin provided by the community to solve this problem: jimmer-gradle
Taking BookDetailView
in the above code as an example, after the dto file is successfully compiled by Jimmer, the following DTO type will be automatically generated:
- Java
- Kotlin
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 View<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 View<BookStore> {
private long id;
private String name;
@Nullable
private String website;
...other members omitted...
}
public static class TargetOf_authors implements View<Author> {
private long id;
private String firstName;
private String lastName;
private Gender gender;
...other members omitted...
}
...other members omitted...
}
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")
data class BookDetailView(
val id: Long = 0,
val name: String = "",
val edition: Int = 0,
val price: BigDecimal,
val store: TargetOf_store? = null,
val authors: List<TargetOf_authors> = emptyList(),
) : View<Book> {
data class TargetOf_store(
val id: Long = 0,
val name: String = "",
val website: String? = null,
) : View<BookStore> {
...other members omitted...
}
data class TargetOf_authors(
val id: Long = 0,
public val firstName: String = "",
public val lastName: String = "",
public val gender: Gender,
) : View<Author> {
...other members omitted...
}
...other members omitted...
}
-
The generated DTO classes are in the
dto
subpackage of the entity package, not the entity package itself. -
For Java, it is assumed lombok is already in use.
Querying DTO Objects
Using findById
- Java
- Kotlin
BookDetailView view = sqlClient.findNullable(
BookDetailView.class,
1L
);
System.out.println(view);
val view = sqlClient.findNullable(
BookDetailView::class,
1L
);
println(view)
The printed result is as follows (formatted manually for readability):
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
)
]
)
It's easy to see that although dynamic entity objects are no longer returned, the functionality is exactly the same as object fetchers. Why is this?
The DTO types generated from the DTO language all contain object fetchers matching their own shapes, as follows:
- Java
- Kotlin
@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
);
...other members omitted...
}
@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
);
...other members omitted...
}
...other members omitted...
}
@Data
data class BookDetailView(
...omitted...
) : View<Book> {
...other members omitted...
companion object {
@JvmStatic
public val METADATA: ViewMetadata<Book, BookDetailView> =
ViewMetadata<Book, CompositeBookInput>(
newFetcher(Book::class).by {
name()
edition()
price()
store(TargetOf_store.METADATA.fetcher)
authors(TargetOf_authors.METADATA.fetcher)
}
) {
CompositeBookInput(it)
}
}
@Data
public static class TargetOf_store implements View<BookStore> {
...other members omitted...
companion object {
@JvmStatic
val METADATA: ViewMetadata<BookStore, TargetOf_store> =
ViewMetadata<BookStore, TargetOf_store>(
newFetcher(BookStore::class).by {
name()
website()
}
) {
TargetOf_store(it)
}
}
}
@Data
public static class TargetOf_authors implements View<Author> {
...other members omitted...
companion object {
@JvmStatic
val METADATA: ViewMetadata<Author, TargetOf_authors> =
ViewMetadata<Author, TargetOf_authors>(
newFetcher(Author::class).by {
firstName()
lastName()
gender()
}
) {
TargetOf_authors(it)
}
}
}
}
This is why this article says at the beginning that the DTO language is essentially another way to express object fetchers.
Using Custom Queries
- Java
- Kotlin
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()
val books = sqlClient
.createQuery(Book::class) {
where(table.name eq "GraphQL in Action")
orderBy(table.name)
orderBy(table.edition.desc())
select(
table.fetch(BookDetailView::class)
)
}
.execute()
Here we see that table.fetch(fetcher)
in previous code is replaced with table.fetch(BookDetailView.class)
.
All low-level query APIs can replace fetcher
with viewType
, again proving that the DTO language is essentially another way to express object fetchers, as stated at the beginning.
Association Property Specific Configurations
Jimmer DTOs are divided into three types:
- Output DTO
- Input DTO
- Specification DTO
The DTOs discussed in this article are used to define object fetchers and map their query results to static objects, which belong to the Output DTO category.
Therefore, the configurations introduced in this section are specific to Output DTOs.
In Object Fetcher/Association Properties, we introduced some specific configurations for association properties:
- BatchSize
- Association-level pagination
- Property filters
- Reference fetching methods
In Object Fetcher/Recursive Queries, we introduced:
- Limit recursion depth
- Controlling whether each node is recursive
Similarly, as a more type-oriented representation of type fetchers, Output DTOs also have these configurations.
BatchSize
export com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
BookDetailView {
#allScalars
!batch(2)
authors {
#allScalars
}
}
Association-level Pagination
export com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
BookDetailView {
#allScalars
!limit(10, 90) // limit: 10, offset: 90
authors {
#allScalars
}
}
Or
export com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
BookDetailView {
#allScalars
!limit(10) // limit: 10, offset: 0
authors {
#allScalars
}
}
Property Filters
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
}
}
To limit the complexity of the DTO language, !where
and !orderBy
intentionally restrict the syntax complexity. Taking the conditional expressions in !where
as an example:
- Supports
and
andor
, as well as changing operator precedence through()
. - Variables must be properties in the associated entity (in this example,
Author
) that can be mapped to database columns, i.e., scalar properties and association IDs.If the property is a composite field, you can use the
.
chain to reference internal properties, such asfullName.firstName
. - Supports binary operators
=
,<>
,!=
,<
,<=
,>
,>=
,like
, andilike
, where<>
and!=
are equivalent. - Supports unary operators
is null
andis not null
.
Obviously, the design purpose is to control the complexity of the DTO language. If you need to write arbitrarily complex filtering and sorting, please use !filter
as follows:
-
First, create a new class.
- For Java, implement the
org.babyfish.jimmer.sql.fetcher.FieldFilter<T>
interface - For Kotlin, implement the
org.babyfish.jimmer.sql.kt.fetcher.KFieldFilter<E>
interface
- Java
- Kotlin
AuthorFilter.javapackage com.yourcompany.yourpackage.strategy;
...omitted import statements...
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()
);
}
}AuthorFilterpackage com.yourcompany.yourpackage.strategy
...omitted import statements...
class AuthorsPropFilter : KFieldFilter<Author> {
override fun KFieldFilterDsl<Author>.applyTo() {
where(
or(
table.firstName ilike "a",
table.lastName ilike "a",
)
)
orderBy(
table.firstName,
table.lastName
)
}
}As you can see, this type allows us to use the full capability of the Jimmer DSL to implement arbitrarily complex filtering and sorting logic.
- For Java, implement the
-
Use the above Java/Kotlin class in the DTO declaration
Book.dtoexport com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
import com.yourcompany.yourpackage.strategy.AuthorsPropFilter
BookDetailView {
#allScalars
!filter(AuthorsPropFilter)
authors {
#allScalars
}
}
Reference Fetching Methods
BookDetailView {
#allScalars
!fetchType(JOIN_ALWASY)
store {
#allScalars
}
}
Note: Just like object fetchers, fetch methods can only be specified for reference associations (non-collection associations, @ManyToOne
or @OneToOne
) properties
Limit recursion depth
export com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
BookDetailView {
#allScalars
!depth(2)
childNodes*
}
Controlling Whether Each Node is Recursive
-
First, create a new class implementing the
org.babyfish.jimmer.sql.fetcher.RecursionStrategy<E>
interface- Java
- Kotlin
ChildNodesRecursionStrategy.javapackage com.yourcompany.yourpackage.strategy;
...omitted import statements...
public class ChildNodesRecursionStrategy implements RecursionStrategy<TreeNode> {
@Override
public boolean isRecursive(Args<TreeNode> args) {
return !args.getEntity().name().equals("Clothing");
}
}ChildNodesRecursionStrategy.ktpackage com.yourcompany.yourpackage.strategy
...omitted import statements...
class ChildNodesRecursionStrategy : RecursionStrategy<TreeNode> {
override fun isRecursive(args: RecursionStrategy.Args<TreeNode>): Boolean {
return args.entity.name != "Clothing"
}
} -
Use the above Java/Kotlin class in the DTO declaration
Book.dtoexport com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
import com.yourcompany.yourpackage.strategy.ChildNodesRecursionStrategy
BookDetailView {
#allScalars
!recursion(ChildNodesRecursionStrategy)
childNodes*
}