Handle Null Values
Input DTO is used for data input, so it provides powerful control over nullable properties in the objects submitted by the client, and standardizes this capability.
Null-related Issues in Data input
Review: Directly Saving Entity Objects
One of the most important features of Jimmer entities is the strict distinction between unknown data (not specifying object properties) and no data (setting object properties to null).
Let's temporarily set aside the concept of Input DTO and review the differences when directly saving data using Jimmer entities.
-
Setting nullable property to null
- Java
- Kotlin
Book book = BookDraft.$.produce(draft -> {
draft.setId(12L);
draft.setName("TURING");
draft.setStoreId(null);
});
sqlClient.update(book);val book = Book {
id = 12L
name = "TURING"
storeId = null
}
sqlClient.update(book);The following SQL is generated:
update BOOK
set
NAME = ?, /* TURING */
// highlight-next-line
STORE_ID = ? /* <null: long> */
where
ID = ? /* 12 */`As you can see, by explicitly setting the object's property to null and executing the save command with an update operation, the value in the database will be updated to null.
-
Not specifying nullable property at all
- Java
- Kotlin
Book book = BookDraft.$.produce(draft -> {
draft.setId(12L);
draft.setName("TURING");
// `storeId` is not specified
});
sqlClient.update(book);val book = Book {
id = 12L
name = "TURING"
// `storeId` is not specified
}
sqlClient.update(book);The following SQL is generated:
update BOOK
set
NAME = ? /* TURING */
// highlight-next-line
/* `STORE_ID` is not updated */
where
ID = ? /* 12 */`As you can see, by not setting the object's property and executing the save command with an update operation, the value in the database will not be updated.
This distinction is very important.
In the subsequent content of this article, we will no longer discuss what SQL statements the ORM generated because we only need to focus on what the entity object obtained from the conversion of the Input DTO is.
Issues Faced by Input DTO
Now, let's define an Input DTO:
input BookUpdateInput {
id!
name
id(store)
}
For more details on the DTO language, please refer to the relevant chapter. Here, we focus on the Java/Kotlin code automatically generated by the Jimmer pre-compiler based on this DTO code.
The generated code is as follows:
- Java
- Kotlin
@GeneratedBy(file = "<your_project>/src/main/dto/Book.dto")
public class BookUpdateInput implements Input<Book> {
private long id;
private String name;
@Nullable
private Long storeId;
@Override
public Book toEntity() {
...omitted...
}
...other members omitted...
}
@GeneratedBy(file = "<your_project>/src/main/dto/Book.dto")
data class BookUpdateInput(
val id: Long,
val name: String,
val storeId: Long? = null
) {
override fun toEntity(): Book = ...omitted...
...other members omitted...
}
In the original entity, the associated property Book.store
is nullable. The DTO code does not change this here, so in the generated classes, the field storeId
is also nullable.
If the storeId
property of the BookUpdateInput
object uploaded by the user is null, is the user's intention to update the corresponding foreign key STORE_ID
in the database to null, or not to update it at all?
In fact, both of these intentions are common. For a long time, developers have been very casual about the conventions for these two behaviors, and even if the API documentation mentions such details, the format is also very casual. This has led to difficulties in communication and understanding and has caused lasting damage to the industry.
Input DTO provides a standardized definition for this issue, aiming to handle different intentions in a standardized manner.
4 Ways to Handle Nullable Properties
To solve the problem raised above, the DTO language specifies that if a DTO property satisfies both of the following conditions:
-
It is defined in an input type
-
It allows null values
Then, a modifier representing the null handling mode can be added to the DTO property: it can be fixed
, static
, dynamic
, or fuzzy
.
For convenience in the subsequent discussion, let's assume that the following Web Controller exists:
- Java
- Kotlin
@RestController
public class BookController {
@PutMapping("/book")
public void update(
@RequestBody BookUpdateInput input
) {
Book book = input.toEntity();
System.out.println(book);
...subsequent code omitted...
}
...other members omitted...
}
@RestController
class BookController {
@PutMapping("/book")
fun update(
@RequestBody input: BookUpdateInput
) {
val book = input.toEntity()
println(book)
...subsequent code omitted...
}
...other members omitted...
}
Here, we convert the Input DTO object uploaded by the user into a Jimmer entity and print it out.
We only need to focus on the print result, as mentioned earlier, we only need to focus on what the entity object obtained from the conversion of the Input DTO is, and we do not need to discuss what SQL statements the ORM will generate.
Therefore, the subsequent code is not important and is omitted.
1. fixed
DTO code example:
input BookUpdateInput {
id!
name
fixed id(store)
}
This mode can also be called the super static mode.
-
It does not allow the user to submit an
Input DTO
without specifying certain properties. Even if they want a property to be null, they need to explicitly specify it. -
If the property of the Input DTO is null, the corresponding property of the obtained Jimmer entity object will also be set to null, effectively modifying the corresponding field in the database to null.
Two ways for the client to submit data:
-
Submit an Input DTO with the
storeId
property set to nullcurl -X 'PUT' \
'http://localhost:8080/book' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"id": 12,
"name": "TURING",
"storeId": null
}'The print result (the final Jimmer entity object obtained) is as follows:
{
"id":12,
"name":"TURING",
"store":null
}That is, subsequent operations will update the corresponding field in the database to null.
-
Submit an Input DTO without the
storeId
propertycurl -X 'PUT' \
'http://localhost:8080/book' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"id": 12,
"name": "TURING"
}'This request will be rejected, HTTP error code 400, parameter error. If you check the Java log, you will see the following error:
Resolved [org.springframework.http.converter.HttpMessageNotReadableException:
JSON parse error: Cannot construct instance of
`org.doc.j.model.dto.BookUpdateInput$Builder`,
problem: An object whose type is "org.doc.j.model.dto.BookUpdateInput"
cannot be deserialized by Jackson.
The current type is fixed input DTO so that
all JSON properties must be specified explicitly,
however, the property "storeId" is not specified by JSON explicitly.
Please either explicitly specify the property as null in the JSON,
or specify the current input properties as static, dynamic or fuzzy
in the DTO language]
If developer use the Automatic TypeScript Generation feature in Jimmer, the generated typeScript code will require web developers to provide the storeId
property for the BookUpdateInput
object, otherwise client code cannot be compiled successfully.
2. static
DTO code example:
input BookUpdateInput {
id!
name
static id(store)
}
-
When submitting an
Input DTO
, the user can either set thestoreId
property to null or not specify it at all. -
Regardless of the user's choice, the behavior remains the same: the corresponding property of the obtained Jimmer entity object will definitely be set to null, effectively modifying the corresponding field in the database to null.
Two ways for the client to submit data:
-
Submit an Input DTO with the
storeId
property set to nullcurl -X 'PUT' \
'http://localhost:8080/book' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"id": 12,
"name": "TURING",
"storeId": null
}'The print result (the final Jimmer entity object obtained) is as follows:
{
"id":12,
"name":"TURING",
"store":null
}That is, subsequent operations will update the corresponding field in the database to null.
-
Submit an Input DTO without the
storeId
propertycurl -X 'PUT' \
'http://localhost:8080/book' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"id": 12,
"name": "TURING"
}'The print result (the final Jimmer entity object obtained) is as follows:
{
"id":12,
"name":"TURING",
"store":null
}That is, subsequent operations will update the corresponding field in the database to null.
The effects of the two operations are the same, and the final effect is only affected by the DTO shape, regardless of whether the user specified the DTO property or not.
3. dynamic
DTO code example:
input BookUpdateInput {
id!
name
dynamic id(store)
}
-
If the user chooses to set the
storeId
property of the DTO to null, then thestoreId
property of the final obtained Jimmer entity object will also be null, effectively modifying the corresponding field in the database to null. -
If the user does not set the
storeId
property of the DTO at all, then thestoreId
property of the final obtained Jimmer entity object will also not be set, so the corresponding field in the database will not be updated.
Two ways for the client to submit data:
-
Submit an Input DTO with the
storeId
property set to nullcurl -X 'PUT' \
'http://localhost:8080/book' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"id": 12,
"name": "TURING",
"storeId": null
}'The print result (the final Jimmer entity object obtained) is as follows:
{
"id":12,
"name":"TURING",
"store":null
}That is, subsequent operations will update the corresponding field in the database to null.
-
Submit an Input DTO without the
storeId
propertycurl -X 'PUT' \
'http://localhost:8080/book' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"id": 12,
"name": "TURING"
}'The print result (the final Jimmer entity object obtained) is as follows:
{
"id":12,
"name":"TURING",
// There is no storeId property here
}That is, subsequent operations will not update the corresponding field in the database.
The two ways of submitting data correspond to two completely different behaviors, suitable for professional client teams to have flexible control over the service behavior.
4. fuzzy
This mode sacrifices functionality for conservativeness and safety, and is the only mode with incomplete functionality.
DTO code example:
input BookUpdateInput {
id!
name
fuzzy id(store)
}
-
If the user sets the
storeId
property of the DTO object to a non-null value, then thestoreId
property of the final obtained Jimmer entity object will be set accordingly, effectively modifying the corresponding field in the database to the specified value. -
Otherwise (whether the
storeId
property of the DTO object is set to null or not specified at all), thestoreId
property of the final obtained Jimmer entity object will not be set, so the corresponding field in the database will not be updated.
Three ways for the client to submit data:
-
Submit an Input DTO with the
storeId
property set to nullcurl -X 'PUT' \
'http://localhost:8080/book' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"id": 12,
"name": "TURING",
"storeId": null
}'The print result (the final Jimmer entity object obtained) is as follows:
{
"id":12,
"name":"TURING",
// There is no storeId property here
}That is, subsequent operations will not update the corresponding field in the database.
-
Submit an Input DTO without the
storeId
propertycurl -X 'PUT' \
'http://localhost:8080/book' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"id": 12,
"name": "TURING"
}'The print result (the final Jimmer entity object obtained) is as follows:
{
"id":12,
"name":"TURING",
// There is no storeId property here
}That is, subsequent operations will not update the corresponding field in the database.
-
Submit an Input DTO with the
storeId
property set to a non-null valueThe previous two ways cannot modify the corresponding field in the database unless a non-null value is specified, as follows:
curl -X 'PUT' \
'http://localhost:8080/book' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"id": 12,
"name": "TURING",
"storeId": 2
}'The print result (the final Jimmer entity object obtained) is as follows:
{
"id":12,
"name":"TURING",
"store":3
}
This mode sacrifices the ability to modify the corresponding field in the database to null in exchange for absolute conservativeness and safety. It is particularly suitable for client teams with less experience.
Higher-Level Configurations
In the examples above, the keywords fixed
, static
, dynamic
, and fuzzy
were used to modify the nullable properties of the Input DTO.
Field-level control is the most fine-grained. However, if there are many nullable properties in the Input DTO, configuring them one by one may be cumbersome.
Jimmer provides configuration methods with a broader scope of influence:
-
Input type level
dynamic input XxxInput {
fixed nullableProp1
static nullableProp2
nullableProp3
fuzzy nullableProp4
nullableProp5
}Here, the null handling mode is not declared for
nullableProp3
andnullableProp5
, and they will share the configuration at the input type level (in this case,dynamic
). -
Precompiler level
If no configuration is found at the input type level, refer to the global configuration parameter
jimmer.dto.defaultNullableInputModifier
of the precompiler (for Java, it is APT; for Kotlin, it is KSP).- Java(Maven)
- Java(Gradle)
- Kotlin(Gradle.kts)
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<compilerArgs>
<arg>-Ajimmer.dto.defaultNullableInputModifier=fixed</arg>
</compilerArgs>
</configuration>
</plugin>tasks.withType<JavaCompile>().configureEach {
options.compilerArgs.add("-Ajimmer.dto.defaultNullableInputModifier=fixed")
}ksp {
arg("jimmer.dto.defaultNullableInputModifier", "fixed")
} -
Final default mode
If no configuration is provided at the precompiler level either, the final default is
static
.
Configurations at different levels may conflict, and the priorities among them are:
Input property level config > Input type level config > Precompiler global config > Final default static
Caveats
For the fixed
and dynamic
modes, Jimmer requires the server-side to use Jackson for deserialization.
Therefore, if you plan to use the fixed
or dynamic
mode, please:
-
Add
@RequestBody
If you carefully look at the examples in this article, you will notice that
@RequestBody
was used there. -
Do not replace the Jackson Message Converter enabled by default in Spring Boot.
In fact, not only the Input DTOs using the
fixed
ordynamic
mode discussed in this article have this requirement; if the user needs to use the serialization/deserialization of Jimmer entities themselves, Jackson is also required.Jackson is carefully designed to strike the perfect balance between functionality and performance. Therefore, Jimmer regards Jackson as an essential infrastructure.