Define Entities
Unlike ORMs like JPA/Hibernate, entities in Jimmer are defined as interfaces instead of classes. The reasons are discussed in Why Interfaces.
Before defining entities, two concepts need to be introduced:
-
Jimmer entities are not simple Java beans, but dynamic objects.
That is, an unset property is completely different from a property set to null.
-
Jimmer entities are immutable, so interfaces only have getters, no setters.
Define Non-Associated Fields
Assume the entity package is "com.example.model". Ignore associated properties first, entity definitions are:
-
BookStore
- Java
- Kotlin
BookStore.javapackage com.example.model;
import org.babyfish.jimmer.sql.*;
import org.jetbrains.annotations.Nullable;
@Entity
public interface BookStore {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();
String name();
@Nullable
String website();
}BookStore.ktpackage com.example.model
import org.babyfish.jimmer.sql.*
@Entity
interface BookStore {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long
val name: String
val website: String?
} -
Book
- Java
- Kotlin
Book.javapackage com.example.model;
import org.babyfish.jimmer.sql.*;
import java.math.BigDecimal;
@Entity
public interface Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();
String name();
int edition();
BigDecimal price();
}Book.ktpackage com.example.model
import org.babyfish.jimmer.sql.*
import java.math.BigDecimal
@Entity
interface Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long
val name: String
val edition: Int
val price: BigDecimal
} -
Author
- Java
- Kotlin
Author.javapackage com.example.model;
import org.babyfish.jimmer.sql.*;
@Entity
public interface Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();
String firstName();
String lastName();
/*
* Gender is an enum defined later
*/
Gender gender();
}Author.ktpackage com.example.model
import org.babyfish.jimmer.sql.*
@Entity
interface Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long
val firstName: String
val lastName: String
/*
* Gender is an enum defined later
*/
val gender: Gender
}Where
Gender
is an enum with two options:MALE
andFEMALE
.ORMs can map enums to strings (default) or numbers.
Although this example maps enum to string by default, the database has a check constraint limiting values to
'M'
and'F'
, not default'MALE'
and'FEMALE'
. So the enum needs to be configured as:- Java
- Kotlin
Author.javapackage com.example.model;
import org.babyfish.jimmer.sql.EnumItem;
public enum Gender {
@EnumItem(name = "M")
MALE,
@EnumItem(name = "F")
FEMALE
}Author.ktpackage com.example.model
import org.babyfish.jimmer.sql.EnumItem
enum class Gender {
@EnumItem(name = "M")
MALE,
@EnumItem(name = "F")
FEMALE
} -
TreeNode
- Java
- Kotlin
TreeNode.javapackage com.example.model;
import org.babyfish.jimmer.sql.*;
@Entity
public interface TreeNode {
@Id
@Column(name = "NODE_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();
String name();
}TreeNode.ktpackage com.example.model
import org.babyfish.jimmer.sql.*
@Entity
interface TreeNode {
@Id
@Column(name = "NODE_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long
val name: String
} -
For Java, each getter can use the traditional Java bean
get/is
prefix likegetName()
, or omit it likename()
in this example.noteBreaking the Java bean convention of getter prefixes is enabled by Java 14 records, not Jimmer. The new style allows more concise immutable objects.
-
Jimmer entities are very sensitive to nullability:
- For Kotlin, use language's own nullity.
- For Java:
- Primitives like boolean, char, byte, short, int, long, float, double are non-null.
- Boxed types like Boolean, Character, Byte, Short, Integer, Long, Float, Double are nullable.
- Other types are non-null by default. Add
@Nullable
to allow null.
-
Annotations used in example:
@Entity
- Indicates entity type.@Id
- Specifies ID property.@GeneratedValue
- Specifies auto-generated ID, using database autoincrement here.
-
Java/Kotlin interface short name is converted to table name using
word1Word2...WordN -> WORD1_WORD2_..._WORDN
.If mismatch, use
@Table
on interface. -
Similarly, property name is converted to column name.
If mismatch, use
@Column
on property.noteNote
@Column
can be used on non-foreign key fields, not just foreign keys discussed later. -
Entities have non-null ID like
long
, not nullable ID likeLong
, otherwise framework will report error.noteJPA/Hibernate users note this difference in handling ID nullability.
One-to-Many Association Book.store
Modify Book.java
/Book.kt
to add store
association:
- Java
- Kotlin
package com.example.model;
...imports omitted...
@Entity
public interface Book {
...other properties omitted...
@ManyToOne
@Nullable
BookStore store();
}
package com.example.model
...imports omitted...
@Entity
interface Book {
...other properties omitted...
@ManyToOne
val store: BookStore?
}
-
@ManyToOne
declares one-to-many association, mapping foreign key to associated entity. -
Property name is converted to foreign key column like
store -> STORE_ID
, matching database.If mismatch, use
@JoinColumn
on property.
One-to-Many Association BookStore.books
Modify BookStore.java
/BookStore.kt
to add books
association:
- Java
- Kotlin
package com.example.model;
...imports omitted...
@Entity
public interface BookStore {
...other properties omitted...
@OneToMany(mappedBy = "store")
List<Book> books();
}
package com.example.model
...imports omitted...
@Entity
interface BookStore {
...other properties omitted...
@OneToMany(mappedBy = "store")
val books: List<Book>
}
-
@OneToMany
declares one-to-many association.It does not map any database field, just mirrors
@ManyToOne
to make association bidirectional.Here,
mappedBy = "store"
meansBookStore.books
is mirror ofBook.store
.@Entity
public interface BookStore {
@OneToMany(
mappedBy = "store"
)
List<Book> books();
...
}@Entity
public interface Book {
@ManyToOne
@Nullable
BookStore store();
...
}The side with
mappedBy
is called the mirror side. -
Mirroring is optional, bidirectional association is not required.
-
Unlike JPA/Hibernate,
@OneToMany
in Jimmer can only mirror@ManyToOne
, always implying bidirection.
Many-to-Many Association Book.authors
Modify Book.java
/Book.kt
to add authors
association:
- Java
- Kotlin
package com.example.model;
...imports omitted...
@Entity
public interface Book {
...other properties omitted...
@ManyToMany
@JoinTable(
name = "BOOK_AUTHOR_MAPPING",
joinColumnName = "BOOK_ID",
inverseJoinColumnName = "AUTHOR_ID"
)
List<Author> authors();
}
package com.example.model
...imports omitted...
@Entity
interface Book {
...other properties omitted...
@ManyToMany
@JoinTable(
name = "BOOK_AUTHOR_MAPPING",
joinColumnName = "BOOK_ID",
inverseJoinColumnName = "AUTHOR_ID"
)
val authors: List<Author>
}
-
@ManyToMany
declares many-to-many association. -
There are two sides of many-to-many: owner and mirror. This is the owner side.
-
For owner,
@JoinTable
can specify join table:- name: Join table name (default based on entities)
- joinColumnName: FK to current entity (Book)
- inverseJoinColumnName: FK to target entity (Author)
noteHere
@JoinTable
uses all default values, so it can be omitted.
Many-to-Many Association Author.books
Modify Author.java
/Author.kt
to add books
association:
- Java
- Kotlin
package com.example.model;
...imports omitted...
@Entity
public interface Author {
...other properties omitted...
@ManyToMany(mappedBy = "authors")
List<Book> books();
}
package com.example.model
...imports omitted...
@Entity
interface Author {
...other properties omitted...
@ManyToMany(mappedBy = "authors")
val books: List<Book>
}
Here, mappedBy
makes this the mirror side of the many-to-many mapping.
@ManyToMany(mappedBy = "authors")
means Author.books
mirrors Book.authors
.
@Entity
public interface Author {
@ManyToMany(
mappedBy = "authors"
)
List<Book> books();
...
}
@Entity
public interface Book {
@ManyToMany
@JoinTable(...略...)
List<Author> authors();
...
}
Mirroring many-to-many is optional, bidirection association is optional. But if bidirectional, one side must be owner and the other one must be mirror.
Unlike JPA/Hibernate, owner vs mirror does not affect save behavior in Jimmer. You can choose freely.
Complete TreeNode Definition
Now we understand associations. Let's quickly complete TreeNode:
- Java
- Kotlin
package com.example.model;
...imports omitted...
@Entity
public interface TreeNode {
...other properties omitted...
@ManyToOne
@Nullable
TreeNode parent();
@OneToMany(mappedBy = "parent")
List<TreeNode> childNodes();
}
package com.example.model
...imports omitted...
@Entity
interface TreeNode {
...other properties omitted...
@ManyToOne
val parent: TreeNode?
@OneToMany(mappedBy = "parent")
val childNodes: List<TreeNode>
}
Why Interfaces
We've seen entities declared as interfaces, not classes. Why?
-
Dynamicity
Jimmer entities are not simple Java beans, but dynamic objects.
-
Unset vs null property are totally different
-
Accessing non-existent property throws
UnloadedException
-
Serializing with Jackson omits unset properties
See Dynamic for details on dynamicity.
-
Benefits of dynamicity
-
Easily express arbitrary complex data structures. Entities can be partial object, complete object, or aggregate root of complex tree.
-
Since data structure is flexible, ORM can query and save entire object graph in one go instead of individual entities.
-
For querying, dynamic entity graphs can directly return from HTTP services without needing DTOs.
-
-
Compared to dynamic language ORMs
As discussed, Jimmer exploits dynamicity for flexibility unmatched by static language ORMs.
But it rejects unsafety and unmaintainability of dynamic languages. Jimmer entities remain ordinary Java/Kotlin objects with all static typing, even null safety in Kotlin.
noteThe only unsafety is
UnloadedException
when accessing unloaded properties, similar toLazyInitializationException
in JPA/Hibernate.This is a necessary cost universally accepted in ORM.
tipJimmer finds the optimal balance between dynamic language flexibility and static language safety.
-
-
Immutability
We've seen entities only have getters, meaning they are immutable.
Mutable object allows
circular references
which the most headache problem of business system, Immutable data avoids this risk.However, enjoying and leveraging the advantages of immutable objects while suppressing their disadvantages is not an easy thing, especially for scenarios like ORM where the object hierarchy is very deep.
Luckily, in the JavaScript/TypeScript domain, there is a framework called immer that provides a perfect way to handle deep immutability. Jimmer ports immer to Java/Kotlin and makes it the foundational programming model for the entire ORM. The name "Jimmer" is also a tribute to immer.
See Immutability/Solution for details on how to create and "modify" immutable objects.
In summary, because Jimmer entities themselves are dynamic and immutable, they are not simple Java objects. Their types have complex internal implementations. Therefore, Jimmer chooses to have developers write interfaces, and generate implementations at compile-time using AnnotationProcessors (Java) or KSP (Kotlin).