Feature Introduction
Concept
Save instructions allow developers to save data structures of any shape, rather than save simple objects.
By default, when AssociatedSaveMode is set to REPLACE, Jimmer will completely replace the existing data structure in the database with the structure being saved, as shown in the figure:
- Top right: Users pass in a data structure of any shape for Jimmer to write to the database.
Top left: Query the existing data structure from the database to compare with the new data structure passed in by users.
Whatever shape of data structure the user passes in, the same shape will be queried from the database, ensuring the shapes of old and new data structures are identical. Therefore, the querying and comparison costs are determined by the complexity of the user-provided data structure.
- Below:Compare the new and old data structures, find the DIFF and execute corresponding SQL operations to make them consistent:
- Orange parts: For entity objects that exist in both new and old data structures, modify data if scalar properties have changed
- Blue parts: For entity objects that exist in both new and old data structures, modify associations if they have changed
- Green parts: For entity objects that exist in the old data structure but not in the new one, decouple this object, clear associations and possibly delete data
- Red parts: For entity objects that exist in the new data structure but not in the old one, insert data and establish associations
Unlike other ORMs, Jimmer doesn't require describing how data should be saved in the entity model
- Whether certain scalar properties need to be savedTaking JPA as an example, this is controlled throughColumn.insertable andColumn.updatable.
- Whether certain association properties need to be savedTaking JPA as an example, this is controlled throughOneToOne.cascade,ManyToOne.cascade,OenToMany.cascade andManyToOne.cascade.
Jimmer adopts a completely different strategy - its entity objects are not POJOs and can flexibly control the shape of data structures.
That is, entity objects have dynamic properties - not specifying a property for an entity object and setting an entity's property to null are completely different things.
For any entity object, Jimmer will only save the specified properties while ignoring unspecified ones.
Therefore, Jimmer doesn't need to consider data saving behavior during entity modeling, but rather describes the expected behavior at runtime through the data structure being saved itself, providing absolute flexibility.
Scenarios
The UI design for modifying data in an application can be divided into two styles:
-
Fully Commit
This type of UI often has complex forms and provides a final button. After editing, the user submits all the information in the form at once.
-
Incremental Commit
This type of UI does not have a submit button. Each time the user completes a local operation, the page automatically submits the changed part, which is a fragmented commit mode.
The greatest value of the Save Command lies in simplifying the development of fully commit mode functionality. For the two different modes, the usage is different.
Fully Commit | Incremental Commit |
---|---|
Jimmer automatically handles the internal details, comparing the new and old data to find all differences and executing the relevant modification operations (Jimmer's unique perspective) Use the Save Command (parameters are often complex data structures) | Business code uses a combination of multiple simple operations to implement complex operations, and the user handles the internal details (the same as traditional methods). Comprehensively use multiple methods:
|
Developers need to analyze their business scenarios to determine whether the current modification operation is a fully commit or an incremental commit, and make the right choice accordingly, without abuse.
Demo
In actual development, the data to be saved is always submitted by the client and can be passively accepted by the server (for example, @RequestBody
in Spring).
However, to simplify the discussion here, we directly hard code the object to be saved, so the code for the saved parameters is relatively more.
-
Save simple object
- Java
- Kotlin
sqlClient.save(
Immutables.createBook(draft -> {
draft.setName("GraphQL in Action");
draft.setEdition(4);
draft.setPrice(new BigDecimal("59.9"));
})
);sqlClient.save(
Book {
name = "GraphQL in Action"
edition = 4
price = BigDecimal("59.9")
}
)noteHere, the id property of the object to be saved is not specified. Jimmer will determine whether related data exists in the database according to the
name
andedition
properties,
so as to decide whether toINSERT
orUPDATE
.This is because in the entity definition,
Book.name
andBook.edition
are annotated with@org.babyfish.jimmer.sql.Key
.
This article is just a quick preview and does not go deep into it. Interested parties can view Mapping Part/Advanced Mapping/Key and Mutation Part/Save Command. -
Save data structures formed by multiple objects
- Java
- Kotlin
sqlClient.save(
Immutables.createBook(draft -> {
draft.setName("GraphQL in Action");
draft.setEdition(4);
draft.setPrice(new BigDecimal("59.9"));
draft.applyStore(store -> {
store.setName("MANNING");
store.setWebsite("https://www.manning.com");
});
draft.addIntoAuthors(author -> {
author.setFirstName("Bob");
author.setLastName("Rockefeller");
author.setGender(Gender.MALE);
});
draft.addIntoAuthors(author -> {
author.setFirstName("Eve");
author.setLastName("Procello");
author.setGender(Gender.FEMALE);
});
})
);sqlClient.save(
Book {
name = "GraphQL in Action"
edition = 4
price = BigDecimal("59.9")
store {
name = "MANNING"
website = "https://www.manning.com"
}
authors().addBy {
firstName = "Bob"
lastName = "Rockefeller"
gender = Gender.MALE
}
authors().addBy {
firstName = "Eve"
lastName = "Procello"
gender = Gender.FEMALE
}
}
);
Essential Difference from Other ORM
In the previous text, we demonstrated two examples, one describing how to save a simple object, and the other describing how to save an aggregate root object and cascade save more associated objects.
It is obvious that Jimmer's save directive can appear both simple and complex, depending on whether the data structure expressed by the dynamic entity passed by the user is simple or complex.
Jimmer does not provide configuration cascade
options for association properties like traditional ORM, because it is not necessary at all. The dynamic entity gives Jimmer's save capability unlimited possibilities, so there is no need to limit it to some fixed configuration.
This absolute flexibility has many wonderful uses. For example, changing the price of book with id 100 to 60, the traditional ORM and Jimmer approaches are different:
-
Traditional ORM (take JPA as an example) adopts find first and then modify, which is intuitive but wastes performance
Book book = entityManager.find(Book.class, 100L);
if (book != null) {
book.setPrice(new BigDecimal(60));
// entityManager.merge(book); //Omit if the current JPA transaction context exists
} -
Jimmer's approach, make up a mutilated object and directly update
- Java
- Kotlin
boolean matched = sqlClient.update(
Immutables.createBook(draft -> {
draft.setId(100L);
draft.setPrice(new BigDecimal(60));
// No other properties except `id` and `price` are specified
// So no other properties except `price` will be affected
})
).getTotalAffectedRowCount() != 0;val matched = sqlClient.update(
Book {
id = 100L
price = BigDecimal(60)
// No other properties except `id` and `price` are specified
// So no other properties except `price` will be affected
}
).totalAffectedRowCount != 0
Note: Cannot Expose Directly
The ability to save data structures of arbitrary shapes is too powerful so that it cannot be exposed directly, otherwise, it will lead to huge security vulnerabilities. For example:
- Java
- Kotlin
@RestController
public class BookController {
private final JSqlClient sqlClient;
public BookController(JSqlClient sqlClient) {
this.sqlClient = sqlClient;
}
@PutMapping("/book")
pubic int saveBook(
@RequestBody Book book
) {
return sqlClient
.save(book)
.getTotalAffectedRowCount();
}
}
class BookController(
private val sqlClient: KSqlClient
) {
@PutMapping("/book")
fun saveBook(
@RequestBody book: Book
): Int =
sqlClient
.save(book)
.totalAffectedRowCount
}
This method can work and is very powerful. The client can upload any data structure with Book
as the aggregate root for the server to save.
But this is also dangerous. You cannot limit the complexity of the data structure uploaded by the client. The client can arbitrarily modify associated data of any depth through this API.
Even if you try to verify and limit the shape of the book
parameter, it is still very easy to overlook and make mistakes.
Therefore, the powerful Jimmer data saving capability can only be used as an underlying support internally in the service, and cannot expose this capability directly to remote clients by using dynamic entities as input parameters, because this will result in the security door wide open.
To safely expose Jimmer's data saving capabilities, please continue reading the next article: Exposing Features