Hibernate CRUD Tutorial

Hibernate CRUD Example: Insert, Update, Delete, Fetch (2026) | D.Tech Academic

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.AUTO to GenerationType.IDENTITY — IDENTITY matches MySQL's AUTO_INCREMENT explicitly 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 Criteria API. The original used session.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 then session.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 Criteria with typed Query<Teacher>. This gives compile-time type safety instead of raw List.

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 3 fetched the teacher, then called t.update(tt) — but the update method in Test passed 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 simple while (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 times scanner.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, and id at 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 Criteria API. session.createCriteria() was deprecated in Hibernate 5.2 and removed in Hibernate 6. Migrate to HQL or the JPA CriteriaBuilder API.
  • Opening a session without closing it. Every openSession() must be matched with a close() in a finally block. Otherwise connections leak from the pool and the app eventually hangs.
  • Forgetting to start a transaction. In Hibernate 5.x, save(), update(), and delete() 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. Use saveOrUpdate() or merge() for entities that may or may not be persisted.
  • Calling update() on a nonexistent row. Hibernate throws StaleStateException because 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". Use nextLine() if you expect spaces in the input.
  • Deprecated MySQL driver class. com.mysql.jdbc.Driver was removed in Connector/J 8. Use com.mysql.cj.jdbc.Driver.
  • Missing dialect. Hibernate can usually guess, but on some setups it warns and defaults poorly. Set hibernate.dialect explicitly.

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() and setMaxResults() to your queries to fetch a page at a time.
  • Search filters. Build HQL queries with WHERE clauses 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: