Directly Return Entities
Enable Web API Analysis
At present, this part of the content supports Spring, and more web frameworks will be supported in the future.
To export client code, you first need to enable Web API analysis capabilities. Developers have two options
-
Use
@org.babyfish.jimmer.client.EnableImplicitApito decorate any class in the project where the RestController belongs. For Spring Boot applications, the Application class is a good choice. -
Use
@org.babyfish.jimmer.client.Apito decorate allRestControllerclasses that need to be exported and theirHTTP Mappingmethods.
The first method is relatively simple, so applying the @EnableImplicitApi annotation to the Spring Boot Application class is enough. Since the code is too simple, there is no need to demonstrate.
Write RestController
As an example, there is no complex business logic. We ignore the Service layer and write BookController directly based on the BookRepository in the previous article, as follows:
- Java
- Kotlin
@RestController
public class BookController implements Fetchers {
private final BookRepository bookRepository;
public BookController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@Nullable
@GetMapping("/book/{id}")
public
@FetchBy("COMPLEX_BOOK") Book ❶
findBookById(@PathVariable("id") long id) {
return bookRepository.findBookById(
id,
COMPLEX_BOOK ❷
);
}
@GetMapping("/books")
public List<
@FetchBy("SIMPLE_BOOK") Book ❸
> findBooksByName(
@RequestParam(name = "name", required = false) String name
) {
return bookRepository.findBooksByName(
name,
SIMPLE_BOOK ❹
);
}
/**
* Simple Book DTO which can only access `id` and `name` of `Book` itself
*/
private static final Fetcher<Book> SIMPLE_BOOK = ❺
BOOK_FETCHER
.name();
/**
* Complex Book DTO which can access not only properties of `Book` itself,
* but also associated `BookStore` and `Author` objects with names
*/
private static final Fetcher<Book> COMPLEX_BOOK = ❻
BOOK_FETCHER
.allScalarFields()
.store(
BOOK_STORE_FETCHER.name()
)
.authors(
AUTHOR_FETCHER
.firstName()
.lastName()
);
}
@RestController
class BookController(
private val bookRepository: BookRepository
) {
@GetMapping("/book/{id}")
fun findBookById(
@PathVariable id: Long
): @FetchBy("COMPLEX_BOOK") Book = ❶
bookRepository.findBookById(
id,
COMPLEX_BOOK ❷
)
@GetMapping("/books")
fun findBooksByName(
@RequestParam(required = false) name: String
): List<
@FetchBy("SIMPLE_BOOK") Book ❸
> =
bookRepository.findBooksByName(
name,
SIMPLE_BOOK ❹
)
companion object {
/**
* Simple Book DTO which can only access `id` and `name` of `Book` itself
*/
val SIMPLE_BOOK = ❺
newFetcher(Book::class).by {
name()
}
/**
* Complex Book DTO which can access not only properties of `Book` itself,
* but also associated `BookStore` and `Author` objects with names
*/
private val COMPLEX_BOOK = ❻
newFetcher(Book::class).by {
allScalarFields()
store {
name()
}
authors {
firstName()
lastName()
}
}
}
}
The focus is on the 6 numbered items:
-
❶ Declare that the exact shape of the
Bookobject returned by thefindBookByIdmethod is defined by the static variableCOMPLEX_BOOK -
❷ The internal implementation of the
findBookByIdmethod needs to be consistent with the external declaration at ❶, querying theBookobject in the shape ofCOMPLEX_BOOK -
❸ Declare that the exact shape of each
Bookobject in theListreturned by thefindBooksByNamemethod is defined by the static variableSIMPLE_BOOK -
❹ The internal implementation of the
findBooksByNamemethod needs to be consistent with the external declaration at ❸, querying theBookobject in the shape ofSIMPLE_BOOK -
❺ The definition of the
SIMPLE_BOOKshape is used both at ❸ as part of the external API declaration and at ❹ to control the shape of the returned data structure -
❻ The definition of the
COMPLEX_BOOKshape is used both at ❶ as part of the external API declaration and at ❷ to control the shape of the returned data structure
View API Documentation
To recognize the Jimmer-specific annotation @FetchBy, Jimmer gives OpenAPI/Swagger a set of highly distinctive implementations.
Without using any other frameworks in the JVM ecosystem for automatically generating OpenAPI/Swagger, just modify application.yml (or application.properties) as follows:
jimmer:
...Omit other configurations...
client:
openapi:
path: /openapi.yml
ui-path: /openapi.html
properties:
info:
title: My Web Service
description: |
Restore the DTO explosion that was
eliminated by server-side developers
version: 1.0
Start the web project and access http://localhost:8080/openapi.html with a browser to see:

