DTO Language
1. Concepts
1.1. Pain Point
Jimmer provides dynamic entities that can solve a large part of the DTO explosion problem very well. Therefore, generally speaking, it is not necessary to define output DTO types to express query results.
However, not all DTO types can be eliminated. Input DTO objects are hard to remove.
For example, in GraphQL, although dynamic
GraphQLObject
data is returned for the client from the output perspective, staticGraphQLInput
data submitted by the client is accepted from the input perspective.Why does the GraphQL protocol define
GraphQLInput
as a static type? Because API explicitness and system security are very important requirements, please refer to Problems with dynamic objects as input parameters.The problems faced by the GraphQL protocol are also faced by Jimmer, which must provide a complete solution.
The DTO language is designed for that part of the DTO types that cannot be eliminated, with the goal of making them extremely cheap.
1.2. Solution
As a comprehensive solution, Jimmer is not limited to ORM itself, but considers the whole project. To solve this problem, it provides the DTO language.
The DTO language is a very powerful feature provided by Jimmer, and is a concept highly similar to Object Fetcher, but at an earlier stage during compilation.
The DTO language is used to quickly define the shape of data structures. Based on these shapes, DTOs can be compile-time
-
Generate specific static DTO types corresponding to specific data structures
-
Generate mutual conversion logic between DTO static objects and Jimmer dynamic objects
-
Generate Object Fetchers that match perfectly with the DTO shape definitions
Using the DTO language to quickly build DTO types is a solution tailored for Jimmer with extremely high development efficiency, much faster than using mapstruct, and is the recommended approach.
1.3. Application Scenarios
The application scenarios of the DTO language:
-
Serve as return results in query APIs (optional, not recommended, don't use unnecessarily)
-
Serve as input parameters in super QBE queries (optional, recommended)
2. Create File
The code of the DTO language is reflected in files with the extension dto
. Once compilation is complete, these files have no value. So these files cannot be packaged into src/main/resources
, but should be placed in the src/main/dto
directory.
Therefore, the first thing is to create a dto
subdirectory under src/main
.
Jimmer does not require the src/main/dto
directory to be in the project where the entity type is located. In fact, you can create this directory in any project that can legally access the entity.
Jimmer only requires using jimmer-apt
or jimmer-ksp
in projects containing dto files. They are responsible for compiling dto files and generating related code.
For Java projects, in addition to
jimmer-apt
, it may be necessary to add an extra configuration. Please refer to Notes.
2.1. Two ways to create files
Multiple dto files can be defined under the src/main/dto
directory, with each file corresponding to an original entity.
Assuming there is a Jimmer entity type whose full type name is com.yourcompany.yourproject.Book
, and this class is decorated with @org.babyfish.jimmer.sql.Entity
(DTO language only supports Entity types), there are two ways to create dto files.
-
Not using
export
statementIn this case, the dto file directory needs to correspond to the original entity package, and the name needs to correspond to the original entity name:
-
Under the
src/main/dto
directory, create the directorycom/yourcompany/yourproject
, that is, create a directory consistent with the package name -
In the directory created in the previous step, create a new file
Book.dto
. This file must have the same name as the class and have the extensiondto
-
-
Using
export
statementThe
export
statement is important, so we will explain it separately in a chapter.
2.2 Export statement
In this case, the dto file directory and name are arbitrary, because we will use the export
statement inside the file to define which original entity the dto file corresponds to.
-
Since there is no requirement for the dto file directory, it is recommended to create the dto file directly under
src/main/dto
-
Although there is no requirement for the dto file name either, for the maintainability of the project, it is still recommended that the file name adopts the name of the original entity, which is
Book.dto
in this case -
The first line of code in the dto file is the
export
statementexport com.yourcompany.yourproject.Book
...subsequent code...
After the DTO file is compiled, more Java/Kotlin types will be generated automatically. Their default package name is: entity package name
+ .dto
.
If you use the export
statement, you can further define the package where the generated code is located, for example:
export com.yourcompany.yourproject.Book
-> package com.yourcompany.yourproject.mydto
Users can edit the Book.dto
file to define any number of DTO types with Book
as the aggregate root. Here we first define a DTO type:
...export statement omitted if any...
BookView {
...omitted...
}
The first line of code in the dto file may be the export
statement. For simplicity, export statements are omitted in subsequent sections.
After compilation, the Java/Kotlin type BookView
will be generated. Assuming the default value of the package where the generated code is located has not been changed, the generated code is as follows:
- Java
- Kotlin
package com.yourcompany.yourproject.dto;
import com.yourcompany.yourproject.Book;
import org.babyfish.jimmer.View;
public class BookView implements View<Book> {
...omitted...
}
package com.yourcompany.yourproject.dto
import com.yourcompany.yourproject.Book
import org.babyfish.jimmer.View
open class BookView(
...omitted...
) : View<Book> {
...omitted...
}
2.3. Notes
-
For Java projects (Kotlin developers please ignore):
If the current project is not the project that defines the entity, you need to decorate any class in the current project with
@org.babyfish.jimmer.sql.EnableDtoGeneration
.Otherwise, the DTO file will not be compiled.
-
dto files are compiled by Jimmer's Annotation Processor (Java) or Ksp (Kotlin).
Therefore, if you are using an IDE like Intellij to develop projects, then
-
In addition to dto files, if other Java/Kotlin files are modified, clicking the run button in the IDE directly can cause recompilation of dto files
-
However, if no other Java/Kotlin files are modified except dto files, simply clicking the run button in the IDE does not cause dto files to be recompiled unless explicitly rebuilding!
-
If you are using Gradle as your build tool, you can also use the third-party Gradle plugin provided by the community to solve this problem: jimmer-gradle
-
3. view, input and specification
It was mentioned earlier that the DTO language has three use cases:
-
Serve as return values in query APIs (optional, not recommended, don't use unnecessarily)
-
Serve as input parameters in super QBE queries (optional, recommended)
So, the DTO language can define three types of DTOs:
-
view: Neither uses the
input
keyword nor thespecification
keyword, which can be understood as Output DTO. -
input: Declared with the
input
keyword, which can be understood as Input DTO. -
specification: Declared with the
specification
keyword. It is not closely related to DTO itself, but can be used as a query parameter to support super QBE queries.
BookView {
...omitted...
}
AnotherBookView {
...omitted...
}
input BookInput {
...omitted...
}
input AnotherBookInput {
...omitted...
}
specification BookSpecification {
...omitted...
}
specification AnotherBookSpecification {
...omitted...
}
This means:
-
BookView
andAnotherBookView
are used as query outputs. The generated Java/Kotlin types implement theorg.babyfish.jimmer.View<E>
interfacenoteIt is recommended that output DTOs end with
View
-
BookInput
andAnotherBookInput
are used as save command inputs. The generated Java/Kotlin types implement theorg.babyfish.jimmer.Input<E>
interfacenoteIt is recommended that input DTOs end with
Input
-
BookSpecification
andAnotherBookSpecification
are used as query parameters to support super QBE queries. The generated Java/Kotlin types implement theorg.babyfish.jimmer.Specification<E>
interfacenoteIt is recommended that query parameter DTOs end with
Specification
3.1 Common functionalities of view and input
For view and input, their generated Java/Kotlin types can convert between entities, with the following capabilities:
-
Constructor taking original entity type as parameter: Convert Jimmer dynamic entity object to static DTO object
-
toEntity()
: Convert static DTO object to Jimmer dynamic entity object
Take BookView
as an example:
- Java
- Kotlin
Book entity = ...omitted...;
// Entity -> DTO
BookView dto = new BookView(entity);
// DTO -> Entity
Book anotherEntity = dto.toEntity();
val entity: Book = ...omitted...
// Entity -> DTO
val dto = BookView(entity)
// DTO -> Entity
val anotherEntity: Book = dto.toEntity()
3.2 input-specific functionalities
Compared with Output DTO, Input DTO has the following differences:
-
If the entity ID property is configured with auto-increment strategy, the id property in the input DTO is nullable.
Here is the translation into English:
infoThe reason for this design is that saving the object does not necessarily require the id property when the entity's id property has an auto-increment strategy.
However, this does not mean Jimmer will simply assume like other ORMs such as JPA that lacking an id property means an insert operation while having an id property means an update operation.
Jimmer has smarter strategies in this regard, please refer to Save Commands/Save Modes. This article will not repeat the details.
If the developer does not accept this default behavior, there are two ways to write the DTO code:
-
Make the DTO type have no id property at all
input BookInput {
#allScalars(this)
-id
} -
Make the DTO type's id property non-nullable
input BookInput {
#allScalars(this)
id!
}
-
-
The input DTO can only define savable properties, such as simple properties, ordinary ORM associations, and id-view properties. Unsavable properties such as transient properties, formula properties, computed properties, and remote associations cannot be defined, otherwise it will cause compilation errors.
-
Input DTO provides comprehensive and powerful support for nullable properties.
tipFor properties that are allowed to be null in the original entity, how to map them through Input DTO is a complex topic. Jimmer provides comprehensive and powerful support.
Please refer to Modification/Save Command/Input DTO/Handle Null Values.
3.3 specification-specific functionalities
The role of specification
is similar to input
, used to decorate input types, but specification
does not provide the ability to convert between entity objects, but is used as a query parameter to support super QBE queries.
Super QBE Query is a very powerful feature of Jimmer that is not explained here. Please refer to the relevant section.
4. Simple properties
Attributes can be defined for DTO types to map properties in the original entity type, for example:
BookView {
id
name
edition
}
This means that the DTO only maps the three properties of the entity: id
, name
and edition
, as follows:
- Java
- Kotlin
public class BookView implements View<Book> {
private long id;
private String name;
private String edition;
public BookView(Book book) {
...omitted...
}
@Override
public Book toEntity() {
...omitted...
}
...other members omitted...
}
open class BookView(
val id: Long = 0,
val name: String = "",
val edition: Int = 0
) : View<Book> {
constructor(book: Book): this(...omitted...)
override fun toEntity(): Book {
...omitted...
}
...other members omitted...
}
5. allScalars
The DTO language supports a special macro property, #allScalars
, which represents mapping all scalar properties in the entity.
BookView {
#allScalars
}
According to the definition of the Book
type in the official example:
-
The scalar properties of
Book
itself areid
,name
,edition
, andprice
-
Book
inheritsTenantAware
, and theTenantAware
interface defines thetenant
property -
Book
also inheritsCommonEntity
(multiple inheritance), andCommonEntity
interface defines thecreatedTime
andmodifiedTime
properties
#allScalars
means automatically mapping all scalar properties of the entity, including inherited ones.
If this is not the behavior you want, then
-
You can use
allScalars(Book)
to indicate mapping only the scalar properties defined by the Book type itself, excluding inherited properties. -
You can also use
allScalars(TenantAware)
andallScalars(CommonEntity)
to indicate properties of specific super types.
For the current type Book
, #allScalars(Book)
can also be written as #allScalars(this)
.
In fact, #allScalars
supports multiple parameters, for example:
Macro Expression | Automatically Mapped Attributes |
---|---|
#allScalars | createdTime, modifiedTime, tenant, id, name, edition, price |
#allScalars(this) | id, name, edition, price |
#allScalars(TenantAware) | tenant |
#allScalars(CommonEntity) | createdTime, modifiedTime |
#allScalars(this, TenantAware) | tenant, id, name, edition, price |
#allScalars(this, CommonEntity) | createdTime, modifiedTime, id, name, edition, price |
#allScalars(TenantAware, CommonEntity) | createdTime, modifiedTime, tenant |
#allScalars(this, TenantAware, CommonEntity) | createdTime, modifiedTime, tenant, id, name, edition, price |
-
If the
#allScalars
macro is used, it must be defined as the first property, otherwise it will cause compile errors. -
If parameters are specified for the
#allScalars
macro, each parameter must be the current entity or its base type, otherwise it will cause compile errors.
6. Negative Attributes
The #allScalars
macro mentioned above will batch map multiple properties. Negative properties can remove some properties.
BookView {
#allScalars
-tenant
}
Similar to above, if we understand according to the inheritance relationship of the Book
type in the official example, #allScalars
will add the properties createdTime
, modifiedTime
, tenant
, id
, name
, edition
and price
.
-tenant
removes the tenant
property from it, finally adding the properties createdTime
, modifiedTime
, id
, name
, edition
and price
.
What is specified by the negative property is not the property name of the entity, but the name of the corresponding property in the DTO. Here the two are the same and there is no difference.
The Renaming section later will reveal the difference between the two.
It is not difficult to find that for this example, there is another equivalent writing:
BookView {
// Exclude `TenantAware`
#allScalars(Book, CommonEntity)
}
7. Nullability
By default:
-
All properties in
specification
are null by default. -
Otherwise, the nullability of DTO properties is the same as the nullability of original properties in entities.
We can use ?
or !
to modify DTO properties to change the nullability of DTO properties.
7.1. ?
We can use ?
to modify DTO properties to make them nullable.
If the original property in the entity is already nullable, it will cause compile errors.
For example:
input UpdateBookInput {
#allScalars
price?
}
You can even apply ?
to allScalars
to make all automatically mapped non-associative properties nullable:
input UpdateBookInput {
#allScalars?
}
-
Specification types do not allow
?
warningSince all properties in
specification
are nullable by default, specifying the modifier?
for properties withinspecification
will cause compile errors. -
When the original property in the entity does not allow null
For non-nullable original properties in an entity, if the corresponding property in the DTO object is null, the original property in the entity object will not be assigned after converting the DTO object to the entity object.
-
When the original property in the entity allows null
tipFor properties that are allowed to be null in the original entity, how to map them through Input DTO is a complex topic. Jimmer provides comprehensive and powerful support.
Due to length limitations, this issue forms a separate article. Please refer to Modification/Save Command/Input DTO/Handle Null Values.
7.2. !
We can use !
to modify DTO properties to make them non-null.
If the original property in the entity is already non-null, it will cause compile errors.
Its use is limited and can only be used in the following three cases:
-
Modify the id property in
input
typeIf the id property is configured with auto-increment strategy, the corresponding input DTO type will make the id property nullable.
However, this behavior does not always meet user expectations. You can use
!
to modify the id property in theinput
type to make it non-null, for example:Book.dtoinput BookUpdateInfo {
#allScalars
id!
}warningUsing
?
to modify other properties will cause compile errors. -
If
input
is modified by theunsafe
keyword, any nullable entity property can be converted to non-null, for example:Book.dtounsafe input BookUpdateInfo {
#allScalars
store! {
...omitted...
}
}infoFor an entity object, if the corresponding value of this property is null, when using the Input DTO constructor to convert it to an Input DTO object, an exception will occur because the corresponding property in DTO does not allow null.
This is the meaning of the
unsafe
keyword. -
For
specification
, since all properties are null by default, any nullable entity property can be converted to non-null, for example:Book.dtospecification BookSpecification {
#allScalars
edition!
}
8. Renaming
The as
keyword can be used to set aliases for properties so that entity property names are different from DTO property names.
as
has two usages, it can precisely set an alias for a single property, and it can also vaguely set aliases for multiple properties.
8.1. Renaming a single property
BookView {
name as bookName
}
8.2. Renaming multiple properties
Assume there is an entity type called Robot
:
OrderView {
as (^ -> oldSystem) { ❶
prop1
prop2
}
as ($ -> ForNewSystem) { ❷
prop3
prop4
}
}
Where:
-
❶ Adds the prefix
oldSystem
toprop1
andprop2
, where^
indicates the start position -
❷ Adds the suffix
ForNewSystem
toprop3
andprop4
, where$
indicates the end position
The generated RobotView
class is as follows:
- Java
- Kotlin
public class RobotView implements View<Robot> {
private String oldSystemProp1;
private String oldSystemProp2;
private String prop3ForNewSystem;
private String prop4ForNewSystem;
...other members omitted...
}
open class RobotView(
val oldSystemProp1 = "",
val oldSystemProp2 = "",
val prop3ForNewSystem = "",
val prop4ForNewSystem = ""
) : View<Robot> {
...other members omitted...
}
You can define most properties in the as() {...}
block, of course including #allScalars
, for example:
RobotView {
as(^ -> robot) {
#allScalars
}
}
Below are all the usages of the as() {...}
block:
Code Example | Description |
---|---|
as(^ -> prefix) | Add prefix |
as(^prefix ->) | Remove prefix |
as(^prefix -> newPrefix) | Replace prefix |
as($ -> suffix) | Add suffix |
as($suffix ->) | Remove suffix |
as($suffix -> newSuffix) | Replace suffix |
as(infix -> ) | Remove any content |
as(infix -> newInfix) | Replace any content |
^
and $
cannot appear at the same time, because it means precise renaming of a single property, which duplicates existing functionality.
9. Enum Mapping
By default, enum type properties in entities are still enum types after being mapped to DTO properties.
You can map enum types to numbers or strings:
-
Map to numbers
AuthorView {
#allScalars
gender -> {
MALE: 100
FEMALE: 101
}
}The following code will be generated:
- Java
- Kotlin
BookView.javapublic class AuthorView implements View<AuthorView> {
private int gender;
...other members omitted...
}BookView.ktopen class RobotView(
val gender: Int,
...other members omitted...
) : View<Robot> {
...other members omitted...
} -
Map to strings
AuthorView {
#allScalars
gender -> {
MALE: "Male"
FEMALE: "Female"
}
}The following code will be generated:
- Java
- Kotlin
BookView.javapublic class AuthorView implements View<AuthorView> {
private String gender;
...other members omitted...
}BookView.ktopen class RobotView(
val gender: String,
...other members omitted...
) : View<Robot> {
...other members omitted...
}
10. Association Attributes
The handling of association properties is a very powerful feature of the DTO language with three usages:
- Direct use
- Recursive association
- Call id function
- Call flat function
10.1. Ordinary Association
-
Basic usage
Robot.dtoinput CompositeBookInput {
#allScalars(Book)
store {
#allScalars(BookStore)
-id
}
authors {
#allScalars(Author)
-id
}
}The generated code is:
- Java
- Kotlin
CompositeBookInput.javapublic class CompositeBookInput implements Input<Book> {
@Nullable
private Long id;
private String name;
private int edition;
private BigDecimal price;
private TargetOf_store store;
private List<TargetOf_authors> authors;
public static class TargetOf_store implements Input<BookStore> {
private String name;
@Nullable
private String website;
...other members omitted...
}
public static class TargetOf_authors implements Input<Author> {
private String firstName;
private String lastName;
private Gender gender;
...other members omitted...
}
...other members omitted...
}CompositeBookInput.ktopen class CompositeBookInput(
val id: Long? = null,
val name: String = "",
val edition: Int = 0,
val price: BigDecimal,
val store: TargetOf_store? = null,
val authors: List<TargetOf_authors> = emptyList(),
) : Input<Book> {
open class TargetOf_store(
val name: String = "",
val website: String? = null,
) : Input<BookStore> {
...other members omitted...
}
open class TargetOf_authors(
public val firstName: String = "",
public val lastName: String = "",
public val gender: Gender,
) : Input<Author> {
...other members omitted...
}
...other members omitted...
} -
Renaming
If you want to rename association properties, the
as
clause should be before the association definition block, for example:Book.dtoinput CompositeBookInput {
authors as authorList {
...omitted...
}
...omitted...
}
10.2 Recursive Association
For recursively associative properties in entities, such as TreeNode.parent
or TreeNode.childNodes
in the attached example, the corresponding associative property in the DTO can be marked as recursive using an asterisk *
.
TreeNodeView {
#allScalars
childNodes*
}
10.3. id Function
Here is the translation into English:
For
, while of course we can choose to define @IdView properties on the entities and simply reference them in DTOs, we can't always rely on entity types having declared @IdView properties. A better approach should not make that assumption.Therefore, the DTO language supports the id
function on associated properties to achieve the same purpose:
BookView {
id(store)
id(authors) as authorIds
...omitted...
}
This code has the exact same function as the previous example, but no longer assumes the entity type always declares @IdView properties, which is a better solution.
10.4. flat Function
-
For view and input, the
flat
function can only be used for referential associations (one-to-one or many-to-one), not collection associations (one-to-many or many-to-many). It is used to promote the properties of associated DTOs to the current DTO type to obtain a flattened object without associations. -
For specification, the
flat
function does not have the above restrictions and can be used for collection associations. Please check (../../query/super_qbe).
Take the TreeNode
entity in the official example (in fact, this example is wrong, we will discuss this issue later):
FlatTreeNodeView {
#allScalars(TreeNode)
flat(parent) {
#allScalars(TreeNode)
}
}
As mentioned above, this example is wrong, because the aggregate root and the associated object both have the id
and name
properties. Unconditionally extracting the properties of the associated object to the aggregate root will inevitably lead to name conflicts, eventually causing DTO language compilation errors.
The right approach is to combine it with the user's multi-property fuzzy renaming as(...) {}
block. Let's look at a new example:
FlatTreeNodeView {
#allScalars(TreeNode) // id, name
flat(parent) {
as(^ -> parent) {
// parentId, parentName
#allScalars(TreeNode)
}
}
}
Thus, we get a flattened DTO type with 4 properties: id
, name
, parentId
and parentName
.
You can even nest the flat
function to convert multi-level associations into flattened objects:
FlatTreeNodeView {
#allScalars(TreeNode)
flat(parent) {
as(^ -> parent) {
#allScalars(TreeNode)
}
flat(grandParent) {
as(^ -> parent) {
#allScalars(TreeNode)
}
}
}
}
Thus, we get a flattened DTO type with 6 properties: id
, name
, parentId
, parentName
, grandParentId
, and grandParentName
.
- Java
- Kotlin
TreeNode treeNode = Immutables.createTreeNode(cola -> {
cola.setId(4L);
cola.setName("Coca cola");
cola.applyParent(drinks -> {
drinks.setId(3L);
drinks.setName("Drinks");
drinks.applyParent(food -> {
food.setId(2L);
food.setName("Food");
})
})
});
// Convert hierarchical Entity to flattened DTO
FlatTreeNodeView view = new FlatTreeNodeView(treeNode);
System.out.println(view);
val treeNode = TreeNode {
id = 4L
name = "Coca cola"
parent {
id = 3L
name = "Drinks"
parent {
id = 2L
name = "Food"
}
}
}
// Convert hierarchical Entity to flattened DTO
val view = FlatTreeNodeView(treeNode)
println(view)
The print result is (formatted here for readability):
com.yourcompany.yourproject.FlatTreeNodeView(
id = 4,
name = Coco cola,
parentId = 3,
parentName = Drinks,
grandParentId = 2,
grandParentName = "Food"
)
The flat
solution also has drawbacks. For output DTOs, the flat
solution is controversial and is not recommended to be blindly adopted.
Please refer to
11. Custom Fields
11.1 Introduction to Custom Fields
In the examples we have discussed before, all DTO properties are mapped from entity properties and become mapped properties.
In addition to mapped properties, DTO types also support mapping custom properties:
BookInput {
#allScalars(Author)? - id
remark: String
}
Here, BookInput
has a custom field remark
.
The difference between custom properties and mapped properties is that custom properties require type definitions.
11.2 Built-in Types
Custom fields can be defined as built-in types, which are types built into the DTO language that do not require import via import
.
-
Primitive and boxed types
DTO Language Type Generated Java Type Generated Kotlin Type Boolean boolean Boolean Boolean? Boolean Boolean? Char char Char Char? Character Char? Byte byte Byte Byte? Byte Byte? Short short Short Short? Short Short? Int int Int Int? Integer Int? Long long Long Long? Long Long? Float float Float Float? Float Float? Double double Double Double? Double Double? -
Any and String types
DTO Language Type Generated Java Type Generated Kotlin Type Any Object Any String String String -
Array types
DTO Language Type Generated Java Type Generated Kotlin Type Array<Boolean> boolean[] BooleanArray Array<Boolean?> Boolean[] Array<Boolean?> Array<Char> char[] CharArray Array<Char?> Character[] Array<Char?> Array<Byte> byte[] ByteArray Array<Byte?> Byte[] Array<Byte?> Array<Short> short[] ShortArray Array<Short?> Short[] Array<Short?> Array<Int> int[] IntArray Array<Int?> Integer[] Array<Int?> Array<Long> long[] LongArray Array<Long?> Long[] Array<Long?> Array<Float> float[] FloatArray Array<Float?> Float[] Array<Float?> Array<Double> double[] DoubleArray Array<Double?> Double[] Array<Double?> Array<UserType> UserType[] Array<UserType> Array<UserType?> UserType[] Array<UserType?> Array<*> Object[] Array<*> -
Collection types
DTO Language Type Generated Java Type Generated Kotlin Type Iterable<E> Iterable<? extends E> Iterable<E> MutableIterable<E> Iterable<E> MutableIterable<E> Collection<E> Collection<? extends E> Collection<E> MutableCollection<E> Collection<E> MutableCollection<E> List<E> List<? extends E> List<E> MutableList<E> List<E> MutableList<E> Set<E> Set<? extends E> Set<E> MutableSet<E> Set<E> MutableSet<E> Map<K, V> Map<? extends K, ? extends V> Map<K, V> MutableMap<K, V> Map<K, V> Mutable<K, V>
11.3 Generic Parameter Modifiers
Except for generic class types (arrays and collections) in built-in types, other generic class types support using in
or out
to modify generic parameters, for example:
DTO Language Type | Generated Java Type | Generated Kotlin Type |
---|---|---|
UserType1<UserType2> | UserType1<UserType2> | UserType1<UserType2> |
UserType1<out UserType2> | UserType1<? extends UserType2> | UserType1<out UserType2> |
UserType1<in UserType2> | UserType1<? super UserType2> | UserType1<in UserType2> |
11.4 Import Statements
Except for built-in types, other types must be fully qualified or imported at the beginning of the file when referenced, otherwise they will be considered to belong to the same package as the current entity.
The DTO language supports various styles of import statements, for example:
-
Single class import
import java.time.LocalDateTime
-
Single class renamed import
import java.time.LocalDateTime as LDT
-
Multi-class import
import java.time.{
LocalDateTime,
OffsetDataTime,
ZonedDataTime
} -
Multi-class renamed import
import java.time.{
LocalDateTime as LDT,
OffsetDataTime as ODT,
ZonedDataTime as ZDT
}
12. Annotations
12.1 Basic Usage
You can use annotations to decorate DTO types, mapped properties, and custom properties in the DTO language:
import org.babyfish.jimmer.client.Doc
import javax.validation.constraints.Size
@Doc("BookInput without associations") ❶
BookInput {
#allScalars
@Size(min = 4, max = 20) ❷
name
@Size(min = 10, max = 50) ❸
remark: String
}
Where:
-
❶ Decorates type
-
❷ Decorates mapped properties
-
❸ Decorates custom properties
There is a detail to note:
...import statements omitted...
BookView {
...other properties omitted...
@A authors @B {
...associated object properties omitted...
}
...other DTO types omitted...
}
Where:
-
@A
decorates theauthors
property -
@B
decorates the anonymous associated type
12.2 Annotation Replacement
-
As mentioned above, the DTO language supports annotations to directly control the annotations of the generated DTO types.
-
If a DTO property is not decorated by any annotation, the annotations of the corresponding property in the original entity will be copied (if the DTO property is mapped, not custom). Among them, any non-jimmer annotations under the
org.babyfish.jimmer.client
package will be copied to the generated DTO type, except for those.
12.3 value Parameter
The value
parameter in annotations can be abbreviated, for example:
@UserAnnotation(value = "a", value2 = "b", value3 = "c")
can be abbreviated as:
@UserAnnotation("a", value2 = "b", value3 = "c")
Unlike Java, no matter how many parameters an annotation has, the value
parameter can be abbreviated, as long as the abbreviated property is configured first.
12.4 Mixing Java and Kotlin Syntax
There are some subtle differences between Java and Kotlin annotation syntax. The DTO language mixes the features of both, for example:
-
@UserAnnotation(
"key1",
items = {
@Item("subKey1", description = "Detail information for subKey1 ..."),
@Item("subKey2", description = "Detail information for subKey2 ..."),
@Item("subKey3", description = "Detail information for subKey3 ...")
}
) -
@UserAnnotation(
"key1",
items = [
@Item("subKey1", description = "Detail information for subKey1 ..."),
@Item("subKey2", description = "Detail information for subKey2 ..."),
@Item("subKey3", description = "Detail information for subKey3 ...")
]
) -
@UserAnnotation(
"key1",
items = {
Item("subKey1", description = "Detail information or subKey1 ..."),
Item("subKey2", description = "Detail information for subKey2 ..."),
Item("subKey3", description = "Detail information for subKey3 ...")
}
) -
@UserAnnotation(
"key1",
items = [
Item("subKey1", description = "Detail information for subKey1 ..."),
Item("subKey2", description = "Detail information for subKey2 ..."),
Item("subKey3", description = "Detail information for subKey3 ...")
]
)
The above four styles are equivalent. You can freely choose the style you like.
13. Implementing Interfaces
DTO types are low-value information, and if different DTO types are allowed to reference each other, it would lead to a problem: the early effect is convenience, but the later effect is difficulty in maintenance. Therefore, Jimmer prohibits users from seeking reusability in low-value DTO types.
However, sometimes there is indeed a need to find commonalities between different DTO types (especially for internal classes of DTO), in order to facilitate abstraction and design. For this purpose, the DTO language provides a compromise: DTO types are allowed to implement existing Java/Kotlin interfaces, provided that the abstract properties in the interface are a subset of the properties in the DTO and can be implemented by the DTO type.
DTO code as follows
export com.yourcompany.yourproject.model.Book
-> com.yourcompany.yourproject.model.dto
import com.yourcompany.yourcompany.common.{
Shape1, Shape2, Shape3, Shape4, Shape5, Shape6
}
BookView implements Shape1<String>, Shape2 {
#allScalars
store implements Shape3<String>, Shape4 {
#allScalars
}
authors implements Shape5<String>, Shape6 {
#allScalars
}
}
Compiled, generates the following code
- Java
- Kotlin
package com.yourcompany.yourproject.dto;
import com.yourcompany.yourproject.Book;
import org.babyfish.jimmer.View;
public class BookView
implements View<Book>, Shape1<String>, Shape2 {
@Nullable
private TargetOf_store store;
private List<TargetOf_authors> authors;
...other members omitted...
public static class TargetOf_store
implements View<BookStore>, Shape3<String>, Shape4 {
...members omitted...
}
public static class TargetOf_authors
implements View<Author>, Shape5<String>, Shape6 {
...members omitted...
}
}
package com.yourcompany.yourproject.dto
import com.yourcompany.yourproject.Book
import org.babyfish.jimmer.View
open class BookView(
...other properties omitted...
store: TargetOf_store? = null,
authors: List<TargetOf_authors> = emptyList()
) : View<Book>, Shape1<String>, Shape2 {
...other members omitted...
open class TargetOf_store(
...omitted...
): View<BookStore>, Shape3<String>, Shape4 {
...omitted...
}
open class TargetOf_authors(
...omitted...
): View<Author>, Shape5<String>, Shape6 {
...omitted...
}
}
If a property in the DTO overrides an abstract property in the interface, then in the auto-generated Java/Kotlin type, that property will be annotated with @Override
(Java) or override
keyword (Kotlin).
Apart from this, the DTO language does not perform much validation on interface implementation. If the user makes other mistakes, it will result in generating incorrect Java/Kotlin types, which will be handled by the Java/Kotlin compiler.
12. Related Links
As mentioned earlier, the DTO language has two more powerful features that are not discussed in depth in this article due to length limitations, but have been separated into other documents. Here we emphasize them again: