DEV Community

Silver_dev
Silver_dev

Posted on

Hibernate Under the Hood. Part 3: Entity Mapping

Entity mapping is the foundation of working with Hibernate/JPA. It defines how Java objects are mapped to database tables.

Basic Annotations: Where an Entity Begins

Any mapping starts with declaring a class as an entity and defining its primary key.

  • @Entity: Marks the class as an entity that will be mapped to a database table. This is a mandatory annotation.
  • @Table: Allows you to explicitly specify the table name. If omitted, Hibernate will default to the class name (e.g., for User, the table will be User).
  • @Id: Defines the field that serves as the primary key.
  • @GeneratedValue: Specifies that the primary key value will be generated automatically. The GenerationType.IDENTITY strategy (database auto-increment) is one of the most common and straightforward.
  • @Column: Used for fine-tuning the column: you can specify its name, length, precision, forbid null values, etc.

Example of a simple entity:

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "full_name", nullable = false)
    private String name;

    // ... getters and setters
}
Enter fullscreen mode Exit fullscreen mode

Relationship Types: From One-to-One to Many-to-Many

In JPA, there are four main types of relationships. The key concept to understand is the owning side. It is on the owning side that the relationship is defined in the database (e.g., which table will hold the foreign key or the join table).

  • @ManyToOne (Many-to-One): The most common and basic relationship. It maps directly to a foreign key in the table. Important: By default, JPA uses FetchType.EAGER for this, which is almost always undesirable.
@Entity
public class Book {
    // ...
    @ManyToOne(fetch = FetchType.LAZY) // Explicitly specify lazy loading!
    @JoinColumn(name = "publisher_id") // Name of the foreign key column
    private Publisher publisher;
}
Enter fullscreen mode Exit fullscreen mode
  • @OneToMany (One-to-Many): The inverse side of @ManyToOne. In JPA, this relationship is almost always bidirectional and must be the non-owning (inverse) side. For this, the mappedBy attribute is used, pointing to the field in the child entity that owns the relationship.
@Entity
public class Publisher {
    // ...
    @OneToMany(mappedBy = "publisher", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Book> books = new ArrayList<>();
}
Enter fullscreen mode Exit fullscreen mode
  • @OneToOne (One-to-One): A relationship where one record in table A corresponds to exactly one record in table B. Like @ManyToOne, it defaults to EAGER fetching. To enable lazy loading on the child side (where the foreign key resides), you must explicitly use @JoinColumn.

  • @ManyToMany (Many-to-Many): Requires an intermediate join table. The owning side defines this table using the @JoinTable annotation.

// Owning side
@Entity
public class Student {
    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>();
}

// Inverse (non-owning) side
@Entity
public class Course {
    @ManyToMany(mappedBy = "courses")
    private Set<Student> students = new HashSet<>();
}
Enter fullscreen mode Exit fullscreen mode

Cascades (CascadeType) and orphanRemoval

These mechanisms determine how operations performed on one entity affect its related entities.

  • CascadeType: Defines which operations (persist, remove, update) should be cascaded to related entities.

    • CascadeType.PERSIST: When the parent is saved, new child entities are automatically saved as well.
    • CascadeType.REMOVE: When the parent is deleted, all related child entities are also deleted.
    • CascadeType.ALL: Applies all operations. Use with caution, only if the child entity cannot exist without the parent.
  • orphanRemoval = true: A more specific and powerful tool. It deletes a child entity if it is "unlinked" from the parent (e.g., removed from the List<Book> collection in Publisher), even if the parent entity itself is not deleted. This is ideal for modeling strict "composition" relationships.

Fetch Strategies (FetchType): LAZY vs EAGER

This is one of the most critical topics for application performance.

  • FetchType.LAZY (Lazy): Hibernate loads the related entity only upon the first access to it. This is the recommended default approach for all relationships, especially for collections (@OneToMany, @ManyToMany), as it prevents the N+1 query problem and excessive data loading.
  • FetchType.EAGER (Eager): Hibernate loads the related entity immediately, along with the parent. While this might seem convenient, in practice, it often leads to loading massive amounts of unnecessary data and severe performance issues.

The golden rule: Always explicitly specify fetch = FetchType.LAZY for all @ManyToOne and @OneToOne relationships, as EAGER is their default value.

Best Practices and Common Pitfalls

  1. Always use FetchType.LAZY for @ManyToOne and @OneToOne. This is the single most important rule for avoiding performance bottlenecks.
  2. Define the owning side correctly. For @OneToMany, use mappedBy on the "one" side, and @JoinColumn on the "many" (owning) side.
  3. Be cautious with CascadeType.ALL. Apply it only for true compositions, where the child object has no business meaning without the parent.
  4. Use orphanRemoval = true for clean deletion of child entities when they are removed from a collection.
  5. Don't forget @JsonIgnoreProperties (or @JsonManagedReference/@JsonBackReference) when serializing bidirectional relationships to JSON to avoid infinite recursion.
  6. To load lazy relationships, use JOIN FETCH or @EntityGraph, rather than relying on OpenSessionInView, which is widely considered an anti-pattern.

Top comments (0)