Mastering Hibernate Relationships: The Ultimate Guide for 2025

  • As a Java Developer, modeling data is a core part of your Job. When working with JPA and Hibernate, correctly mapping the different entities of database plays a crucial role. Correctly mapping entity relationships is the key to clean, efficient and scalable application for an enterprise.

  • Incorrect mappings of entities can lead to nightmares, bugs and the database schema that is difficult to maintain due to its bad mapping of entities.

  • This in-depth guide will walk you through everything that you should know about JPA and Hibernate in 2025. This blog will cover the core concepts, copy-paste-ready code examples, and later dive into the crucial best practices that should be followed while mapping entities.

What Are Entity Relationships in Hibernate & JPA?

  • In relational database, data is stored in form of tables, and one table is linked with another table via FOREIGN KEY which works as a reference key for that table. Now take this same concept in the Java, Hibernate is a framework in Java that is used to map different entities with each other using JPA. Different annotations are used to describe the links between entities directly into the code. Each entity is defined as Object class in Java and further these objects are linked with other objects via Hibernate and JPA Relationships.

The 4 Types of Hibernate Relationships

  • There are 4 different fundamental ways by which you can map Entity-A with Entity-B:

    • @OneToOne: One A is linked to One B. (e.g., One User have One Address)

    • @OneToMany: One A is linked to Many B. (e.g., One User have Many Post)

    • @ManyToOne: Many A’s are linked to One B. (e.g., Many Post have One User)

    • @ManyToMany: Many A’s are linked to Many B’s. (e.g., Many User have Many Group)

  • Let's dive into how to implement each one.

@OneToOne: The Exclusive Pairing

  • Use this when one record in the table is associated with exactly one record of another table.

  • Example: One User have One unique Address.

  • Bidirectional @OneToOne example code

  • The best practice is to make the relationship bidirectional, where each entity knows about the other. The side with the FOREIGN KEY is the relationship owning side.

  • Now let’s implement this with One User have One Address relationship.

  • First, we will describe User.java file as attached below, where we are mapping Address class with @OneToOne annotation.

  •       @Data
          @Entity
          @Builder
          @NoArgsConstructor
          @AllArgsConstructor
          @Table(name = "users")
          public class User {
    
              @Id
              @GeneratedValue(strategy = GenerationType.IDENTITY)
              private Long userId;
    
              @NotNull
              @Column(unique = true)
              private String username;
    
              @ToString.Exclude
              @EqualsAndHashCode.Exclude
              @OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
              private Address userAddress;
    
          }
    
  • Secondly, we will be defining the Address.java file where we will be mapping User class with Address class using @OneToOne annotation.

  •       @Data
          @Entity
          @Builder
          @NoArgsConstructor
          @AllArgsConstructor
          public class Address {
    
              @Id
              @GeneratedValue(strategy = GenerationType.IDENTITY)
              private Long addressId;
    
              @NotNull
              private String street;
    
              @NotNull
              private String city;
    
              @NotNull
              private String zipCode;
    
              @OneToOne
              @ToString.Exclude
              @EqualsAndHashCode.Exclude
              @JoinColumn(name = "user_fk_id")
              private User user;
    
          }
    
  • Key Annotations Explained:

    • @OneToOne: Defines the One To One association of two classes.

    • @JoinColumn(name = “user_fk_id“): Describes the Foreign Key column for the Address table. This annotation is placed on the Owning side of the relationship.

    • mappedBy = “user“: Placed on the non-owning side of the relationships. It tells the Hibernate to look as the user field in the Address class to find the mapping configurations.

    • cascade = CascadeType.ALL: This tells Hibernate, whenever the non-owning side(User class is non-owning side in our case) is saved, updated, merged or deleted, the owning side of relationship(Address class in our case) should also be automatically saves, updated, merged or deleted.

