Skip to main content

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

tip

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.

caution

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

  1. In the project where the entity is defined, create the directory src/main/dto

  2. Under src/main/dto, create a file Book.dto.

  3. Edit this file, use the DTO language to define various DTO shapes for the Book entity

    Book.dto
    export 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.

caution

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:

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 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...
}
info
  • 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

BookDetailView view = sqlClient.findNullable(
BookDetailView.class,
1L
);
System.out.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:

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
);

...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...
}
tip

This is why this article says at the beginning that the DTO language is essentially another way to express object fetchers.

Using Custom Queries

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()

Here we see that table.fetch(fetcher) in previous code is replaced with table.fetch(BookDetailView.class).

tip

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

note

Jimmer DTOs are divided into three types:

  1. Output DTO
  2. Input DTO
  3. 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:

  1. BatchSize
  2. Association-level pagination
  3. Property filters
  4. Reference fetching methods

In Object Fetcher/Recursive Queries, we introduced:

  1. Limit recursion depth
  2. Controlling whether each node is recursive

Similarly, as a more type-oriented representation of type fetchers, Output DTOs also have these configurations.

BatchSize

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

BookDetailView {

#allScalars

!batch(2)
authors {
#allScalars
}
}

Association-level Pagination

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
}
}

Or

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

BookDetailView {

#allScalars

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

Property Filters

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
}
}

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:

  1. Supports and and or, as well as changing operator precedence through ().
  2. 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 as fullName.firstName.

  3. Supports binary operators =, <>, !=, <, <=, >, >=, like, and ilike, where <> and != are equivalent.
  4. Supports unary operators is null and is 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:

  1. 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
    AuthorFilter.java
    package 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()
    );
    }
    }

    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.

  2. Use the above Java/Kotlin class in the DTO declaration

    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
    }
    }

Reference Fetching Methods

Book.dto
BookDetailView {

#allScalars

!fetchType(JOIN_ALWASY)
store {
#allScalars
}
}
note

Note: Just like object fetchers, fetch methods can only be specified for reference associations (non-collection associations, @ManyToOne or @OneToOne) properties

Limit recursion depth

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

BookDetailView {

#allScalars

!depth(2)
childNodes*
}

Controlling Whether Each Node is Recursive

  1. First, create a new class implementing the org.babyfish.jimmer.sql.fetcher.RecursionStrategy<E> interface

    ChildNodesRecursionStrategy.java
    package 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");
    }
    }
  2. Use the above Java/Kotlin class in the DTO declaration

    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*
    }