CRUD Operations with Hibernate (Create, Read, Update, Delete)

This is a hands-on lesson demonstrating how to perform basic CRUD operations using Hibernate. You'll learn how to save new objects to the database (Create), retrieve objects (Read), modify existing objects (Update), and delete objects (Delete) using Hibernate's API.


Creating (Saving) Objects with Hibernate

Hibernate is an Object-Relational Mapping (ORM) framework for Java, simplifying database interaction by mapping Java objects to database tables. This document explains how to save new objects to the database using Hibernate, focusing on session.save() and session.persist(), and outlining transaction management.

Saving New Objects with `session.save()` and `session.persist()`

Hibernate provides two primary methods for saving new objects to the database: session.save() and session.persist(). While they often appear similar, there are subtle differences in their behavior, especially within the context of transactions.

`session.save()`

The save() method is used to save a transient object to the database. It assigns a unique identifier (primary key) to the object and makes it persistent. save() returns the generated identifier.

Example:

 import org.hibernate.Session;
import org.hibernate.Transaction;

public class HibernateSaveExample {

    public static void main(String[] args) {
        try (Session session = HibernateUtil.getSessionFactory().openSession()) {
            Transaction transaction = null;
            try {
                transaction = session.beginTransaction();

                // Create a new object
                Student student = new Student();
                student.setFirstName("John");
                student.setLastName("Doe");
                student.setEmail("john.doe@example.com");

                // Save the object
                Long studentId = (Long) session.save(student);

                System.out.println("Saved student with ID: " + studentId);

                // Commit the transaction
                transaction.commit();

            } catch (Exception e) {
                if (transaction != null) {
                    transaction.rollback();
                }
                e.printStackTrace();
            }
        }
    }
}

// Student class (example)
class Student {
    private Long id;
    private String firstName;
    private String lastName;
    private String email;