@OneToMany & @ManyToOne: The Most Common Relationship

  • This relationship is the bread & butter of the data modeling. A User can have many Post, but each Post can be uploaded by only one User. This is @OneToMany relationship from User to Post and @ManyToOne relationship from Post to User.

  • Bidirectional @OneToMany / @ManyToOne Example code:

  • First things First, Let’s build User.java class as shown in below attached code block. The below code block show @OneToMany Relationship between User and Post.

  •       @Data
          @Entity
          @Builder
          @NoArgsConstructor
          @AllArgsConstructor
          @Table(name = "users")
          public class User {
    
              @Id
              @GeneratedValue(strategy = GenerationType.IDENTITY)
              private Long userId;
    
              @NotNull
              @Column(unique = true)
              private String username;
    
              @Builder.Default
              @ToString.Exclude
              @EqualsAndHashCode.Exclude
              @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
              private List<Post> posts = new ArrayList<>();
    
          }
    
  • Then after we will build Post.java file to Represent @ManyToOne relationship between Post and User.

  •       @Data
          @Entity
          @Builder
          @NoArgsConstructor
          @AllArgsConstructor
          public class Post {
    
              @Id
              @GeneratedValue(strategy = GenerationType.IDENTITY)
              private Long postId;
    
              @NotNull
              private String title;
    
              @NotNull
              private String content;
    
              @ManyToOne
              @ToString.Exclude
              @EqualsAndHashCode.Exclude
              @JoinColumn(name = "users_fk_id")  // Here 'fk' represents the 'Foreign Key'!!
              private User user;
    
          }
    
  • Key Best Practice: The @ManyToOne is almost always the Owning side of the Relationships because it is easy for the table having multiple rows related with the single foreign key to its “Parent“.

@ManyToMany: Handling Complex Connections

  • Use this relationship when one table can be linked with many records of another table, and vice-versa.

  • Example: A User can Many Groups and, a Group can have Many Users.

  • This relationship requires a Third table in the database also known as a Join Table to store the Pairings. (e.g., user_group join table with user_id and group_id as keys, where one is primary key and another is foreign key.)

  • Now Let’s follow up with a code block representing this relationship.

  • The attached below code block represents the User.java class.

  •       @Data
          @Entity
          @Builder
          @NoArgsConstructor
          @AllArgsConstructor
          @Table(name = "users")
          public class User {
    
              @Id
              @GeneratedValue(strategy = GenerationType.IDENTITY)
              private Long userId;
    
              @NotNull
              @Column(unique = true)
              private String username;
    
              @Builder.Default
              @ToString.Exclude
              @EqualsAndHashCode.Exclude
              @ManyToMany(mappedBy = "users", cascade = CascadeType.ALL)
              private Set<Group> groups = new HashSet<>();
    
          }
    
  • Let’s explore the Group.java class now.

  •       @Data
          @Entity
          @Builder
          @NoArgsConstructor
          @AllArgsConstructor
          @Table(name = "groups")
          public class Group {
    
              @Id
              @GeneratedValue(strategy = GenerationType.IDENTITY)
              private Long groupId;
    
              @NotNull
              private String name;
    
              @ManyToMany
              @Builder.Default
              @ToString.Exclude
              @EqualsAndHashCode.Exclude
              @JoinTable(
                      name = "user_groups",
                      joinColumns = @JoinColumn(name = "group_id"),
                      inverseJoinColumns = @JoinColumn(name = "user_fk_id")
              )
              private Set<User> users = new HashSet<>();
    
          }
    
  • Key Annotations Explained:

    • @JoinTable: This annotations tells Hibernate to create a third table to store the pairings of two tables.

    • joinColumns: This joins as primary key of the class in which @JoinTable annotation is used in to the newly created third table that will store the pairings of both the tables.

    • inverseJoinColumns: This joins as foreign key of the class in which @JoinTable annotation is not used in to the newly created third table that will store the pairings of both the tables.

