Problem
In this article we discuss:
-
The problem of using dynamic entities as method parameters
-
Solutions
-
Comparison with GraphQLInput
Problem of Dynamic Entities Parameters
Up to this point, we have systematically explained all the capabilities of save command.
Now we know that no matter what shape the data structure to be saved is, we can persist it to the database in one line of code calling save command, with all internal details hidden. This is a very convenient low-level capability.
However, how should the upper layer APIs of business systems be designed? Should we directly accept dynamic objects via @RequestBody
?
- Java
- Kotlin
@PutMapping("/book")
public void saveBook(
@RequestBody Book book
) {
bookRepository.save(book);
}
@PutMapping("/book")
fun saveBook(
@RequestBody book: Book
) {
bookRepository.save(book)
}
This code can work, allowing HTTP clients to submit data structures of arbitrary shapes. This seems to be an extremely powerful capability.
However, this approach has two problems:
-
Security issue
-
API ambiguity
Security Issue
The client is granted too much power, being able to submit very deep and wide tree-shaped data structures that far exceed its allowed scope of modification under current security policies. This is a huge security vulnerability.
For example, the client can totally submit data like:
{
"name": "SQL in Motion",
"edition": 1,
"price": 41.99,
"store":{
"name": "MANNING",
"location": {
"city": "Vancouver",
"country": "Canada",
...
}
},
"authors":[
{
"firstName": "Ben",
"lastName": "Brumm",
"job":{
"company": {
"name": "IBM"
},
"title": "Senior HR Manager",
...
}
...
}
]
}
Assume your intention in providing this HTTP API is that only the BOOK
, BOOK_STORE
, AUTHOR
and BOOK_AUTHOR_MAPPING
tables can be affected.
However, now the scope of data submitted by the client is too large, with far greater destructive power than you anticipated. In the above example, there are at least 4 unexpected destructions:
-
<root>.store.location.city
is modified fromNew York
toVancouver
-
<root>.store.location.country
is modified fromUSA
toCanada
-
<root>.authors[0].job.company
is modified fromNAB
toIBM
-
<root>.authors[0].job.title
is modified fromSenior Business Analyst
toSenior HR Manager
Using @RequestBody
to directly accept dynamic objects as input parameters in external APIs is very dangerous and will lead to serious security issues.
Therefore, save commands must be sealed internally as a low-level capability, and their raw capabilities must absolutely not be directly exposed through HTTP APIs.
API Ambiguity
Using dynamic objects as parameters leads to ambiguity in the API. Client developers do not know which fields are decided automatically by the business system and which must be specified by themselves, so they do not know how to invoke the API properly.
This issue is especially obvious for insert operations. Unlike update operations which can modify just a few properties, insert operations often need to specify many properties, otherwise exceptions will occur. Client developers are unclear about exactly which properties must be specified to avoid errors.
Solutions
To resolve this issue, Jimmer provides three solutions:
-
caution
This is a very crude solution, only for learning or very simple projects.
This solution only handles data persistence operations on a single table, and only resolves the security issue rather than the API ambiguity issue.
It is also the only solution that does not require defining Input DTOs.
-
Auto-Generated Input DTOs via DTO Language
This solution quickly auto-generates Input DTOs with very little cost.
It is a complete and extremely convenient solution, so is the recommended approach. Its huge convenience advantage will be introduced later.
infoAll built-in examples use this solution.
-
Manual Input DTOs via MapStruct
This solution requires developers to manually create Input DTOs and handle conversion to dynamic objects.
It is absolutely flexible, but requires a lot of work from developers.
Comparison with GraphQLInput
The above introduces three solutions. Among them, although the latter two solutions differ greatly, they share one commonality - relying on Input DTOs.
Comparing Jimmer's Input DTOs with GraphQLInput:
-
Similarities
In GraphQL, query results are GraphQLObjects, i.e. dynamic objects of arbitrary shapes. However, if mutation operations accept object parameters, they must be GraphQLInputs, i.e. static objects of fixed shapes.
GraphQLInput has exactly the same idea as InputDTO here, which is the inevitable design to resolve security issues - different routes leading to the same destination.
-
Differences
-
GraphQLInput is just a protocol, merely constraining that object parameters for modification operations must be static objects of fixed shapes.
The benefit is not limiting specific implementation technologies, but the downside is the persistence business logic needs to be implemented by developers for every specific GraphQLInput shape. Tedious work still exists objectively, and developers can truly feel the pain of Input DTO explosion.
-
Jimmer's InputDTO is just an alternative representation of the dynamic entity to address security issues. Once the user finishes converting Input DTO to dynamic entity, the huge convenience of persisting arbitrary shaped data structures in one line of code via save commands can still be enjoyed.
If developers adopt the Auto-Generated Input DTOs via DTO Language solution, Input DTO classes will be auto-generated, and the conversion logic between Input DTOs and dynamic entities will also be auto-generated. Thus the pain of Input DTO explosion no longer exists. So this is the recommended solution.
-