Full Stack • Java • System Design • Cloud • AI Engineering

Hibernate2026-06-17

Hibernate Overview

Master Hibernate fundamentals with Mermaid diagrams covering object states, lifecycle, lazy loading, dirty checking, locking strategies, and entity configuration

Hibernate is an Object-Relational Mapping (ORM) framework that simplifies database operations by mapping Java objects to database tables. Understanding object states, lifecycle, and configuration is essential for effective Hibernate usage.

Hibernate Object States

stateDiagram-v2
    [*] --> Transient: new Object()
    Transient --> Persistent: save(), persist()
    Persistent --> Detached: close(), clear()
    Detached --> Persistent: update(), merge()
    Persistent --> Removed: delete(), remove()
    Removed --> [*]
    
    note right of Transient
        Not associated with Session
        Not in database
    end note
    
    note right of Persistent
        Associated with Session
        Synchronized with database
        Automatic dirty checking
    end note
    
    note right of Detached
        Was persistent
        Session closed
        Can be reattached
    end note

Key Points:

  • Transient: New object, never associated with Session, not in database
  • Persistent: Associated with Session, changes tracked and synchronized
  • Detached: Was persistent, Session closed, can be reattached later
  • Removed: Marked for deletion, will be deleted on flush/commit
  • State Transitions: Controlled by Session methods (save, update, delete, etc.)

Object States Code Example

// Transient State - new object, not associated with Session
Employee employee = new Employee();
employee.setName("John Doe");
employee.setSalary(50000.0);
// Object is transient - not tracked by Hibernate

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

// Persistent State - save() associates object with Session
session.save(employee);
// Now employee is persistent - changes will be tracked

// Modify persistent object
employee.setSalary(55000.0);
// Change automatically detected (dirty checking)
// Will be synchronized with database on flush/commit

tx.commit();
session.close();

// Detached State - Session closed, object no longer tracked
employee.setSalary(60000.0);
// This change NOT tracked - object is detached

// Reattach detached object
Session session2 = sessionFactory.openSession();
Transaction tx2 = session2.beginTransaction();

// Merge detached object back to persistent state
Employee managedEmployee = (Employee) session2.merge(employee);
// Now changes are tracked again

tx2.commit();
session2.close();

Hibernate Object Lifecycle

sequenceDiagram
    participant App as Application
    participant Session as Hibernate Session
    participant Cache as First-Level Cache
    participant DB as Database
    
    App->>Session: new Employee()
    Note over App: Transient State
    
    App->>Session: session.save(employee)
    Session->>Cache: Add to cache
    Session->>DB: INSERT (on flush)
    Note over Session,Cache: Persistent State
    
    App->>Session: employee.setSalary(60000)
    Note over Cache: Dirty checking active
    
    Session->>DB: UPDATE (on flush/commit)
    
    App->>Session: session.close()
    Note over App: Detached State
    
    App->>Session: session2.update(employee)
    Session->>Cache: Add to cache
    Note over Session,Cache: Persistent again

Key Points:

  • Transient → Persistent: save(), persist(), saveOrUpdate()
  • Persistent → Detached: close(), clear(), evict()
  • Detached → Persistent: update(), merge(), lock()
  • Persistent → Removed: delete(), remove()
  • Automatic Synchronization: Persistent objects sync with DB on flush/commit

Lifecycle Management Code

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

// Create and persist
Employee emp = new Employee("Jane Smith", 50000);
session.save(emp); // Transient → Persistent
Long id = emp.getId(); // ID available after save

// Load existing entity
Employee existing = session.get(Employee.class, id);
// Object is persistent, changes tracked

// Detach object
session.evict(existing); // Persistent → Detached
existing.setSalary(55000); // Change not tracked

// Reattach
session.update(existing); // Detached → Persistent
// Now change will be saved

tx.commit();
session.close();

// Working with detached objects
Employee detached = new Employee();
detached.setId(id);
detached.setName("Updated Name");

Session session2 = sessionFactory.openSession();
Transaction tx2 = session2.beginTransaction();

// merge() returns persistent copy
Employee persistent = (Employee) session2.merge(detached);
// detached object remains detached
// persistent object is managed

tx2.commit();
session2.close();

Lazy Loading

sequenceDiagram
    participant App as Application
    participant Session as Session
    participant Proxy as Proxy Object
    participant DB as Database
    
    App->>Session: load(Employee.class, id)
    Session->>Proxy: Create proxy
    Proxy->>App: Return proxy
    Note over Proxy: Proxy created, no DB hit
    
    App->>Proxy: employee.getName()
    Proxy->>Session: Initialize proxy
    Session->>DB: SELECT employee
    DB->>Session: Return data
    Session->>Proxy: Populate real object
    Proxy->>App: Return name
    
    Note over App,DB: Subsequent calls use loaded data

Key Points:

  • Proxy Objects: Hibernate creates proxy instead of loading actual object
  • Initialization: Real object loaded when property accessed
  • Session Required: Must have active Session to initialize proxy
  • LazyInitializationException: Thrown if Session closed before access
  • Fetch Strategies: LAZY (default for collections), EAGER (immediate load)

Lazy Loading Code Example

// Entity with lazy associations
@Entity
public class Employee {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    // Lazy loading (default for @ManyToOne)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "department_id")
    private Department department;
    
    // Lazy loading (default for collections)
    @OneToMany(mappedBy = "employee", fetch = FetchType.LAZY)
    private List<Project> projects;
    
    // Getters and setters
}

// Using lazy loading
Session session = sessionFactory.openSession();

// Load employee - department and projects NOT loaded
Employee emp = session.get(Employee.class, 1L);
System.out.println(emp.getName()); // Works - name is loaded

// Access lazy association - triggers DB query
Department dept = emp.getDepartment();
System.out.println(dept.getName()); // Loads department now

// Access lazy collection - triggers DB query
List<Project> projects = emp.getProjects();
System.out.println(projects.size()); // Loads projects now

session.close();

// LazyInitializationException example
try {
    // Session closed, can't initialize lazy proxy
    String deptName = emp.getDepartment().getName();
} catch (LazyInitializationException e) {
    System.out.println("Session closed - can't load lazy data");
}

// Solution: Use JOIN FETCH
Session session2 = sessionFactory.openSession();
Employee empWithDept = session2.createQuery(
    "FROM Employee e JOIN FETCH e.department WHERE e.id = :id",
    Employee.class)
    .setParameter("id", 1L)
    .uniqueResult();
session2.close();

// Now department is loaded, no LazyInitializationException
System.out.println(empWithDept.getDepartment().getName());

Automatic Dirty Checking

flowchart TB
    A[Persistent Object Modified] --> B[Session.flush or commit]
    B --> C[Hibernate Checks PersistenceContext]
    C --> D{Object Modified?}
    D -->|Yes| E[Compare with snapshot]
    D -->|No| F[No action]
    E --> G[Generate UPDATE SQL]
    G --> H[Execute UPDATE]
    H --> I[Update snapshot]
    
    style D fill:#FF9800
    style G fill:#4CAF50
    style F fill:#2196F3

Key Points:

  • Automatic Detection: Hibernate tracks changes to persistent objects
  • Snapshot Comparison: Compares current state with loaded state
  • Flush Time: Updates generated during flush() or commit()
  • No Manual Updates: Don't need to call update() for persistent objects
  • Performance: Only modified fields included in UPDATE statement

Dirty Checking Code Example

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

// Load persistent object
Employee emp = session.get(Employee.class, 1L);
System.out.println("Original salary: " + emp.getSalary());

// Modify persistent object - NO update() call needed
emp.setSalary(60000.0);
emp.setName("John Updated");

// Hibernate automatically detects changes
// UPDATE generated on commit
tx.commit(); // Dirty checking happens here
session.close();

// Hibernate generates:
// UPDATE employee SET salary=60000.0, name='John Updated' WHERE id=1

// Disable dirty checking for specific session
Session session2 = sessionFactory.openSession();
session2.setDefaultReadOnly(true); // Read-only session
Transaction tx2 = session2.beginTransaction();

Employee emp2 = session2.get(Employee.class, 1L);
emp2.setSalary(70000.0); // Change ignored - read-only

tx2.commit(); // No UPDATE generated
session2.close();

// Selective dirty checking
Session session3 = sessionFactory.openSession();
Transaction tx3 = session3.beginTransaction();

Employee emp3 = session3.get(Employee.class, 1L);
session3.setReadOnly(emp3, true); // Make this object read-only

emp3.setSalary(80000.0); // Change ignored

tx3.commit(); // No UPDATE for emp3
session3.close();

Locking Strategies

graph TB
    A[Locking Strategies] --> B[Optimistic Locking]
    A --> C[Pessimistic Locking]
    
    B --> B1[Version-based]
    B --> B2[Timestamp-based]
    B --> B3[No database locks]
    B --> B4[Check on commit]
    
    C --> C1[Database locks]
    C --> C2[Lock on read]
    C --> C3[Prevents concurrent access]
    C --> C4[Can cause deadlocks]
    
    style A fill:#2196F3
    style B fill:#4CAF50
    style C fill:#FF9800

Key Points:

  • Optimistic: Assumes conflicts rare, checks version on update
  • Pessimistic: Locks record immediately, prevents concurrent access
  • Version Field: @Version annotation for optimistic locking
  • Isolation Levels: Control transaction isolation for pessimistic locking
  • Trade-offs: Optimistic better for read-heavy, pessimistic for write-heavy

Locking Code Examples

// Optimistic Locking with @Version
@Entity
public class Employee {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    private Double salary;
    
    // Version field for optimistic locking
    @Version
    private Long version;
    
    // Or use timestamp
    // @Version
    // private Timestamp lastModified;
    
