Skip to main content

Return Output DTO

Java/Kotlin Applications Use Query Results

In the previous article, we introduced that the web service does not need to define DTO types, directly returns entities, and uses @FetchBy annotations to restore all DTO type definitions in automatically generated client code.

However, what if a query result is not returned to the Web remote client, but the server itself uses it?

List<Book> books = bookRepository.findBooksByName(
"graphql",
Fetchers.BOOK_FETCHER
.name()
.edition()
);
for (Book book : books) {
System.out.println("--------");
System.out.println("Id: " + book.id());
System.out.println("Name: " + book.name());
System.out.println("Edition: " + book.edition());
System.out.println("Price:" + book.price());
}

Here, there is no web service or remote call, it is just a call within the same JVM

  • ❶ Only queries three properties of the object: id (implicit), name and edition

  • ❷ Access the unqueried price property of the book object.

    This erroneous access will cause an exception:

    • Exception type: org.babyfish.jimmer.UnloadedException

    • Exception message: The property "com.yourcompany.yourproject.model.Book.price" is unloaded

It can be seen that it is not enough to only consider automatically defining DTO types in remote client APIs. When the JVM itself directly uses query results, if sufficient compile-time security is required, defining DTO types for Java/Kotlin will be inevitable and necessary to ensure better compile-time safety.

DTO Language

The mutual conversion between entity objects and DTO objects is a boring, labor-intensive and error-prone thing that is common pain point in information management software development. Although many frameworks are trying to mitigate this problem, development efficiency has remained unable to be improved qualitatively.

In order to minimize the cost of DTO type creation, Jimmer introduces the DTO language, which supplements the Java/Kotlin type system and can quickly generate Java/Kotlin DTO type definitions at compile time.

This article only provides a quick overview without detailed introduction. For complete information, please refer to DTO Language

DTO Language Plugin

A jimmer user provide Intellij plugins for the DTO language. For details, see https://github.com/ClearPlume/jimmer-dto

Installing the DTO language plugin is not required, but you can get a better development experience after installation, so it is recommended to install.

Define DTO Files

  1. For any Java/Kotlin project that needs to use the DTO language, create a subdirectory dto under its src/main directory. That is, src/main/dto is where DTO files are stored.

  2. Create a Book.dto file under src/main/dto and enter the following code:

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

    SimpleBookView {
    id
    name
    }

    ComplexBookView {
    #allScalars(this)
    store {
    id
    name
    }
    authors {
    id
    firstName
    lastName
    }
    }
  3. Compile the project (either use gradle/maven commands on the command line or click gradle/maven build on the right side of Intellij) to generate related DTO types

View Generated DTOs

After compilation, the following two types SimpleBookView and ComplexView will be generated automatically. The code for each is as follows:

  • SimpleBookView

    SimpleBookView.java
    @GeneratedBy(
    file = "<yourproject>/src/main/dto/Book.dto"
    )
    public class SimpleBookView implements View<Book> {

    private long id;

    @NotNull
    private String name;

    public SimpleBookView(@NotNull Book base) {
    ...omitted...
    }

    @Override
    public Book toEntity() {
    ...omitted...
    }

    ...Omit getters and setters...

    ...Omit hashCode/equals/toString...

    ...Omit other members...
    }
    • ❶ Reminds users that this is code automatically generated by Jimmer

    • ❷ Output DTO based on Book entity needs to implement View<Book> interface

    • ❸ Converts entity to DTO

    • ❹ Converts DTO to entity

  • ComplexBookView

    ComplexBookView.java
    @GeneratedBy(
    file = "<yourproject>/src/main/dto/Book.dto"
    )
    public class ComplexBookView implements View<Book> {

    private long id;

    @NotNull
    private String name;

    private int edition;

    @NotNull
    private BigDecimal price;

    @Nullable
    private TargetOf_store store;

    @NotNull
    private List<TargetOf_authors> authors;

    public ComplexBookView(@NotNull Book base) {
    ...omitted...
    }

    @Override
    public Book toEntity() {
    ...omitted...
    }

    ...Omit getters and setters...

    ...Omit hashCode/equals/toString...

    ...Omit other members...

    public static class TargetOf_store implements View<BookStore> {

    private long id;

    @NotNull
    private String name;

    public TargetOf_store(@NotNull BookStore base) {
    ...omitted...
    }

    @Override
    public BookStore toEntity() {
    ...omitted...
    }

    ...Omit getters and setters...

    ...Omit hashCode/equals/toString...

    ...Omit other members...
    }

    public static class TargetOf_authors implements View<Author> {

    private long id;

    @NotNull
    private String firstName;

    @NotNull
    private String lastName;

    public TargetOf_authors(@NotNull Author base) {
    ...omitted...
    }

    @Override
    public Author toEntity() {
    ...omitted...
    }

    ...Omit getters and setters...

    ...Omit hashCode/equals/toString...

    ...Omit other members...
    }
    }
    • ❶ Reminds users that this is code automatically generated by Jimmer

    • ❷ Output DTO based on Book entity needs to implement View<Book> interface

    • ❸ Converts entity to DTO

    • ❹ Converts DTO to entity

    • ❺ DTO definition of associated object referenced by many-to-one association Book.store

    • ❻ DTO definition of associated object referenced by many-to-many association Book.authors

