Skip to main content

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

    BookStore.java
    package 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();
    }
  • Book

    Book.java
    package 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();
    }
  • Author

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

    Where Gender is an enum with two options: MALE and FEMALE.

    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:

    Author.java
    package com.example.model;

    import org.babyfish.jimmer.sql.EnumItem;

    public enum Gender {

    @EnumItem(name = "M")
    MALE,

    @EnumItem(name = "F")
    FEMALE
    }
  • TreeNode

    TreeNode.java
    package 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();
    }
  • For Java, each getter can use the traditional Java bean get/is prefix like getName(), or omit it like name() in this example.

    note

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

    note

    Note @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 like Long, otherwise framework will report error.

    note

    JPA/Hibernate users note this difference in handling ID nullability.

One-to-Many Association Book.store

Modify Book.java/Book.kt to add store association:

Book.java
package com.example.model;

...imports omitted...

@Entity
public interface Book {

...other properties omitted...

@ManyToOne
@Nullable
BookStore store();
}
  • @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:

BookStore.java
package com.example.model;

...imports omitted...

@Entity
public interface BookStore {

...other properties omitted...

@OneToMany(mappedBy = "store")
List<Book> books();
}
  • @OneToMany declares one-to-many association.

    It does not map any database field, just mirrors @ManyToOne to make association bidirectional.

    Here, mappedBy = "store" means BookStore.books is mirror of Book.store.

    @Entity
    public interface BookStore {

    @OneToMany(
    mappedBy = "store"
    )
    List<Book> books();

    ...
    }

    mirror

    @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:

Book.java
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();
}
  • @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)
    note

    Here @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:

Author.java
package com.example.model;

...imports omitted...

@Entity
public interface Author {

...other properties omitted...

@ManyToMany(mappedBy = "authors")
List<Book> books();
}

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();

...
}

mirror

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

tip

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:

TreeNode.java
package com.example.model;

...imports omitted...

@Entity
public interface TreeNode {

...other properties omitted...

@ManyToOne
@Nullable
TreeNode parent();

@OneToMany(mappedBy = "parent")
List<TreeNode> childNodes();
}

Why Interfaces

We've seen entities declared as interfaces, not classes. Why?

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

      note

      The only unsafety is UnloadedException when accessing unloaded properties, similar to LazyInitializationException in JPA/Hibernate.

      This is a necessary cost universally accepted in ORM.

      tip

      Jimmer finds the optimal balance between dynamic language flexibility and static language safety.

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