Skip to main content

Draft

In previous documents we have seen that

  • When the user defines the Book type, the example code can use the BookDraft type

  • When the user defines the TreeNode type, the example code can use the TreeNodeDraft 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.

TreeNode.java
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();
}

Generate TreeNodeDraft

To automatically generate TreeNodeDraft, the preprocessor needs to be enabled:

  • Java: Use AnnotationProcessor jimmer-apt

  • Kotlin: Use KSP jimmer-ksp

info

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.

TreeNodeDraft.java
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...
}

You can use it like this:

  1. Create a brand new object from scratch

    TreeNode oldTreeNode = Objects.createTreeNode(treeNodeDraft -> {
    ...omitted...
    });
  2. Based on an existing object, make some "changes" and create a new object

    TreeNode newTreeNode = Objects.createTreeNode(oldTreeNode, treeNodeDraft -> {
    ...omitted...
    });

Scalar properties

TreeNode.name is a scalar property. TreeNodeDraft will define a setter method/writable property like below:

TreeNodeDraft.java
public interface TreeNodeDraft extends TreeNode, Draft {

@OldChain
TreeNodeDraft setName(String name);

...other code omitted...
}

Developers can use this method to set the name property of the draft proxy:

TreeNode treeNode = Objects.createTreeNode(draft -> {
draft.setName("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()

TreeNodeDraft.java
public interface TreeNodeDraft extends TreeNode, Draft {

TreeNodeDraft parent();

...other code omitted...
}
info

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.

TreeNode newTreeNode = Objects.createTreeNode(treeNode, draft -> {
draft.parent().setName("Daddy");
draft.parent().parent().setName("Grandpa");
});

Add getter parent(boolean)

TreeNodeDraft.java
public interface TreeNodeDraft extends TreeNode, Draft {

TreeNode parent(boolean autoCreate);

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

TreeNode treeNode = Objects.createTreeNode(/* No `base` here */ draft -> {
draft.parent(true).setName("Daddy");
draft.parent(true).parent(true).setName("Grandpa");
});

Add setParent

TreeNodeDraft.java
public interface TreeNodeDraft extends TreeNode, Draft {

@OldChain
TreeNodeDraft setParent(TreeNode parent);

...other code omitted...
}

This setter allows the user to replace the entire associated object.

TreeNode treeNode = Objects.createTreeNode(draft -> {
draft.setParent(
Objects.createTreeNode(daddyDraft -> {
daddyDraft.setName("Daddy")
})
)
});

Add lambda-based applyParent

info

This feature only applies to Java.

Kotlin code is already concise enough and does not need this method to simplify the code.

TreeNodeDraft.java
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:

  1. More verbose writing:

    TreeNode oldTreeNode = Objects.createTreeNode(draft -> {
    draft.setParent(
    Objects.createTreeNode(daddyDraft -> {
    daddyDraft.setName("Daddy")
    })
    )
    });
  2. More concise writing:

    TreeNode oldTreeNode = Objects.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.

List<TreeNode> childNodes(); 
caution

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

info

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.

TreeNode newTreeNode = Objects.createTreeNode(treeNode, draft -> {
((TreeNodeDraft)
draft
.childNodes().get(0)
).setName("Son");
((TreeNodeDraft)
draft
.childNodes().get(0)
.childNodes().get(0)
).setName("Grandson");
});
danger

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)

TreeNodeDraft.java
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.

TreeNode newTreeNode = Objects.createTreeNode(treeNode, draft -> {
draft
.childNodes(false)
.get(0)
.setName("Son");
draft
.childNodes(false)
.get(0)
.childNodes(false)
.get(0)
.setName("Grandson");
});
tip

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.

Add setChildNodes

TreeNodeDraft.java
public interface TreeNodeDraft extends TreeNode, Draft {

@OldChain
TreeNodeDraft setChildNodes(List<TreeNode> childNodes);

...other code omitted...
}

This setter allows the user to replace the entire associated collection.

TreeNode treeNode = Objects.createTreeNode(draft -> {
draft.setChildNodes(
Arrays.asList(
Objects.createTreeNode(childDraft -> {
childDraft.setName("Eldest son")
}),
Objects.createTreeNode(childDraft -> {
childDraft.setName("Second son")
})
)
)
});
info

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:

TreeNodeDraft.java
public interface TreeNodeDraft extends TreeNode, Draft {

@OldChain
TreeNodeDraft addIntoChildNodes(
DraftConsumer<TreeNodeDraft> block
);

@OldChain
TreeNodeDraft addIntoChildNodes(
TreeNode base,
DraftConsumer<TreeNodeDraft> block
);

...other code omitted...
}

You should use it like this:

TreeNode treeNode = Objects.createTreeNode(draft -> {
draft
.addIntoChildNodes(childDraft ->
childDraft.setName("Eldest son")
)
.addIntoChildNodes(childDraft ->
childDraft.setName("Second son")
)
});
info

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.