Draft
In previous documents we have seen that
-
When the user defines the
Book
type, the example code can use theBookDraft
type -
When the user defines the
TreeNode
type, the example code can use theTreeNodeDraft
type
These types ending in Draft
that correspond one-to-one with user-defined types are called Draft types.
Use preprocessor
Define TreeNode
The user first defines an immutable data interface. Here, the ORM entity annotation @Entity
is not needed, the non-ORM annotation @Immutable
can achieve the demo purpose.
- Java
- Kotlin
package yourpackage;
import javax.validation.constraints.Null;
import java.util.List;
import org.babyfish.jimmer.Immutable;
@Immutable
public interface TreeNode {
String name();
@Null
TreeNode parent();
List<TreeNode> childNodes();
}
package yourpackage
import org.babyfish.jimmer.Immutable
@Immutable
interface TreeNode {
val name: String
val parent: TreeNode?
val childNodes: List<TreeNode>
}
Generate TreeNodeDraft
To automatically generate TreeNodeDraft
, the preprocessor needs to be enabled:
-
Java: Use AnnotationProcessor
jimmer-apt
-
Kotlin: Use KSP
jimmer-ksp
How to use jimmer-apt
/jimmer-ksp
and how to handle possible issues are introduced in great detail in Generate Code. This article will not repeat it.
- Java
- Kotlin
package org.babyfish.jimmer.example.core.model;
import java.util.List;
import org.babyfish.jimmer.DraftConsumer;
import org.babyfish.jimmer.lang.OldChain;
public interface TreeNodeDraft extends TreeNode, Draft {
TreeNodeDraft.Producer $ = Producer.INSTANCE;
@OldChain
TreeNodeDraft setName(String name);
TreeNodeDraft parent();
TreeNodeDraft parent(boolean autoCreate);
@OldChain
TreeNodeDraft setParent(TreeNode parent);
@OldChain
TreeNodeDraft applyParent(DraftConsumer<TreeNodeDraft> block);
@OldChain
TreeNodeDraft applyParent(TreeNode base, DraftConsumer<TreeNodeDraft> block);
List<TreeNodeDraft> childNodes(boolean autoCreate);
@OldChain
TreeNodeDraft setChildNodes(List<TreeNode> childNodes);
@OldChain
TreeNodeDraft addIntoChildNodes(DraftConsumer<TreeNodeDraft> block);
@OldChain
TreeNodeDraft addIntoChildNodes(TreeNode base, DraftConsumer<TreeNodeDraft> block);
class Producer {
private Producer() {}
public TreeNode produce(
DraftConsumer<TreeNodeDraft> block
) {
return produce(null, block);
}
public TreeNode produce(
TreeNode base,
DraftConsumer<TreeNodeDraft> block
) {
...omit...
}
...other code omitted...
}
...other code omitted...
}
@DslScope
public interface TreeNodeDraft : TreeNode {
public override var name: String
public override var parent: TreeNode?
public override var childNodes: List<TreeNode>
public fun parent(): TreeNodeDraft
public fun childNodes(): MutableList<TreeNodeDraft>
public object `$` {
...other code omitted...
public fun produce(
base: TreeNode? = null,
block: TreeNodeDraft.() -> Unit
): TreeNode {
...omit code...
}
}
...other code omitted...
}
public fun ImmutableCreator<TreeNode>.`by`(
base: TreeNode? = null,
block: TreeNodeDraft.() -> Unit
): TreeNode =
TreeNodeDraft.`$`.produce(base, block)
public fun MutableList<TreeNodeDraft>.addBy(
base: TreeNode? = null,
block: TreeNodeDraft.() -> Unit
): MutableList<TreeNodeDraft> {
add(TreeNodeDraft.`$`.produce(base, block) as TreeNodeDraft)
return this
}
public fun TreeNode.copy(block: TreeNodeDraft.() -> Unit): TreeNode =
TreeNodeDraft.`$`.produce(this, block)
You can use it like this:
-
Create a brand new object from scratch
- Java
- Kotlin
TreeNode oldTreeNode = Immutables.createTreeNode(treeNodeDraft -> {
...omitted...
});val oldTreeNode = TreeNode {
...omitted...
} -
Based on an existing object, make some "changes" and create a new object
- Java
- Kotlin
TreeNode newTreeNode = Immutables.createTreeNode(oldTreeNode, treeNodeDraft -> {
...omitted...
});val newTreeNode = TreeNode(oldTreeNode) {
...omitted...
}or
val newTreeNode = oldTreeNode.copy {
...省略...
}
Scalar properties
TreeNode.name
is a scalar property. TreeNodeDraft
will define a setter method/writable property
like below:
- Java
- Kotlin
public interface TreeNodeDraft extends TreeNode, Draft {
@OldChain
TreeNodeDraft setName(String name);
...other code omitted...
}
@DslScope
public interface TreeNodeDraft : TreeNode {
// var, not val
public override var name: String
...other code omitted...
}
Developers can use this method to set the name
property of the draft proxy:
- Java
- Kotlin
TreeNode treeNode = Immutables.createTreeNode(draft -> {
draft.setName("Root Node");
});
val treeNode = TreeNode {
name = "Root Node"
}
Reference associations
TreeNode.parent
is an association property. Its type is an object, not a collection. In ORM terms, it is a one-to-one or many-to-one association.
TreeNodeDraft
defines multiple methods for it:
Override getter parent()
- Java
- Kotlin
public interface TreeNodeDraft extends TreeNode, Draft {
TreeNodeDraft parent();
...other code omitted...
}
@DslScope
public interface TreeNodeDraft : TreeNode {
public fun parent(): TreeNodeDraft
...other code omitted...
}
Note that the return type of this method is TreeNodeDraft
rather than TreeNode
.
That is, if the reference association of the draft object has been set and set to non-null, this method will definitely return the draft object. This allows the user to directly modify deeper associated objects.
- Java
- Kotlin
TreeNode newTreeNode = Immutables.createTreeNode(treeNode, draft -> {
draft.parent().setName("Daddy");
draft.parent().parent().setName("Grandpa");
});
@DslScope
var newTreeNode = TreeNode(treeNode) {
parent().name = "Daddy"
parent().parent().name = "Grandpa"
}
Add getter parent(boolean)
- Java
- Kotlin
public interface TreeNodeDraft extends TreeNode, Draft {
TreeNode parent(boolean autoCreate);
...other code omitted...
}
@DslScope
public interface TreeNodeDraft : TreeNode {
// This getter is equivalent to Java's `parent(false)`
override fun parent: TreeNode
// This function is equivalent to Java's `parent(true)`
public fun parent(): TreeNodeDraft
...other code omitted...
}
Java's parent(false)
and Kotlin's parent
have two issues:
-
Accessing it will cause an exception if the
parent
property of the draft object is not set -
If the
parent
property of the draft object is set to null, accessing it will return null, and null does not support further modification
parent(true)
can resolve the above issues. If either of the above situations is met, it will automatically create and set an associated object, then allow the user to modify it. This is a very useful feature, especially when creating objects from scratch.
- Java
- Kotlin
TreeNode treeNode = Immutables.createTreeNode(/* No `base` here */ draft -> {
draft.parent(true).setName("Daddy");
draft.parent(true).parent(true).setName("Grandpa");
});
val treeNode = TreeNode /* No `base` here */ {
parent().setName("Daddy");
parent().parent().setName("Grandpa");
}
Add setParent
- Java
- Kotlin
public interface TreeNodeDraft extends TreeNode, Draft {
@OldChain
TreeNodeDraft setParent(TreeNode parent);
...other code omitted...
}
@DslScope
public interface TreeNodeDraft : TreeNode, Draft {
// var, not val
public var parent: TreeNode
...other code omitted...
}
This setter allows the user to replace the entire associated object.
- Java
- Kotlin
TreeNode treeNode = Immutables.createTreeNode(draft -> {
draft.setParent(
Immutables.createTreeNode(daddyDraft -> {
daddyDraft.setName("Daddy")
})
)
});
val treeNode = TreeNode {
parent = TreeNode {
name = "Daddy"
}
}
Add lambda-based applyParent
This feature only applies to Java.
Kotlin code is already concise enough and does not need this method to simplify the code.
public interface TreeNodeDraft extends TreeNode, Draft {
@OldChain
TreeNodeDraft applyParent(
DraftConsumer<TreeNodeDraft> block
);
@OldChain
TreeNodeDraft applyParent(
TreeNode base,
DraftConsumer<TreeNodeDraft> block
);
...other code omitted...
}
These two setters are used to simplify the code. Since the two methods are highly similar in usage, only the first one is used as an example:
-
More verbose writing:
TreeNode oldTreeNode = Immutables.createTreeNode(draft -> {
draft.setParent(
Immutables.createTreeNode(daddyDraft -> {
daddyDraft.setName("Daddy")
})
)
}); -
More concise writing:
TreeNode oldTreeNode = Immutables.createTreeNode(draft -> {
draft.applyParent(daddyDraft -> {
daddyDraft.setName("Daddy")
})
});
The two are completely equivalent.
Collection associations
TreeNode.childNodes
is an association property whose type is a collection rather than an object. In ORM terms, it is a one-to-many or many-to-many association.
TreeNodeDraft
defines multiple methods for it:
Inherit getter childNodes()
For both Java and Kotlin, TreeNodeDraft
cannot override the return type of childNodes()
method. From a syntactic point of view, it can only inherit the childNodes()
method of TreeNode
.
- Java
- Kotlin
List<TreeNode> childNodes();
var childNodes: List<TreeNode>
Although the return type defined in the TreeNode
interface is List<TreeNode>
, after being inherited by the TreeNodeDraft
interface, its return type should be understood as List<TreeNodeDraft>
.
If the collection association of the draft object has been set, all elements in the returned collection are drafts. This allows the user to directly modify deeper associated objects in the collection.
- Java
- Kotlin
TreeNode newTreeNode = Immutables.createTreeNode(treeNode, draft -> {
((TreeNodeDraft)
draft
.childNodes().get(0)
).setName("Son");
((TreeNodeDraft)
draft
.childNodes().get(0)
.childNodes().get(0)
).setName("Grandson");
});
val newTreeNode = TreeNode(treeNode) {
(childNodes[0] as TreeNodeDraft)
.name = "Son"
(childNodes[0].childNodes[0] as TreeNodeDraft)
.name = "Son"
}
The two forced type conversions in the above code significantly impair the developer experience. Therefore, this usage is not recommended in actual projects.
To achieve the same purpose, the childNodes(boolean)
method introduced below is more recommended.
Add getter childNodes(boolean)
- Java
- Kotlin
public interface TreeNodeDraft extends TreeNode, Draft {
List<TreeNodeDraft> childNodes(boolean autoCreate);
...other code omitted...
}
childNodes(false)
is equivalent to childNodes()
, and accessing it will cause an exception if the childNodes
property of the draft object is not set.
childNodes(true)
will resolve this problem by automatically creating and setting a collection if the collection association property has not yet been set, then allowing the user to modify that collection.
@DslScope
public interface TreeNodeDraft : TreeNode, Draft {
override var childNodes: List<TreeNode>
fun childNodes(): MutableList<TreeNode>
...other code omitted...
}
- The getter of the
childNodes
property is equivalent to Java'schildNodes(false)
. Accessing it will cause an exception if this mutable proxy property is not set. - The
childNodes()
function is equivalent to Java'schildNodes(true)
. It automatically creates the property if not set, allowing the user to modify the collection.
- Java
- Kotlin
TreeNode newTreeNode = Immutables.createTreeNode(treeNode, draft -> {
draft
.childNodes(false)
.get(0)
.setName("Son");
draft
.childNodes(false)
.get(0)
.childNodes(false)
.get(0)
.setName("Grandson");
});
Indeed, when the parameter is true, childNodes(boolean) can automatically create the collection when the childNodes property is not specified.
However, when the parameter is false, it is also very useful. Because the return type of this method is List<TreeNodeDraft>
instead of List<TreeNode>
, the developer-unfriendly code in the previous example will no longer exist.
val newTreeNode = TreeNode(treeNode) {
childNodes()[0].name = "Son"
childNodes()[0].childNodes()[0].name = "Grandson"
}
Add setChildNodes
- Java
- Kotlin
public interface TreeNodeDraft extends TreeNode, Draft {
@OldChain
TreeNodeDraft setChildNodes(List<TreeNode> childNodes);
...other code omitted...
}
@DslScope
public interface TreeNodeDraft : TreeNode, Draft {
// var, not val
override var childNodes: List<TreeNode>
...other code omitted...
}
This setter allows the user to replace the entire associated collection.
- Java
- Kotlin
TreeNode treeNode = Immutables.createTreeNode(draft -> {
draft.setChildNodes(
Arrays.asList(
Immutables.createTreeNode(childDraft -> {
childDraft.setName("Eldest son")
}),
Immutables.createTreeNode(childDraft -> {
childDraft.setName("Second son")
})
)
)
});
val treeNode = TreeNode {
childNodes = listOf(
TreeNode {
name = "Eldest son"
},
TreeNode {
name = "Second son"
}
)
}
Somewhat cumbersome, the addIntoChildNodes
to be introduced below is more recommended.
Add addIntoChildNodes
In the example above, we used setChildNodes
to replace the entire collection, but we can also choose to add collection elements one by one, rather than replacing the entire collection at one time.
The generated code is:
- Java
- Kotlin
public interface TreeNodeDraft extends TreeNode, Draft {
@OldChain
TreeNodeDraft addIntoChildNodes(
DraftConsumer<TreeNodeDraft> block
);
@OldChain
TreeNodeDraft addIntoChildNodes(
TreeNode base,
DraftConsumer<TreeNodeDraft> block
);
...other code omitted...
}
@DslScope
public interface TreeNodeDraft : TreeNode, Draft {
public fun childNodes(): MutableList<TreeNodeDraft>
...other code omitted...
}
public fun MutableList<TreeNodeDraft>.addBy(
base: TreeNode? = null,
block: TreeNodeDraft.() -> Unit
): MutableList<TreeNodeDraft> {
...omit...
return this;
}
You should use it like this:
- Java
- Kotlin
TreeNode treeNode = Immutables.createTreeNode(draft -> {
draft
.addIntoChildNodes(childDraft ->
childDraft.setName("Eldest son")
)
.addIntoChildNodes(childDraft ->
childDraft.setName("Second son")
)
});
val treeNode = TreeNode {
childNodes().addBy {
name = "Eldest son"
}
childNodes().addBy {
name = "Second son"
}
}
This approach implicitly contains a feature that the childNodes
property of the draft object is not set and the collection is automatically created, i.e. it contains a built-in childNodes(true)
.
Obviously, this approach is simpler than using the setter to replace the entire collection, so it is more recommended.