-
Expand
/booksto see that each element in the returned collection is a relatively simple DTO object -
Expand
/books/{id}to see that the return type is a relatively complex DTO type
Generate TypeScript
Modify application.yml (or application.properties) to add support for TypeScript:
jimmer:
...Omit other configurations...
client:
openapi:
...Omit openapi related configurations...
ts:
path: /ts.zip
Start the web project, download http://localhost:8080/ts.zip, unzip it, and you can see the TypeScript client code defines BookController as follows:
import type {Executor} from '../';
import type {BookDto} from '../model/dto/';
export class BookController {
constructor(private executor: Executor) {}
async findBookById(options: BookControllerOptions['findBookById']): Promise<
BookDto['BookController/COMPLEX_BOOK']
> {
...Omit specific logic...
}
async findBooksByName(options: BookControllerOptions['findBooksByName']): Promise<
ReadonlyArray<
BookDto['BookController/SIMPLE_BOOK']
>
> {
...Omit specific logic...
}
}
export type BookControllerOptions = {
'findBookById': {
readonly id: number
},
'findBooksByName': {
readonly name?: string | undefined
}
}
Where BookDto['BookController/COMPLEX_BOOK'] and BookDto['BookController/SIMPLE_BOOK'] are the restored DTO types in the TypeScript client code generated by Jimmer. You can open the model/dto/BookDto.ts file to view their definitions as follows:
export type BookDto = {
/**
* Complex Book DTO which can access not only properties of `Book` itself,
* but also associated `BookStore` and `Author` objects with names
*/
'BookController/COMPLEX_BOOK': {
readonly id: number;
readonly name: string;
readonly edition: number;
readonly price: number;
readonly store?: {
readonly id: number;
readonly name: string;
} | null | undefined;
readonly authors: ReadonlyArray<{
readonly id: number;
readonly firstName: string;
readonly lastName: string;
}>;
}
/**
* Simple Book DTO which can only access `id` and `name` of `Book` itself
*/
'BookController/SIMPLE_BOOK': {
readonly id: number;
readonly name: string;
}
}
Document Comments
Through the above demonstration, we see that the server side does not need to define any Java/Kotlin types related to DTOs, while the client sees that each specific business API automatically defines precise DTO return types. In this way, both the server side and the client side get the programming models they expect.
This article focuses on demonstrating this powerful feature without discussing how to add textual descriptions to various parts of the API (e.g. types, API methods, API parameters, object properties).
Jimmer provides the simplest solution to these issues. Java/Kotlin developers only need to write the most basic documentation comments, and all documentation comments are automatically copied to the client API.
This feature is simple. Readers can experiment on their own without elaboration here.
Flat Associated IDs
If the associated object has only the id property, the associated id is better than the associated object. For example:
-
Using the associated object will lead to a large number of objects with only the id property, making the results slightly redundant:
{
"id" : 1,
"name" : "Learning GraphQL",
"edition" : 1,
"price" : 50.00,
"store" : {
"id" : 1
},
"authors" : [{
"id" : 1
}, {
"id" : 2
}]
} -
Using the associated id makes the results relatively concise:
{
"id" : 1,
"name" : "Learning GraphQL",
"edition" : 1,
"price" : 50.00,
"storeId" : 1,
"authorIds" : [1, 2]
}
If you choose to directly return entities (rather than the DTO returns in the next article), and want to use associated ids, you need to add @IdView properties to the entities first:
- Java
- Kotlin
@Entity
public interface Book {
@Nullable
@ManyToOne
BookStore store();
@ManyToMany
List<Author> authors();
@Nullable
@IdView
Long storeId();
@IdView("authors")
List<Long> authorIds();
...Omit other members...
}
@Entity
interface Book {
@ManyToOne
val store: BookStore?
@ManyToMany
val authors: List<Author>
@IdView
val storeId: Long?
@IdView("authors")
val authorIds: List<Long>
...Omit other members...
}
In the above example:
-
The
storeIdproperty is not a brand new property. It is just a view of thestoreproperty, getting theidproperty of the associated object represented by thestoreproperty (or null).storeIdandstoreshare the same data. -
The
authorIdsproperty is not a brand new property either. It is just a view of theauthorsproperty, getting a list ofidproperties of all associated objects represented by theauthorsproperty.authorIdsandauthorsshare the same data.
Now, write the REST Controller as follows:
- Java
- Kotlin
@RestController
public class BookController implements Fetchers {
private final BookRepository bookRepository;
public BookController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@Nullable
@GetMapping("/book")
public @FetchBy("SHALLOW_BOOK") Book findBookById(
@PathVariable("id") long id
) {
return bookRepository.findBookById(id, SHALLOW_BOOK);
}
/**
* Shallow Book DTO which can access
* 1. All scalar properties of `Book` itself
* 2. All associated ids, not associated objects.
*/
private static final Fetcher<Book> SHALLOW_BOOK =
BOOK_FETCHER
.allScalarFields()
.storeId()
.authorIds();
...Omit other members...
}
@RestController
class BookController(
private val bookRepository: BookRepository
) {
@GetMapping("/book/{id}")
fun findBookById(
@PathVariable id: Long
): @FetchBy("SHALLOW_BOOK") Book =
bookRepository.findBookById(id, SHALLOW_BOOK)
...Omit other members...
companion object {
/**
* Shallow Book DTO which can access
* 1. All scalar properties of `Book` itself
* 2. All associated ids, not associated objects.
*/
val SHALLOW_BOOK =
newFetcher(Book::class).by {
allScalarFields()
storeId()
authorIds()
}
...Omit other shape definitions...
}
}