Skip to main content

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?

@PutMapping("/book")
public void 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 from New York to Vancouver

  • <root>.store.location.country is modified from USA to Canada

  • <root>.authors[0].job.company is modified from NAB to IBM

  • <root>.authors[0].job.title is modified from Senior Business Analyst to Senior HR Manager

warning

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:

  • Lonely Form

    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.

    info

    All 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.