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