Crucial Concepts You MUST Understand

  • Getting the annotations right is only half the battle. These concepts are vital for performance and correctness.

  • The mappedBy Attribute Explained

    • Question: What does mappedBy do in Hibernate?

    • Answer: In a bidirectional relationship, mappedBy is used on the inverse (non-owning) side to indicate that the other side is responsible for managing the relationship. The value of mappedBy is the name of the field on the owning side that defines the mapping. It tells Hibernate "Don't create a foreign key column for this field; the mapping is handled elsewhere."

  • Cascading Operations (CascadeType)

    • Question: What is CascadeType in JPA?

    • Answer: It defines what happens to a related entity when an operation (like save, update, or delete) is performed on its owner.

      • CascadeType.PERSIST: When you save the parent, the child is saved too.

      • CascadeType.MERGE: When you update the parent, the child is updated.

      • CascadeType.REMOVE: When you delete the parent, the child is deleted.

      • CascadeType.ALL: Includes all cascade operations. Use with caution!

  • Fetching Strategies: LAZY vs EAGER and the N+1 Problem

    • This is the single most important performance concept for Hibernate relationships.

      • FetchType.LAZY (Best Practice): Hibernate will only load the related entities from the database when you explicitly access them (e.g., by calling author.getBooks()). This is the default for collection-based relationships (@OneToMany, @ManyToMany). Always prefer LAZY fetching for collections.

      • FetchType.EAGER (Use with Caution): Hibernate will load the related entities at the same time it loads the parent entity. This is the default for single-entity relationships (@OneToOne, @ManyToOne). While sometimes convenient, it can lead to the infamous N+1 Query Problem.

    • What is the N+1 Query Problem?

    • Imagine you fetch a list of 100 Author entities (1 query). If the books collection is EAGER, Hibernate will then execute a separate query for each author to fetch their books, resulting in 100 additional queries (N queries). Total: 1 + N = 101 queries! This is incredibly inefficient. LAZY fetching avoids this by only fetching books for an author when you need them.

Hibernate Relationships: FAQ

  • Q1: Which side should be the "owning side" in a relationship?

    • In @OneToMany, the @ManyToOne side should always be the owner. For @OneToOne and @ManyToMany, it's your design choice, but be consistent. The owning side is the one where the foreign key or join table is defined.
  • Q2: Why use Set instead of List for @ManyToMany?

    • A Set is an unordered collection of unique elements. This perfectly models a many-to-many relationship, preventing duplicate associations and often performing better.
  • Q3: I'm getting a StackOverflowError in my toString() method. Why?

    • This happens with bidirectional relationships. If Author.toString() prints its list of books, and Book.toString() prints its author, they will call each other infinitely. Exclude the related entities from your toString() methods by using the annotation of @ToString.Exclude.

Conclusion & Key Takeaways

  • You now have a solid foundation for mapping any entity relationship in Hibernate.

  • Remember these golden rules:

    • Clearly Define Ownership: Use @JoinColumn on the owning side and mappedBy on the inverse side.

    • Default to LAZY Fetching: Especially for collections, to avoid the N+1 problem and ensure good performance.

    • Use Cascading Carefully: CascadeType.ALL is convenient but can lead to unintentional data deletion. Think about the entity lifecycle.

    • Use Set for @ManyToMany: It's a more appropriate data structure.

  • By following these guidelines, you'll build robust, performant, and maintainable data layers in your Java applications.

Bonus!

  • To see these Hibernate and JPA relationships in a real project, check out my simple social media backend service on GitHub. I highly recommend looking at the code to see how it all works.

  • Link: harshil8705/Social-Media-Basic-Backend

Happy coding!!!

0
Subscribe to my newsletter

Read articles from Harshil Champaneri directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Harshil Champaneri
Harshil Champaneri

Passionate Java Full Stack Developer | Expert in Java, Spring Boot, Spring AI, Hibernate, Spring Security | Skilled in MySQL, PostgreSQL, React.js, Tailwind CSS | Core Skills: DSA, OOP, DBMS, OS