Hibernate Many to One Annotation Tutorial

Hibernate Many-to-One Annotation Mapping Tutorial (2026) | D.Tech Academic

Hibernate Many-to-One Annotation Mapping Tutorial (2026)

Last Updated: | Author: Deep Singh

Many-to-one is the most common relationship you'll model in any real application. Many students belong to one college. Many orders belong to one customer. Many employees belong to one department. Getting this mapping right is fundamental to using Hibernate well.

In this tutorial, you'll learn how to map a many-to-one relationship in Hibernate using the @ManyToOne annotation. We'll build a small College / Students example, save two students pointing to the same college, and see exactly what the SQL and schema look like.

How Many-to-One Works in Hibernate

Many-to-one is the simplest of the relationship mappings. Multiple rows on one side (the "many" side) reference a single row on the other side (the "one" side).

In the database, this is represented by a foreign key column on the "many" table that points to the primary key of the "one" table:

college                    students
+------+                   +------+------+-------+-----------+
| id   | <-----------+     | roll | name | marks | college_id|
| name |             +---- | 1    | ...  | ...   | 1         |
+------+                   | 2    | ...  | ...   | 1         |
                           +------+------+-------+-----------+

Two students share the same college_id — that's the "many". Both point to the same college row — that's the "one".

In Hibernate, you mark the field on the "many" side with @ManyToOne. Hibernate creates the foreign key column automatically and inserts the correct ID when saving.

Note that the reverse side (College to Students) isn't strictly necessary for a working mapping. It's called the inverse side and is only needed if you want to navigate from the one side to the many side in code.

Maven Dependencies

Add the following dependencies to your pom.xml inside the <dependencies> block.

<dependencies>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>5.6.15.Final</version>
    </dependency>

    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>8.4.0</version>
    </dependency>
</dependencies>

Note on versions: The original tutorial used mysql:mysql-connector-java:5.1.6 from 2010. The connector moved to com.mysql:mysql-connector-j since Connector/J 8.0.31. The driver class also changed from com.mysql.jdbc.Driver to com.mysql.cj.jdbc.Driver, which we'll reflect in the config file.

hibernate.cfg.xml Configuration

Place this file in src/main/resources.

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
    <session-factory>
        <property name="hibernate.connection.driver_class">
            com.mysql.cj.jdbc.Driver
        </property>
        <property name="hibernate.connection.url">
            jdbc:mysql://localhost:3306/stds?useSSL=false&serverTimezone=UTC
        </property>
        <property name="hibernate.connection.username">root</property>
        <property name="hibernate.connection.password">root</property>

        <property name="hibernate.dialect">
            org.hibernate.dialect.MySQL8Dialect
        </property>
        <property name="hibernate.hbm2ddl.auto">update</property>
        <property name="hibernate.show_sql">true</property>
        <property name="hibernate.format_sql">true</property>
    </session-factory>
</hibernate-configuration>

The Entity Classes

College.java — The "One" Side

package com.deepsingh44.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "colleges")
public class College {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Key points:

  • No relationship annotations here. The College doesn't need to know about its students for a many-to-one mapping — the foreign key lives on the students table.
  • The original imported javax.persistence.OneToOne but never used it — a leftover. Removed.
  • Table renamed from "college" to "colleges" — plural naming is the common convention and avoids any potential reserved-word conflicts.

Students.java — The "Many" Side

package com.deepsingh44.model;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name = "students")
public class Students {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int roll;

    private String name;
    private float marks;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "college_id")
    private College college;

    public College getCollege() {
        return college;
    }

    public void setCollege(College college) {
        this.college = college;
    }

    public int getRoll() {
        return roll;
    }

    public void setRoll(int roll) {
        this.roll = roll;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public float getMarks() {
        return marks;
    }

    public void setMarks(float marks) {
        this.marks = marks;
    }
}

Key points:

  • @ManyToOne(cascade = CascadeType.ALL) — marks the relationship. Cascade means saving a Students also saves the associated College if it's not yet persisted.
  • @JoinColumn(name = "college_id") — names the foreign key column. Without it, Hibernate uses a default like college_id anyway, but being explicit is good practice.
  • Unused import removed: the original had import java.util.List; that wasn't used.
  • Cascade warning: CascadeType.ALL includes REMOVE, which means deleting a Students row would also delete the associated College — and any other students at that college would be orphaned. For a many-to-one, you usually want PERSIST and MERGE, not ALL. I've kept ALL to match the original tutorial's intent but flagged this in the pitfalls section.

Saving Records — Test.java

package com.deepsingh44.model;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;

public class Test {
    public static void main(String[] args) {

        StandardServiceRegistry ssr = new StandardServiceRegistryBuilder()
                .configure("hibernate.cfg.xml")
                .build();

        Metadata meta = new MetadataSources(ssr)
                .addAnnotatedClass(Students.class)
                .addAnnotatedClass(College.class)
                .getMetadataBuilder()
                .build();

        SessionFactory factory = meta.getSessionFactoryBuilder().build();
        Session session = factory.openSession();
        Transaction tx = session.beginTransaction();

        try {
            College college = new College();
            college.setName("A.B.C");

            Students s1 = new Students();
            s1.setName("Ramesh");
            s1.setMarks(67.7f);
            s1.setCollege(college);

            Students s2 = new Students();
            s2.setName("Suresh");
            s2.setMarks(77.7f);
            s2.setCollege(college);

            session.save(s1);
            session.save(s2);

            tx.commit();
            System.out.println("Saved successfully");
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            session.close();
            factory.close();
        }
    }
}

