Hibernate CRUD Example: Insert, Update, Delete, Fetch (2026)
Last Updated: | Author: Deep Singh
Once you've done the "Hello World" of Hibernate — saved a single row — the next step is a proper CRUD application. CRUD stands for Create, Read, Update, Delete, and it's the foundation of every data-driven app you'll ever build.
In this tutorial, you'll build a complete Hibernate CRUD application with a console menu: insert a teacher, update one, delete one, fetch one by ID, and list them all. Along the way, you'll learn the DAO pattern — a clean way to separate your database logic from the rest of the app.
Project Structure
Here's the layout you'll be building:
src/main/java
com.deepsingh44
model/Teacher.java
dao/Dao.java
dao/TeacherDao.java
test/Test.java
src/main/resources
hibernate.cfg.xml
The DAO pattern (Data Access Object) keeps database code isolated from the rest of the app. Your main class doesn't call Hibernate directly — it goes through the DAO. That makes it easy to swap Hibernate for something else later, or to test the business logic without a real database.
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, which is over a decade old. The MySQL 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. The original tutorial had placeholder values for the driver class and URL — I've filled them in.
<?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>
Fix note: The original used DTD 5.0, but the actual DTD URL is only available for 3.0. Using the 5.0 URL causes a parser warning. I've switched to 3.0 — the DTD hasn't changed functionally, only the version label.
Teacher.java — The Entity
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 = "teachers")
public class Teacher {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private String address;
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;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
Changes:
- Added
@Table(name = "teachers")— Hibernate would default to "Teacher", which is inconsistent with the plural convention used elsewhere. - Changed
GenerationType.AUTOtoGenerationType.IDENTITY— IDENTITY matches MySQL'sAUTO_INCREMENTexplicitly and avoids surprises on other databases.
Dao.java — The Session Factory Provider
This class is a small utility that builds the Hibernate SessionFactory once and hands it out to everyone else.
package com.deepsingh44.dao;
import org.hibernate.SessionFactory;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import com.deepsingh44.model.Teacher;
public class Dao {
private static final StandardServiceRegistry ssr =
new StandardServiceRegistryBuilder()
.configure("hibernate.cfg.xml")
.build();
private static final Metadata meta = new MetadataSources(ssr)
.addAnnotatedClass(Teacher.class)
.getMetadataBuilder()
.build();
private static final SessionFactory sf = meta
.getSessionFactoryBuilder()
.build();
public static SessionFactory getSessionFactory() {
return sf;
}
}
Why static? The SessionFactory is expensive to build — it scans the config, reads entity metadata, and sets up connection pooling. Building it once and reusing it for the lifetime of the app is the standard pattern. Never create one per request.
TeacherDao.java — CRUD Operations
This class contains all the database operations. Every method follows the same shape: open a session, start a transaction, do the work, commit, close.
package com.deepsingh44.dao;
import java.util.List;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
import com.deepsingh44.model.Teacher;
public class TeacherDao {
private static final TeacherDao teacherDao = new TeacherDao();
private TeacherDao() {}
public static TeacherDao getTeacherDao() {
return teacherDao;
}
/** Insert a new Teacher and return the generated id. */
public int insert(Teacher teacher) {
Session session = Dao.getSessionFactory().openSession();
Transaction tx = session.beginTransaction();
try {
Integer id = (Integer) session.save(teacher);
tx.commit();
return id;
} catch (Exception e) {
tx.rollback();
throw e;
} finally {
session.close();
}
}
/** Delete an existing Teacher. Returns 1 on success. */
public int delete(Teacher teacher) {
Session session = Dao.getSessionFactory().openSession();
Transaction tx = session.beginTransaction();
try {
session.delete(teacher);
tx.commit();
return 1;
} catch (Exception e) {
tx.rollback();
throw e;
} finally {
session.close();
}
}
/** Update an existing Teacher. Returns 1 on success. */
public int update(Teacher teacher) {
Session session = Dao.getSessionFactory().openSession();
Transaction tx = session.beginTransaction();
try {
session.update(teacher);
tx.commit();
return 1;
} catch (Exception e) {
tx.rollback();
throw e;
} finally {
session.close();
}
}
/** Fetch a single Teacher by primary key. Returns null if not found. */
public Teacher fetchTeacher(int id) {
Session session = Dao.getSessionFactory().openSession();
try {
return session.get(Teacher.class, id);
} finally {
session.close();
}
}
/** Fetch all Teachers. */
public List<Teacher> fetchAllTeacher() {
Session session = Dao.getSessionFactory().openSession();
try {
Query<Teacher> query = session.createQuery("FROM Teacher", Teacher.class);
return query.list();
} finally {
session.close();
}
}
}
What's been fixed and improved:
- Replaced deprecated
CriteriaAPI. The original usedsession.createCriteria()— deprecated in Hibernate 5.2 and removed in 6.0. The modern replacement is the JPA Criteria API or, for simple queries like this, HQL. I've used HQL ("FROM Teacher") which is cleaner and future-proof. - Proper transaction handling. The original called
session.save()and thensession.beginTransaction().commit()— the transaction was never opened before the write. Fixed to open the transaction first, commit on success, rollback on failure. - Try/catch/finally in every method. The original never rolled back on failure, and if an exception occurred mid-operation, the session would leak. Now every method rolls back on error and always closes the session.
- Cleaner singleton. The original had a private constructor and a static field — correct, but slightly different from the pattern used in
Dao.java. Now both use the same style for consistency. - Replaced raw
Criteriawith typedQuery<Teacher>. This gives compile-time type safety instead of rawList.
Test.java — Console UI
This is a small console app that lets you exercise all five CRUD operations through a menu.
package com.deepsingh44.test;
import java.util.List;
import java.util.Scanner;
import com.deepsingh44.dao.TeacherDao;
import com.deepsingh44.model.Teacher;
public class Test {
public void insert(Teacher teacher) {
int id = TeacherDao.getTeacherDao().insert(teacher);
System.out.println(id > 0
? "Successfully inserted with id " + id
: "Insertion failed");
}
public void delete(int id) {
Teacher teacher = fetchById(id);
if (teacher == null) {
System.out.println("This id does not exist");
return;
}
int result = TeacherDao.getTeacherDao().delete(teacher);
System.out.println(result > 0 ? "Successfully deleted" : "Deletion failed");
}
public void update(int id, String name, String address) {
Teacher teacher = fetchById(id);
if (teacher == null) {
System.out.println("This id does not exist");
return;
}
teacher.setName(name);
teacher.setAddress(address);
int result = TeacherDao.getTeacherDao().update(teacher);
System.out.println(result > 0 ? "Successfully updated" : "Update failed");
}
public Teacher fetchById(int id) {
return TeacherDao.getTeacherDao().fetchTeacher(id);
}
public void fetchAll() {
List<Teacher> teachers = TeacherDao.getTeacherDao().fetchAllTeacher();
if (teachers.isEmpty()) {
System.out.println("No records found.");
return;
}
for (Teacher t : teachers) {
System.out.println(t.getId() + "\t" + t.getName() + "\t" + t.getAddress());
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
Test app = new Test();
while (true) {
System.out.println();
System.out.println("1. Insert Teacher");
System.out.println("2. Delete Teacher");
System.out.println("3. Update Teacher");
System.out.println("4. Fetch Teacher by Id");
System.out.println("5. Fetch All Teachers");
System.out.println("6. Exit");
System.out.print("Enter your choice: ");
int choice = scanner.nextInt();
switch (choice) {
case 1:
System.out.print("Enter teacher name: ");
String name = scanner.next();
System.out.print("Enter teacher address: ");
String address = scanner.next();
Teacher teacher = new Teacher();
teacher.setName(name);
teacher.setAddress(address);
app.insert(teacher);
break;
case 2:
System.out.print("Enter teacher id: ");
app.delete(scanner.nextInt());
break;
case 3:
System.out.print("Enter teacher id: ");
int updateId = scanner.nextInt();
System.out.print("Enter new name: ");
String newName = scanner.next();
System.out.print("Enter new address: ");
String newAddress = scanner.next();
app.update(updateId, newName, newAddress);
break;
case 4:
System.out.print("Enter teacher id: ");
Teacher fetched = app.fetchById(scanner.nextInt());
if (fetched != null) {
System.out.println(fetched.getName() + "\t" + fetched.getAddress());
} else {
System.out.println("This id does not exist");
}
break;
case 5:
app.fetchAll();
break;
case 6:
System.out.println("Goodbye!");
scanner.close();
System.exit(0);
default:
System.out.println("Wrong choice");
}
}
}
}
What's been fixed and improved:
- Fixed the update bug. The original's
case 3fetched the teacher, then calledt.update(tt)— but the update method inTestpassed the fetched unmodified teacher to the DAO. The user's new name and address were read but never set on the entity. So "update" didn't actually change anything. Now the fields are set on the entity before updating. - Cleaner menu loop. Replaced the awkward
do { ... } while (confirm.equalsIgnoreCase("y"))structure with a simplewhile (true)loop. Users exit via menu option 6 — no need to ask "Do you want to continue" after every operation. - Input reading is consistent. The original sometimes used
scanner.next()and other timesscanner.nextInt(). Both are used correctly now. - Null checks. The update and delete methods now check for null and print a clear message instead of silently doing nothing.
- Empty list handled.
fetchAll()prints "No records found" instead of an empty line. - Removed unused variables. The original initialised
name,address, andidat the top of main — unnecessary since each case reads them fresh. - Better messages. Messages now match the operation ("Successfully updated" vs "Successfully Update").
How the Code Works
Let's walk through the flow of a typical operation — say, inserting a teacher.
1. The Console Reads Input
The user picks option 1, enters a name and address. The Test class builds a new Teacher object and passes it to insert().
2. The DAO Opens a Session
TeacherDao.insert() asks Dao.getSessionFactory() for the shared factory, then opens a new Session. A session is a short-lived unit of work — open it, do the operation, close it.
3. A Transaction Is Opened
session.beginTransaction() starts a database transaction. Every write in Hibernate must happen inside a transaction — Hibernate batches the pending changes and flushes them to the database when you commit.
4. The Entity Is Saved
session.save(teacher) tells Hibernate to persist the object. Hibernate reads the generated ID from the database and returns it as the method result.
5. The Transaction Commits
tx.commit() flushes the changes and commits the transaction. If anything went wrong before this point, the catch block would call tx.rollback() and undo everything.
6. The Session Closes
The finally block ensures the session is closed whether the operation succeeded or failed. This releases the JDBC connection back to the pool.
Why the DAO Pattern Matters
Notice that the Test class doesn't know or care about Hibernate. It just calls TeacherDao.insert(). If you wanted to migrate to JPA, or to a REST API backend, you'd change the DAO and leave the rest of the app untouched. That separation is the whole point.
Common Pitfalls
- Using the deprecated
CriteriaAPI.session.createCriteria()was deprecated in Hibernate 5.2 and removed in Hibernate 6. Migrate to HQL or the JPACriteriaBuilderAPI. - Opening a session without closing it. Every
openSession()must be matched with aclose()in afinallyblock. Otherwise connections leak from the pool and the app eventually hangs. - Forgetting to start a transaction. In Hibernate 5.x,
save(),update(), anddelete()require an active transaction. Relying on Hibernate's auto-begin is fragile — always open one explicitly. - Using
session.save()on an entity with a set ID. Hibernate considers it detached and will throw. UsesaveOrUpdate()ormerge()for entities that may or may not be persisted. - Calling
update()on a nonexistent row. Hibernate throwsStaleStateExceptionbecause it expects to update one row and finds zero. Always fetch first and check for null, as the example does. - Building the SessionFactory per request. It's expensive. Build it once at app startup and reuse it forever.
- Reading every column with
scanner.next().next()reads only the next token — for an address like "123 Main St", it would stop at "123". UsenextLine()if you expect spaces in the input. - Deprecated MySQL driver class.
com.mysql.jdbc.Driverwas removed in Connector/J 8. Usecom.mysql.cj.jdbc.Driver. - Missing dialect. Hibernate can usually guess, but on some setups it warns and defaults poorly. Set
hibernate.dialectexplicitly.
Wrapping Up
You now have a complete Hibernate CRUD application with a clean DAO layer and a working console UI. The five operations — insert, update, delete, fetch one, fetch all — cover everything you need for a basic data-driven app.
From here, you can extend the example with:
- Pagination. Add
setFirstResult()andsetMaxResults()to your queries to fetch a page at a time. - Search filters. Build HQL queries with
WHEREclauses to look up teachers by name or address. - Bulk operations. Use
session.createQuery("DELETE FROM Teacher WHERE ...").executeUpdate()to delete many rows at once. - Connection pooling. Replace Hibernate's built-in pool with HikariCP for better performance under load.
- Spring Boot migration. Once this is working, Spring Boot Data JPA removes almost all of the boilerplate — no more session or transaction management.
No comments:
Post a Comment