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.EnableImplicitApi
to 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.Api
to decorate allRestController
classes that need to be exported and theirHTTP Mapping
methods.
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
Book
object returned by thefindBookById
method is defined by the static variableCOMPLEX_BOOK
-
❷ The internal implementation of the
findBookById
method needs to be consistent with the external declaration at ❶, querying theBook
object in the shape ofCOMPLEX_BOOK
-
❸ Declare that the exact shape of each
Book
object in theList
returned by thefindBooksByName
method is defined by the static variableSIMPLE_BOOK
-
❹ The internal implementation of the
findBooksByName
method needs to be consistent with the external declaration at ❸, querying theBook
object in the shape ofSIMPLE_BOOK
-
❺ The definition of the
SIMPLE_BOOK
shape 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_BOOK
shape 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
/books
to 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
storeId
property is not a brand new property. It is just a view of thestore
property, getting theid
property of the associated object represented by thestore
property (or null).storeId
andstore
share the same data. -
The
authorIds
property is not a brand new property either. It is just a view of theauthors
property, getting a list ofid
properties of all associated objects represented by theauthors
property.authorIds
andauthors
share 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...
}
}