Basic Usage
Introduction
One statement to save complex data of arbitrary shape, find DIFF to change database, like React/Vue
Upper right corner: The user passes in a data structure of any shape, and asks Jimmer to save it.
There is an essential difference between this and the save method of other ORM frameworks. Taking JPA/Hibernate as an example, whether the scalar properties of the entity need to be saved is controlled byColumn.insertable andColumn.updatable, and whether association properties need to be saved is controlled byOneToOne.cascade,ManyToOne.cascade,OenToMany.cascade andManyToOne.cascade. However, no matter how the developer configures it, the shape of the data structure that JPA/Hibernate can save for you is fixed.
Jimmer adopts a completely different approach. Although the saved jimmer object is strongly typed, it is dynamic (that is, not setting the object property and setting the object property to null are completely different things), Properties that are set are saved and properties that are not set are ignored, so that data structures of any shapes can be saved.
Upper left corner: Query the existing data structure from the database for comparison with the new data structure specified by the user.
The shape of the data structure queried from database is same with the shape of new data structure give by user. Therefore, the query cost and comparison cost are determined by the complexity of the data structure specified by the user.
Below: Compare the old and new data structures, find
DIFF
and execute the corresponding SQL operations:- Orange part: For entity objects that exist in both old and new data structures, if some scalar properties change, modify the data
- Blue part: For entity objects that exist in both old and new data structures, if some associations change, modify the association
- Green part: For entity objects that exist in the new data structure but do not exist in the old data structure, insert data and create the association
- Red part: For entity objects that exist in the old data structure but not in the new data structure, dissociate this object, clear the association and possibly delete the data
The purpose of this function: take the data structure of any shape as a whole, and use one line of code to write it into the database, no matter how complicated the intermediate details are, you don't have to care.
If you know React or Vue in the web field, it is not difficult to see that this function is very similar to `Virtual DOM diff`.
Basic Concepts
A save command persists an arbitrary shaped data structure to the database, for example:
-
Saving a simple object
- Java
- Kotlin
Book simpleBook = Immutables.createBook(draft -> {
draft.setName("SQL in Action");
draft.setEdition(1);
draft.setPrice(new BigDecimal("59.9"));
});
sqlClient.save(simpleBook);val simpleBook = Book {
name = "SQL in Action"
edition = 1
price = BigDecimal("59.9")
}
sqlClient.save(simpleBook) -
Saving a complex data structure
- Java
- Kotlin
Book complexBook = Immutables.createBook(draft -> {
draft.setName("SQL in Action");
draft.setEdition(1);
draft.setPrice(new BigDecimal("59.9"));
draft.applyStore(store -> {
store.setName("MANNING");
})
draft.addIntoAuthors(author -> {
author.setFirstName("Dmitry");
author.setLastName("Jamerov");
author.setGender(Gender.MALE);
});
draft.addIntoAuthors(author -> {
author.setFirstName("Svetlana");
author.setLastName("Isakova");
author.setGender(Gender.FEMALE);
})
});
sqlClient.save(simpleBook);val complexBook = Book {
name = "SQL in Action"
edition = 1
price = BigDecimal("59.9")
store {
name = MANNING;
}
authors().addBy {
firstName = "Dmitry"
lastName = "Jamerov"
gender = Gender.MALE
}
authors().addBy {
firstName = "Svetlana"
lastName = "Isakova"
gender = Gender.FEMALE
}
}
sqlClient.save(complexBook)
APIs
Save commands provide multiple APIs for different languages and coding patterns, but with the same functionality:
Spring Data API | Low-level API | ||
---|---|---|---|
APIs focused on conciseness | APIs focused on configurability | ||
Java |
|
| |
Kotlin |
|
|
Among them:
-
Java methods containing
Command
are special. Unlike other methods that execute save commands immediately, these methods only create the command without immediate execution. The user can configure them and finally callexecute
to execute the command. For example:BookStore store = ...
sqlClient
.getEntities()
.saveCommand(store) ❶
.setPessimisticLock() ❷
.execute(); ❸-
❶ Create save command, do not execute immediately
-
❷ Make some configurations
-
❸ After configuration is done, finally call
execute
to execute the save command
Kotlin does not need
saveCommand
methods, it uses a different syntax:val store = ...
sqlClient.save(store) {
setPessimisticLock()
} -
-
Methods containing
saveAll
indicate saving multiple objects rather than one object -
For the aggregate root being saved, there are 3 save modes: UPSERT (default), INSERT_ONLY, UPDATE_ONLY (see Save Modes for more details). This is configured like:
- Java
- Kotlin
BookStore store = ...
sqlClient.save(store, SaveMode.INSERT_ONLY);val store = ...
sqlClient.save(store, SaveMode.INSERT_ONLY)The
insert
andupdate
methods are shorthand forINSERT_ONLY
andUPDATE_ONLY
. The above can be simplified to:- Java
- Kotlin
BookStore store = ...
sqlClient.insert(store);BookStore store = ...
sqlClient.insert(store);
Important Concepts: Full Replacement vs Incremental Change
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.
- Java
- Kotlin
TreeNode treeNode = Immutables.createTreeNode(food -> {
food
.setParent(null)
.setName("Food")
.addIntoChildNodes(drink -> {
drink
.setName("Drink")
.addIntoChildNodes(cocacola -> {
cocacola.setName("Cocacola");
})
.addIntoChildNodes(fanta -> {
fanta.setName("Fanta");
});
;
})
.addIntoAuthors(bread -> {
bread
.setName("Bread")
.addIntoChildNodes(baguette -> {
baguette.setName("Baguette");
})
.addIntoChildNodes(ciabatta -> {
ciabatta.setName("Ciabatta");
})
});
;
});
sqlClient.save(treeNode);
val treeNode = TreeNode {
parent = null
name = "Food"
childNodes().addBy {
name = "Drinks"
childNodes().addBy {
name = "Cocacola"
}
childNodes().addBy {
name = "Fanta"
}
}
childNodes().addBy {
name = "Bread"
childNodes().addBy {
name = "Baguette"
}
childNodes().addBy {
name = "Ciabatta"
}
}
}
sqlClient.save(treeNode)
This code tries to save a tree:
+-Food
|
+---+-Drinks
| |
| +-----Cocacola
| |
| \-----Fanta
|
\---+-Bread
|
+-----Baguette
|
\-----Ciabatta
Where Food
is the aggregate root, and all other associated objects are child nodes.
-
Aggregate root node
Corresponds to incremental change, ultimately generating one INSERT or UPDATE statement.
-
Child nodes (or associated objects)
By default correspond to full replacement operations.
Take
Food
as an example. It has two child nodesDrinks
andBread
. This does not simply mean inserting or updatingDrinks
andBread
.
WhenFood
already exists in the database, we also need to consider whether it has other child nodes besidesDrinks
andBread
, and dissociate these child nodes from the parent (e.g. deleting them).For example:
Existing data structure in database Data structure user wants to save +-Food
|
|
|
+-----Meat(ignore child nodes)
|
\-----Bread(ignore child nodes)+-Food
|
+-----Drinks(ignore child nodes)
|
|
|
\-----Bread(ignore child nodes)Ultimately, for the child nodes, the operations are:
-
Drinks
does not exist in old data structure but exists in new data structure, so insertDrinks
. -
Both old and new data structures have
Bread
, so updateBread
. -
Old data structure has
Meat
while new data structure does not, so deleteMeat
.There are multiple ways to dissociate child nodes. Here we assume delete operations are used.
-
As you can see, by default, operations on child nodes other than the aggregate root correspond to full replacement rather than incremental modification.
Q & A
As discussed above, by default, associated objects other than the aggregate root correspond to full replacement operations rather than incremental modification.
Q:
Why do all associated objects other than the aggregate root default to full replacement operations?
A:
INSERT, UPDATE, and DELETE statements in relational databases are incremental operations themselves. Even using the simplest SQL scheme with just basic CRUD capabilities, incrementally modifying the database is never the real difficulty in application development.
The truly complex problem is to save an entire complex data structure in one go. If Jimmer does not provide such capabilities, developers have to write complex code to compare new and old data to find differences and determine what needs to change. This makes saving complex data structure very difficult.
There is another benefit - ensuring idempotency.
Q:
What are the use cases?
A:
Any scenario requiring saving an entire complex data structure in one go. One typical case is complex forms, for example:
In this example, the form embeds child object tables. The user can perform arbitrarily complex operations on the form, including the embedded child tables, and finally submit the entire form as a whole to the server.
With save commands, the server can persist this data structure in one line of code, without considering how the data structure submitted by the client differs from the database.
No matter how complex the form, or how deeply nested the associations, the entire structure can be saved in one line of code.