Skip to main content

Solution

In order to solve the inconvenience of secondary "modification" (creating a new immutable object based on another immutable object) of complex immutable objects in the current JVM ecosystem, some immutable object frameworks were born:

Jimmer is an ORM framework, and handling deep data structures is a core issue for ORM, so Jimmer must do similar work.

Jimmer needs to provide elegant dual language support for both Java and Kotlin at the same time. None of the above solutions can meet this requirement, so Jimmer did not use any of them, but chose to transplant immer from the JavaScript/TypeScript field.

Next, we will demonstrate the power of immutable objects transplanted from immer in three steps:

  1. Define immutable types

  2. Create an immutable data structure from scratch

  3. Based on an existing data structure, create a new data structure according to some desired modifications.

    This last step is where the core value of immer lies. Please pay close attention.

1. Define immutable type

To demonstrate this feature, there is no need for the @Entity annotation on ORM entities, the non-ORM @Immutable is sufficient.

TreeNode.java
package yourpackage;

import java.util.List;
import org.babyfish.jimmer.Immutable;

@Immutable
public interface TreeNode {

String name();

List<TreeNode> childNodes();
}

2. Create data structure from scratch

TreeNode treeNode = Immutables.createTreeNode(root -> {
root.setName("Root").addIntoChildNodes(food -> {
food
.setName("Food")
.addIntoChildNodes(drink -> {
drink
.setName("Drink")
.addIntoChildNodes(cocacola -> {
cocacola.setName("Cocacola");
})
.addIntoChildNodes(fanta -> {
fanta.setName("Fanta");
});
;
});
;
});
});

3. Create new data based on existing data

TreeNode newTreeNode = Immutables.createTreeNode(
treeNode, // existing data
root -> {
root
.childNodes(false).get(0) // Food
.childNodes(false).get(0) // Drink
.childNodes(false).get(0) // Cocacola
.setName("Cocacola plus");
}
);

// Show that `newTreeNode` reflects the developer's desired modifications
// Note that this does not affect the existing `treeNode` at all

System.out.println("treeNode:" + treeNode);
System.out.println("newTreeNode:" + newTreeNode);

Output (the actual printed output is compact, but is formatted here for readability)

treeNode: {
"name":"Root",
"childNodes":[
{
"name":"Food",
"childNodes":[
{
"name":"Drink",
"childNodes":[
{"name":"Coco Cola"},
{"name":"Fanta"}
]
}
]
}
]
}
newTreeNode: {
"name":"Root",
"childNodes":[
{
"name":"Food",
"childNodes":[
{
"name":"Drink",
"childNodes":[
{"name":"Coco Cola plus"},
{"name":"Fanta"}
]
}
]
}
]
}

As you can see, treeNode is unaffected, and newTreeNode reflects the user's desired modifications.

This transplant is a powerful complement to the JVM ecosystem.

This framework is named Jimmer, paying tribute to immer.

info

The sample code above uses a type called TreeNodeDraft, which is the interface type automatically generated by Jimmer based on the user-defined type TreeNode.

Readers can ignore this auto-generated interface for now, later documentation Draft will introduce it.