Query DTO
Jimmer provides DTO language.
Essentially, this language is another way to express object fetchers.
Using this language, developers can quickly define multiple data structure shapes with an entity type as the aggregate root. The compiler will generate the corresponding Java/Kotlin DTO classes for each shape. Each DTO type contains the mutual conversion logic between itself and the original dynamic type, as well as an object fetcher that matches its own shape.
In some cases, after the server queries data of a certain shape, it is not used as the return of an HTTP request, but is used by itself to drive subsequent complex business logic. This is an ideal scenario for using this approach.
Note that if the data of a certain shape queried by the server is not for its own use, but to be directly returned as the HTTP request return value, it is more recommended to directly return the dynamic entity object and use the scheme in Generate Client Code to automatically generate client code with high development experience.
Define DTO shape
This article focuses on how to query static DTO types, not a systematic introduction to the DTO language. Please refer to Object/DTO Conversion/DTO Language for a complete DTO language.
Assume 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 a directory
src/main/dto
-
Under
src/main/dto
, create subdirectoriescom/yourcompany/yourproject/model
according to the package path where the entity is located -
In the directory created in the previous step, create the file
Book.dto
. The file name must be the same as the entity class name, and the extension must bedto
-
Edit this file and use the DTO language to define various DTO shapes of the Book entity
Book.dtoBookDetailView {
#allScalars
store {
#allScalars
}
authors {
#allScalars
}
}
SimpleBookView { ...Omitted... }
...Omit other DTO shape definitions...
Automatically generate DTO types
Jimmer is responsible for compiling dto files and automatically generating DTO types that conform to these shapes.
If files other than dto files are modified in addition to dto files, directly clicking the run button in the IDE may cause the dto file to be recompiled.
However, if no other Java/Kotlin files are modified except for the dto file, simply clicking the run button in the IDE will not cause the dto file 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
Take BookDetailView
in the above code as an example. After this 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 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;
...Omitted other members...
}
public static class TargetOf_authors implements Input<Author> {
private long id;
private String firstName;
private String lastName;
private Gender gender;
...Omitted other members...
}
...Omitted other members...
}
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(),
) : Input<Book> {
data class TargetOf_store(
val id: Long = 0,
val name: String = "",
val website: String? = null,
) : Input<BookStore> {
...Omitted other members...
}
data class TargetOf_authors(
val id: Long = 0,
public val firstName: String = "",
public val lastName: String = "",
public val gender: Gender,
) : Input<Author> {
...Omitted other members...
}
...Omitted other members...
}
-
The generated DTO classes are not in the package where the entity is located, but in its
dto
subpackage. -
For Java, it is assumed that lombok has been used.
Query DTO types
There are several ways to query DTO types:
-
Use inherited methods from
JRepository/KRepository
-
Define abstract methods in custom Repository
-
Define default methods in custom Repository (Which is actually the method of the underlying API to query DTO types)
Use inherited methods from Repository
- Java
- Kotlin
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(MyApp.class, args);
BookRepository bookRepository = ctx.getBean(BookRepository.class);
Book view = bookRepository
.viwer(BookDetailView.class)
.findNullable(1L);
System.out.println(view);
}
fun main(args: Array<String>) {
val ctx = runApplication<MyApp>(*args)
val bookRepository = ctx.getBean(BookRepository.class.java)
val view = bookRepository
.viwer(BookDetailView.class)
.findNullable(1L)
println(view)
}
Where viewer(BookDetailView.class)
indicates that the following methods are used to query DTO objects instead of dynamic entity objects.
The print 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 is not difficult to find that although the query no longer returns dynamic entity objects, the function is exactly the same as the object fetcher. Why is this?
In fact, an object fetcher is automatically generated inside the BookDetailView
class. Jimmer uses it to query dynamic entity objects matching the shape from the database and automatically converts them to DTO objects.
Related details have been discussed in detail in Object Fetcher/DTO Query. This article will not repeat.
This is why this article says at the beginning that the DTO language is essentially another way to express object fetchers.
Define abstract methods in custom Repository
There are two implementation plans:
-
Intuitive but not recommended
- Java
- Kotlin
BookRepository.java// Intuitive but not recommended
public interface BookRepository extends JRepository<Book, Long> {
List<BookDetailView> findByNameLikeIgnoreCase(
@Nullable String name
);
}BookRepository.kt// Intuitive but not recommended
interface BookRepository : KRepository<Book, Long> {
fun findByNameLikeIgnoreCase(
name: String? = null
) : List<BookDetailView>
}This approach is very simple, the abstract method no longer returns dynamic entities representing arbitrary data, just return the DTO type with fixed shape. Very easy to understand.
cautionHowever, this approach still has drawbacks and is not recommended.
-
Recommended approach
The above code is simple and intuitive, but violates one of the important values that Jimmer has been trying to pursue:
tipDo not solidify the shape of the data structure to be queried in the data layer, but let the upper layer business decide.
- Java
- Kotlin
BookRepository.javapublic interface BookRepository extends JRepository<Book, Long> {
<V extends View<Book>> List<V> findByNameLikeIgnoreCase(
@Nullable String name,
Class<V> viewType
);
}BookRepository.ktinterface BookRepository : KRepository<Book, Long> {
fun <V: View<Book>> findByNameLikeIgnoreCase(
name: String? = null
viewType: KClass<V>
) : List<V>
}You can see:
-
First, define a method-level generic parameter
V
, which must extendorg.babyfish.jimmer.View<Book>
(this is very important, otherwise Jimmer will remind developers to do so via exceptions). -
Then, use
V
to declare the parameterviewType
of typeClass<V>
orKClass<V>
, and hand over the decision-making power of the data structure shape to the caller. -
Finally, return results carrying the
V
type, such asV
,List<V>
orPage<V>
.
In this way, we can use it to query data structures of various shapes. Take Java as an example:
-
bookRepository.findByNameLikeIgnoreCase(null, BookDetailView.class)
-
bookRepository.findByNameLikeIgnoreCase(null, SimpleBookView.class)
-
bookRepository.findByNameLikeIgnoreCase(null, DefaultBookView.class)
Let's recall how we used object fetchers directly to achieve the same functionality before introducing the DTO type:
- Java
- Kotlin
BookRepository.javapublic interface BookRepository extends JRepository<Book, Long> {
List<Book> findByNameLikeIgnoreCase(
@Nullable String name,
fetcher<Book> fetcher
);
}BookRepository.ktinterface BookRepository : KRepository<Book, Long> {
fun findByNameLikeIgnoreCase(
name: String? = null
viewType: Fetcher<Book>
) : List<Book>
}tipComparing the code using DTO with the code using object fetcher directly, it is not difficult to find that the two are doing the same thing in different ways.
This confirms again what is said at the beginning of this article that the DTO language is essentially another way to express object fetchers.
Define default methods in custom Repository
- Java
- Kotlin
public interface BookRepository extends JRepository<Book, Long> {
BookTable table = Tables.BOOK_TABLE;
default <V extends View<Book>> List<V> find(
@Nullable String name,
@Nullable String storeName,
@Nullable String authorName,
Class<V> viewType
) {
return sql()
.createQuery(table)
whereIf(
StringUtils.hasText(name),
table.name().ilike(name)
)
.whereIf(
StringUtils.hasText(storeName),
table.store().name().ilike(storeName)
)
.whereIf(
StringUtils.hasText(authorName),
table.id().in(
sql()
.createSubQuery(author)
.where(
Predicate.or(
author.firstName().ilike(authorName),
author.lastName().ilike(authorName)
)
)
.select(
author.books().id()
)
)
)
.orderBy(table.name())
.orderBy(table.edition().desc())
.select(table.fetch(viewType))
.execute();
}
}
interface BookRepository : KRepository<Book, Long> {
fun <V: View<Book>> findByNameLikeIgnoreCase(
name: String? = null
viewType: KClass<V>
) : List<V> =
sql
.createQuery(Book::class) {
name?.takeIf { it.isNotEmpty() }?.let {
where(table.name ilike it)
}
storeName?.takeIf { it.isNotEmpty() }?.let {
table.store.name ilike it
}
authorName?.takeIf { it.isNotEmpty() }?.let {
where(
table.id valueIn subQuery(Author::class) {
where(
or(
table.firstName ilike it,
table.lastName ilike it
)
)
select(table.books.id)
}
)
}
orderBy(table.name)
orderBy(table.edition.desc())
select(table.fetch(viewType))
}
.execute()
}
Here we see table.fetch(fetcher)
is replaced with table.fetch(viewType)
.
In fact, viewType
can also replace fetcher
in other underlying query APIs. For example, sqlClient.findById(fetcher, 1L)
can be replaced with sqlClient.find(viewType, 1L)
.
All underlying query APIs can replace fetcher
with viewType
, which confirms again that the DTO language is essentially another way to express object fetchers, as stated at the beginning of this article.