    // Getters and setters
}

// Optimistic locking in action
Session session1 = sessionFactory.openSession();
Transaction tx1 = session1.beginTransaction();

Employee emp1 = session1.get(Employee.class, 1L);
System.out.println("Version: " + emp1.getVersion()); // Version: 0

// Another session loads same employee
Session session2 = sessionFactory.openSession();
Transaction tx2 = session2.beginTransaction();

Employee emp2 = session2.get(Employee.class, 1L);
System.out.println("Version: " + emp2.getVersion()); // Version: 0

// Session 1 updates and commits
emp1.setSalary(60000.0);
tx1.commit(); // Version incremented to 1
session1.close();

// Session 2 tries to update - will fail
emp2.setSalary(65000.0);
try {
    tx2.commit(); // Throws OptimisticLockException
} catch (OptimisticLockException e) {
    System.out.println("Concurrent modification detected!");
    tx2.rollback();
}
session2.close();

// Pessimistic Locking
Session session3 = sessionFactory.openSession();
Transaction tx3 = session3.beginTransaction();

// Lock record for update (SELECT ... FOR UPDATE)
Employee emp3 = session3.get(Employee.class, 1L, 
    LockMode.PESSIMISTIC_WRITE);

// Other sessions blocked until this transaction completes
emp3.setSalary(70000.0);

tx3.commit(); // Lock released
session3.close();

// Different lock modes
Session session4 = sessionFactory.openSession();
Transaction tx4 = session4.beginTransaction();

// PESSIMISTIC_READ - shared lock
Employee emp4 = session4.get(Employee.class, 1L,
    LockMode.PESSIMISTIC_READ);

// PESSIMISTIC_WRITE - exclusive lock
Employee emp5 = session4.get(Employee.class, 2L,
    LockMode.PESSIMISTIC_WRITE);

// PESSIMISTIC_FORCE_INCREMENT - lock and increment version
Employee emp6 = session4.get(Employee.class, 3L,
    LockMode.PESSIMISTIC_FORCE_INCREMENT);

tx4.commit();
session4.close();

Entity Configuration

// Complete entity configuration example
@Entity
@Table(name = "employees")
@org.hibernate.annotations.Entity(selectBeforeUpdate = true)
@NamedQueries({
    @NamedQuery(
        name = "Employee.findByDepartment",
        query = "FROM Employee e WHERE e.department.id = :deptId"
    ),
    @NamedQuery(
        name = "Employee.findHighEarners",
        query = "FROM Employee e WHERE e.salary > :minSalary ORDER BY e.salary DESC"
    )
})
@NamedNativeQueries({
    @NamedNativeQuery(
        name = "Employee.avgSalaryByDept",
        query = "SELECT department_id, AVG(salary) FROM employees GROUP BY department_id",
        resultSetMapping = "DeptSalaryMapping"
    )
})
public class Employee {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "employee_id")
    private Long id;
    
    @Column(name = "emp_name", nullable = false, length = 100)
    private String name;
    
    @Column(name = "salary", precision = 10, scale = 2)
    private Double salary;
    
    @Column(name = "emp_type")
    @Enumerated(EnumType.STRING)
    private EmployeeType type;
    
    // Version for optimistic locking
    @Version
    private Long version;
    
    // Audit fields
    @Column(name = "created_at", updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdAt;
    
    @Column(name = "updated_at")
    @Temporal(TemporalType.TIMESTAMP)
    private Date updatedAt;
    
    // Transient field - not persisted
    @Transient
    private Double salaryWithBonus;
    
    // Calculated field - read-only
    @Formula("salary * 1.1")
    private Double salaryWithRaise;
    
    // Many-to-One relationship
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "department_id")
    private Department department;
    
    // One-to-Many relationship
    @OneToMany(mappedBy = "employee", 
               cascade = CascadeType.ALL,
               orphanRemoval = true)
    private List<Project> projects = new ArrayList<>();
    
    // Lifecycle callbacks
    @PrePersist
    protected void onCreate() {
        createdAt = new Date();
        updatedAt = new Date();
    }
    
    @PreUpdate
    protected void onUpdate() {
        updatedAt = new Date();
    }
    
    // Getters and setters
    // equals() and hashCode() based on business key
}

// Enum type
public enum EmployeeType {
    PERMANENT, CONTRACT, INTERN
}

Best Practices

  1. Use Appropriate Fetch Strategy: LAZY for collections, EAGER sparingly
  2. Implement equals() and hashCode(): Based on business key, not ID
  3. Version Fields: Always use @Version for optimistic locking
  4. Avoid LazyInitializationException: Load data within transaction or use JOIN FETCH
  5. Batch Operations: Use flush() and clear() for large batch processing
  6. Named Queries: Define frequently used queries as named queries
  7. Cascade Carefully: Understand cascade types before using
  8. Transaction Boundaries: Keep transactions short and focused