    // Getters and setters (omitted for brevity)
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getFirstName() { return firstName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
     public String getLastName() { return lastName; }
    public void setLastName(String lastName) { this.lastName = lastName; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}


// HibernateUtil (example)
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

public class HibernateUtil {

    private static final SessionFactory sessionFactory = buildSessionFactory();

    private static SessionFactory buildSessionFactory() {
        try {
            // Create the SessionFactory from hibernate.cfg.xml
            return new Configuration().configure().buildSessionFactory();
        } catch (Throwable ex) {
            // Make sure you log the exception, as it might be swallowed
            System.err.println("Initial SessionFactory creation failed." + ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    public static SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    public static void shutdown() {
        // Close caches and connection pools
        getSessionFactory().close();
    }

} 

Key points about save():

  • It returns the generated identifier immediately.
  • It guarantees that the object is inserted into the database at some point, but not necessarily immediately. The actual insertion might be delayed until the transaction is committed (or even later by Hibernate's batch processing).
  • The object becomes associated with the persistence context (the Session).

`session.persist()`

The persist() method also saves a transient object to the database and makes it persistent. However, unlike save(), persist() does not guarantee that the identifier will be assigned immediately. The identifier assignment might be delayed until the transaction is committed.

Example:

 import org.hibernate.Session;
import org.hibernate.Transaction;

public class HibernatePersistExample {

    public static void main(String[] args) {
        try (Session session = HibernateUtil.getSessionFactory().openSession()) {
            Transaction transaction = null;
            try {
                transaction = session.beginTransaction();

                // Create a new object
                Student student = new Student();
                student.setFirstName("Jane");
                student.setLastName("Smith");
                student.setEmail("jane.smith@example.com");

                // Persist the object
                session.persist(student);

                // At this point, student.getId() might be null!
                // System.out.println("Persisted student with ID: " + student.getId()); // Avoid this if using identity generation

                // Commit the transaction
                transaction.commit();

                // After commit, student.getId() will be populated (if using identity generation)
                System.out.println("Persisted student with ID: " + student.getId());


            } catch (Exception e) {
                if (transaction != null) {
                    transaction.rollback();
                }
                e.printStackTrace();
            }
        }
    }
} 

Key points about persist():

  • It does not return the generated identifier immediately. Avoid trying to access the ID immediately after calling persist(), especially if using an "identity" generation strategy (where the database assigns the ID).
  • It follows the JPA (Java Persistence API) standard more closely.
  • Like save(), the actual insertion might be delayed.
  • The object also becomes associated with the persistence context.

Differences and When to Use Which

In most common scenarios, save() and persist() behave identically. The primary difference lies in their behavior regarding identifier assignment and adherence to JPA standards.

  • Use save() when you need the identifier immediately after saving the object. However, be aware that this can force Hibernate to execute an INSERT statement immediately, potentially reducing performance if you're saving many objects in a batch.
  • Use persist() when you don't need the identifier immediately or when adhering to JPA standards is a priority. This allows Hibernate to optimize database interactions and potentially delay identifier assignment until the transaction is committed.

Important: Both save() and persist() only make the object *persistent*. The changes are not committed to the database until the transaction is committed.

Understanding Hibernate's Transaction Management

Transaction management is crucial for maintaining data integrity when interacting with databases. Hibernate relies on transactions to ensure that a series of database operations are treated as a single atomic unit of work. If any part of the transaction fails, the entire transaction is rolled back, preventing inconsistent data.

Transaction Boundaries

A transaction defines a logical unit of work. It starts when you begin a new transaction (e.g., using session.beginTransaction()) and ends when you either commit the transaction (using transaction.commit()) or roll it back (using transaction.rollback()).

ACID Properties

Transactions guarantee the ACID properties:

  • Atomicity: All operations within the transaction succeed or fail as a single unit. There's no partial execution.
  • Consistency: The transaction moves the database from one valid state to another. It enforces integrity constraints.
  • Isolation: Transactions are isolated from each other. Concurrent transactions do not interfere with each other's data. Hibernate uses various isolation levels (e.g., read committed, repeatable read) to control the degree of isolation.
  • Durability: Once a transaction is committed, the changes are permanent and survive even system failures.

Using Transactions in Hibernate

Here's how you typically manage transactions in Hibernate:

 import org.hibernate.Session;
import org.hibernate.Transaction;

public class HibernateTransactionExample {

    public static void main(String[] args) {
        try (Session session = HibernateUtil.getSessionFactory().openSession()) { // Use try-with-resources to ensure session is closed
            Transaction transaction = null;
            try {
                // 1. Begin a new transaction
                transaction = session.beginTransaction();

                // 2. Perform database operations (e.g., save, update, delete)
                Student student = new Student();
                student.setFirstName("Alice");
                student.setLastName("Johnson");
                student.setEmail("alice.johnson@example.com");
                session.save(student);


                // 3. Commit the transaction if all operations succeed
                transaction.commit();
                System.out.println("Transaction committed successfully.");

            } catch (Exception e) {
                // 4. Roll back the transaction if any operation fails
                if (transaction != null) {
                    transaction.rollback();
                    System.err.println("Transaction rolled back due to an error.");
                }
                e.printStackTrace(); // Log the exception
            }
        } // Session is automatically closed here
    }
} 

Explanation:

  1. **Begin a transaction:** The session.beginTransaction() method starts a new transaction.
  2. **Perform database operations:** You then perform your database operations (e.g., saving, updating, deleting objects) within the transaction.
  3. **Commit the transaction:** If all operations are successful, you call transaction.commit() to permanently save the changes to the database. Hibernate then synchronizes the changes with the database.
  4. **Roll back the transaction:** If any exception occurs during the database operations, you call transaction.rollback() to discard all changes made within the transaction. This reverts the database to its state before the transaction started.

It is crucial to always wrap your Hibernate operations within a transaction to ensure data integrity. Using a try-catch block with rollback in the catch block is best practice.

Resource Management

Always ensure that you properly close the Hibernate Session when you're finished with it to release resources. The try-with-resources statement is the recommended way to ensure the session is closed automatically. For example:

 try (Session session = HibernateUtil.getSessionFactory().openSession()) {
    // Use the session
} // Session is automatically closed here 

Failure to close the session can lead to resource leaks and performance issues.

Example of Exception and Rollback

 import org.hibernate.Session;
import org.hibernate.Transaction;

public class HibernateRollbackExample {

    public static void main(String[] args) {
        try (Session session = HibernateUtil.getSessionFactory().openSession()) {
            Transaction transaction = null;
            try {
                transaction = session.beginTransaction();

                // Attempt to save a student with invalid data (e.g., null email)
                Student student = new Student();
                student.setFirstName("Error");
                student.setLastName("Prone");
                student.setEmail(null); // Intentionally set to null, violating constraint
                session.save(student);

                // Commit the transaction (this will likely fail due to the null email)
                transaction.commit(); // This might throw an exception

                System.out.println("Transaction committed successfully."); // This might not be reached

            } catch (Exception e) {
                if (transaction != null) {
                    transaction.rollback();
                    System.err.println("Transaction rolled back due to an error: " + e.getMessage());
                }
                e.printStackTrace(); // Log the full exception
            }
        }
    }
} 

In this example, if saving the student with a null email violates a database constraint (e.g., NOT NULL), the commit() operation will throw an exception. The catch block will then execute, rolling back the transaction and preventing the invalid data from being persisted.