New BookRepository

Review Old BookRepository

In the Feature Introduction article, we wrote a BookRepository class

public class BookRepository {

@Nullable
public Book findBookById(
long id,
Fetcher<Book> fetcher
) {
...omitted...
}

public List<Book> findBooksByName(
@Nullable String name,
@Nullable Fetcher<Book> fetcher
) {
...omitted...
}

...Omit other members...
}

Each query method adds a parameter of type Fetcher<Book>. We can use it to flexibly control the format of queried objects (that is, the shape of queried data structures).

This is the recommended usage. The Repository is only responsible for filtering, sorting, paging and other operations, but does not control the format of the returned data. Instead, it exposes the control of the data format through the Fetcher<E> parameter to allow upper layer business logic to decide.

Write New BookRepository

Now, this BookRepository no longer meets our requirements. Because we no longer want to query Jimmer entities, but want to query DTO types automatically generated by the DTO language, we need to modify it.

However, we want BookRepository to still maintain the excellent quality of exposing shape control externally. The modified code is as follows.

@Component
public class BookRepository {

private final JSqlClient sqlClient;

public BookRepository(JSqlClient sqlClient) {
this.sqlClient = sqlClient;
}

@Nullable
public <V extends View<Book>> V findBookById(
long id,
Class<V> viewType ❷
) {
return sqlClient.findById(
viewType,
id
);
}

public <V extends View<Book>> List<V> findBooksByName(
@Nullable String name,
Class<V> viewType ❺
) {
BookTable table = Tables.BOOK_TABLE;
return sqlClient
.createQuery(table)
.whereIf(
name != null && !name.isEmpty(),
table.name().ilike(name)
)
.select(
table.fetch(viewType)
)
.execute();
}
}
  • ❶ ❹: <V extends View<Book>> in java or <V: View<Book>> in kotlin defines a generic parameter V representing any Output DTO type based on Book.

    For example: SimpleBookView and ComplexBookView generated above implement the View<Book> interface.

  • ❷ ❺: Use the type of any DTO based on Book as a parameter.

    The return type varies with the change of the parameter type to achieve querying any DTO type and hand over the decision of the DTO type to the upper caller.

  • ❸ ❻: Let Jimmer query data of the specified type

    tip

    The DTO type internally includes a Fetcher matching the shape. First, entity data structures matching the shape are queried through this Fetcher, and then automatically converted to DTO types.

Try New BookRepository

Take bookRepository.findById as an example

  • Query relatively simple SimpleBookView

    System.out.println(
    bookRepository.findBookById(
    1L,
    SimpleBookView.class
    )
    );

    Print output:

    SimpleBookView(
    id=1,
    name=Learning GraphQL
    )
  • Query relatively complex ComplexBookView

    System.out.println(
    bookRepository.findBookById(
    1L,
    ComplexBookView.class
    )
    );

    Print output:

    ComplexBookView(
    id=1,
    name=Learning GraphQL,
    edition=1,
    price=50.0,
    store=ComplexBookView.TargetOf_store(
    id=1,
    name=O'REILLY
    ),
    authors=[
    ComplexBookView.TargetOf_authors(
    id=1,
    firstName=Eve,
    lastName=Procello
    ),
    ComplexBookView.TargetOf_authors(
    id=2,
    firstName=Alex,
    lastName=Banks
    )
    ]
    )

Write BookController

Although the DTO language is more suitable for Java/Kotlin applications to use query results internally, you can also use them as return information for HTTP APIs without any difference from using ordinary POJOs.

BookController.java
@RestController  
public class BookController implements Fetchers {

private final BookRepository bookRepository;

public BookController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}

@Nullable
@GetMapping("/book/{id}")
public ComplexBookView findBookById(@PathVariable("id") long id) {
return bookRepository.findBookById(
id,
ComplexBookView.class
);
}

@GetMapping("/books")
public List<SimpleBookView> findBooksByName(
@RequestParam(name = "name", required = false) String name
) {
return bookRepository.findBooksByName(
name,
SimpleBookView.class
);
}
}

Document Comments

In the previous article, we mentioned that Jimmer can copy the document comments in Java/Kotlin code to the client Api, whether it is OpenApi online documentation or generated TypeScript code.

The method introduced in this article has the same functionality, but it needs to be explained that the types and properties in the DTO language support document comments like Java/Kotlin types, so the DTO language can override Java/Kotlin document comments. For example, the original entity definition is as follows

/**
* The book entity
*/
@Entity
public interface Book {

/**
* The name of book entity
*/
String name();

...Omit other members...
}

The document comments here are the original document comments.

The DTO Language also supports document comments. For example:

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

/**
* Simple book dto
*/
SimpleBookView {

/**
* The name of simple book dto
*/
name

...Omit other members...
}

...Omit other DTO definitions...
info

The document comment of the DTO Language has higher priority.

That is, the document comments in the DTO language can override the document comments of the original entity, finally it is used to generate OpenApi documentation or TypeScript code.

Flat Association ID

If the associated object only has the id property, the associated Id will be better than the associated object. For example:

  • Using associated objects will lead to a large number of objects with only id properties, making the results slightly redundant:

    {
    "id": 1,
    "name": "Learning GraphQL",
    "edition": 1,
    "price": 50.00,
    "store": {
    "id": 1
    },
    "authors": [{
    "id": 1
    }, {
    "id": 2
    }]
    }
  • Using associated Ids makes the results relatively concise:

    {
    "id": 1,
    "name": "Learning GraphQL",
    "edition": 1,
    "price": 50.00,
    "storeId": 1,
    "authorIds": [1, 2]
    }

If you choose to return DTO (instead of directly returning entities as in the previous article), you can define the following DTO code:

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

ShallowBookView {
#allScalars(this)
id(store)
id(authors) as authorIds
}

...Omit other DTO definitions...

After compilation, the following code is generated:

ShallowBookView.java
@GeneratedBy(  
file = "<yourproject>/src/main/dto/Book.dto"
)
public class ShallowBookView implements View<Book> {

private long id;

@NotNull
private String name;

private int edition;

@NotNull
private BigDecimal price;

@Nullable
private Long storeId;

@NotNull
private List<Long> authorIds;

...Omit other members...
}

Flat Associated Objects

A large part of server development teams will deal with a type of frontend development team who do not accept data structures composed of associated multiple objects, and only want to accept a huge single object. So they require all non-collection associations to be flattened. That is:

  • They do not accept structured return information:

    {
    "prop1": 1,
    "prop2": 2,
    "a": {
    "prop1": 3,
    "prop2": 4,
    "b": {
    "prop1": 5,
    "prop2": 6,
    }
    },
    "c": {
    "prop1": 7,
    "prop2": 8,
    "d": {
    "prop1": 9,
    "prop2": 10,
    }
    }
    }
  • Insist on asking for such flat data:

    {    
    "prop1": 1,
    "prop2": 2,
    "aProp1": 3,
    "aProp2": 4,
    "aBProp1": 5,
    "abProp2": 6,
    "cProp1": 7,
    "cProp2": 8,
    "cdProp1": 9,
    "cdProp2": 10
    }

In fact, such flat non-structured data is a disaster for client programs that need state management. But such frontend teams only do UI rendering without state management, so they do not realize this problem and insist on it very much.

When you can't argue but need to complete the task quickly, write DTO code as follows:

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

FlatBookView {
#allScalars(this)
flat(store) { ❶
as(^ -> store) { ❷
#allScalar(this)
}
}
}

...Omit other DTO definitions...
  • ❶ The flat function means to flatten the properties of the associated object pointed to by the Book.store one-to-many association to the current object.

  • ❷ For the properties of the associated object, the property name needs to be changed after being flattened to the current object. Prefix the old property name with store. For example, name -> storeName.

After compilation, the following code is generated:

FlatBookView.java
@GeneratedBy(  
file = "<yourproject>/src/main/dto/Book.dto"
)
public class FlatBookView implements View<Book> {

private long id;

@NotNull
private String name;

private int edition;

@NotNull
private BigDecimal price;

@Nullable
private Long storeId;

@Nullable
private String storeName;

@Nullable
private String storeWebsite;

...Omit other members...
}

Here, the flattened properties are all nullable, because the Book.store association itself allows null.