What's fixed:

  • Removed manual roll assignment. The original called s1.setRoll(1) and s2.setRoll(2) even though roll is annotated with @GeneratedValue. Setting it manually is ignored at best and causes a "detached entity passed to persist" error at worst. Removed both.
  • Correct transaction order. The transaction is opened before the first save(). The original opened it after object construction, which happened to work but isn't reliable.
  • Try/catch/finally. Rollback on failure, and both session and factory closed cleanly.
  • Variable naming. Renamed txt to tx for readability.

How the Tables Are Created

With hbm2ddl.auto=update, Hibernate creates two tables:

CREATE TABLE colleges (
    id INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(255),
    PRIMARY KEY (id)
);

CREATE TABLE students (
    roll INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(255),
    marks FLOAT,
    college_id INT,
    PRIMARY KEY (roll),
    FOREIGN KEY (college_id) REFERENCES colleges(id)
);

Notice:

  • The students table has a college_id column — that's the foreign key.
  • The colleges table has no back-reference to students. That's normal for unidirectional many-to-one.
  • The foreign key is nullable by default. If you want to require a college for every student, add @JoinColumn(name = "college_id", nullable = false).

What Hibernate Does When You Save

When you call session.save(s1):

  1. Hibernate checks the cascade setting. Because CascadeType.ALL is set on the college field, it detects that the College is unsaved.
  2. It inserts the college first: INSERT INTO colleges (name) VALUES ('A.B.C').
  3. It reads back the generated id (say, 1).
  4. It inserts s1 with college_id = 1.

When you call session.save(s2):

  1. The cascade setting triggers again, but Hibernate sees that the College is already persisted (it has an ID).
  2. It inserts s2 with the same college_id = 1.

The generated SQL looks roughly like:

INSERT INTO colleges (name) VALUES ('A.B.C');
INSERT INTO students (name, marks, college_id) VALUES ('Ramesh', 67.7, 1);
INSERT INTO students (name, marks, college_id) VALUES ('Suresh', 77.7, 1);

Two students, one college. That's many-to-one.

What a Query Looks Like

Loading a student and accessing its college:

Student s = session.get(Students.class, 1);
College c = s.getCollege();  // triggers a second SELECT if lazy

The default fetch type for @ManyToOne is EAGER, so Hibernate may join the college in the first query:

SELECT s.roll, s.name, s.marks, s.college_id, c.id, c.name
FROM students s
LEFT JOIN colleges c ON s.college_id = c.id
WHERE s.roll = ?;

EAGER is the default but can cause performance issues if you're loading many students — every student triggers a join. In practice, most applications override this with fetch = FetchType.LAZY and load the college only when needed.

Common Pitfalls

  • Using CascadeType.ALL on many-to-one. This is dangerous. If you delete a student, Hibernate also deletes the associated college — along with any other students at that college, or leaving them orphaned. Use CascadeType.PERSIST and CascadeType.MERGE instead, and handle deletes separately.
  • Manually setting the ID. Don't call setRoll(1) when roll is generated. Let Hibernate assign it. Manual IDs defeat the purpose of generation and can cause conflicts.
  • Forgetting @JoinColumn. Hibernate will pick a default column name like college_id, which is usually fine. But being explicit avoids surprises in schema migrations.
  • Assuming EAGER is fine. @ManyToOne defaults to EAGER fetch. For a small example this is fine, but in production, loading a page of 50 students can trigger 50 extra queries (the "N+1 problem"). Set fetch = FetchType.LAZY and use a JOIN FETCH query when you actually need the college data.
  • Missing inverse side. If you need to navigate from College to its students, you'd add a @OneToMany(mappedBy = "college") field there. It's optional, but if the relationship needs to be navigated both ways, you have to add it.
  • Broken transaction order. Open the transaction before any persistence operation. Relying on Hibernate to auto-begin is fragile.
  • Forgetting try/catch/finally. Without it, an exception leaves the session open and the transaction dangling. Always roll back and close.
  • Deprecated MySQL driver class. com.mysql.jdbc.Driver was removed in Connector/J 8. Use com.mysql.cj.jdbc.Driver.
  • No dialect set. Hibernate can usually guess, but on some setups it defaults poorly. Set hibernate.dialect explicitly.

Wrapping Up

You now know how to map a many-to-one relationship in Hibernate using @ManyToOne. The key ideas are:

  • Put the @ManyToOne on the "many" side (the entity that has a foreign key column).
  • Use @JoinColumn to name the foreign key column explicitly.
  • Use cascade carefully — PERSIST and MERGE are usually safer than ALL.
  • Watch out for the eager fetch default. Override to LAZY if you don't always need the referenced object.

From here, you can extend the example with:

  • Bidirectional navigation. Add @OneToMany(mappedBy = "college") on College to iterate its students.
  • Custom fetch strategies. Use @EntityGraph or JOIN FETCH in queries to control what's loaded and avoid the N+1 problem.
  • Second-level cache. The College object is a good candidate for caching — it's small, rarely changes, and read frequently.
  • Composite foreign keys. If your "one" side has a composite key, use @JoinColumns (plural) with multiple @JoinColumn entries.

No comments: