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
Collegedoesn't need to know about its students for a many-to-one mapping — the foreign key lives on thestudentstable. - The original imported
javax.persistence.OneToOnebut 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 aStudentsalso saves the associatedCollegeif it's not yet persisted.@JoinColumn(name = "college_id")— names the foreign key column. Without it, Hibernate uses a default likecollege_idanyway, but being explicit is good practice.- Unused import removed: the original had
import java.util.List;that wasn't used. - Cascade warning:
CascadeType.ALLincludesREMOVE, which means deleting aStudentsrow would also delete the associatedCollege— and any other students at that college would be orphaned. For a many-to-one, you usually wantPERSISTandMERGE, notALL. I've keptALLto 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)ands2.setRoll(2)even thoughrollis 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
txttotxfor 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
studentstable has acollege_idcolumn — that's the foreign key. - The
collegestable 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):
- Hibernate checks the cascade setting. Because
CascadeType.ALLis set on the college field, it detects that theCollegeis unsaved. - It inserts the college first:
INSERT INTO colleges (name) VALUES ('A.B.C'). - It reads back the generated
id(say, 1). - It inserts
s1withcollege_id = 1.
When you call session.save(s2):
- The cascade setting triggers again, but Hibernate sees that the
Collegeis already persisted (it has an ID). - It inserts
s2with the samecollege_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.ALLon 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. UseCascadeType.PERSISTandCascadeType.MERGEinstead, and handle deletes separately. - Manually setting the ID. Don't call
setRoll(1)whenrollis 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 likecollege_id, which is usually fine. But being explicit avoids surprises in schema migrations. - Assuming EAGER is fine.
@ManyToOnedefaults 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"). Setfetch = FetchType.LAZYand use aJOIN FETCHquery when you actually need the college data. - Missing inverse side. If you need to navigate from
Collegeto 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.Driverwas removed in Connector/J 8. Usecom.mysql.cj.jdbc.Driver. - No dialect set. Hibernate can usually guess, but on some setups it defaults poorly. Set
hibernate.dialectexplicitly.
Wrapping Up
You now know how to map a many-to-one relationship in Hibernate using @ManyToOne. The key ideas are:
- Put the
@ManyToOneon the "many" side (the entity that has a foreign key column). - Use
@JoinColumnto name the foreign key column explicitly. - Use cascade carefully —
PERSISTandMERGEare usually safer thanALL. - Watch out for the eager fetch default. Override to
LAZYif you don't always need the referenced object.
From here, you can extend the example with:
- Bidirectional navigation. Add
@OneToMany(mappedBy = "college")onCollegeto iterate its students. - Custom fetch strategies. Use
@EntityGraphorJOIN FETCHin queries to control what's loaded and avoid the N+1 problem. - Second-level cache. The
Collegeobject 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@JoinColumnentries.
No comments:
Post a Comment