Fundamentals — Cheat Sheet
Hibernate & JPA · 7 topics. Download the PDF or the Instagram carousel and share it.
JPA vs Hibernate
JPA is the Java specification (API + annotations); Hibernate is the most popular implementation. You code against JPA interfaces for portability but benefit from Hibernate-specific features.
- ✓JPA is the specification (annotations, EntityManager, JPQL); Hibernate is the implementation.
- ✓Code to JPA interfaces for portability; use Hibernate-specific features only when needed.
- ✓Spring Data JPA sits above JPA — JpaRepository generates queries using Hibernate under the hood.
- ✓@Entity, @Id, @ManyToOne, @Query — all JPA standard; @NaturalId, @Filter, @Cache — Hibernate-specific.
- ✓The full stack: Spring Data JPA → JPA (EntityManager) → Hibernate → HikariCP → DB.
- ✓spring.jpa.show-sql=true and format_sql=true are invaluable for debugging generated SQL.
// JPA-standard — works on any provider
import jakarta.persistence.*;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
}
// JPA standard EntityManager
@Repository
public class ProductJpaRepository {
@PersistenceContext
private EntityManager em;
public Optional<Product> findById(Long id) {
return Optional.ofNullable(em.find(Product.class, id));
}
}
// Hibernate-specific — tied to Hibernate
import org.hibernate.annotations.NaturalId;
import org.hibernate.annotations.BatchSize;
@Entity
public class Product {
@NaturalId // Hibernate-specific: cache by natural key
private String sku;
@BatchSize(size = 25) // Hibernate-specific: IN-clause batch loading
@OneToMany(mappedBy = "product")
private List<Review> reviews;
}EntityManager & Persistence Context
The EntityManager is the gateway to JPA operations; the persistence context is its first-level cache holding managed entity instances for the duration of a transaction.
- ✓EntityManager manages the Persistence Context — a first-level (L1) cache per transaction.
- ✓Entity states: Transient → Managed → Detached (or Removed).
- ✓Dirty checking: Hibernate compares entity snapshots at flush time and generates UPDATE if changed.
- ✓em.find() with the same ID in the same transaction returns the same Java object (cache hit).
- ✓JPQL queries bypass L1 cache but results are merged into the persistence context.
- ✓FlushModeType.AUTO flushes before queries; COMMIT defers to transaction commit.
@Service
@Transactional
public class OrderService {
@PersistenceContext
private EntityManager em;
public void demonstrateLifecycle() {
// 1. TRANSIENT — new object, not yet associated with persistence context
Order order = new Order(customerId, items); // transient
// 2. MANAGED — persisted; tracked by persistence context
em.persist(order); // managed (INSERT on flush)
// order.getId() is now populated (IDENTITY strategy)
// 3. Dirty checking — change detected automatically
order.setStatus(OrderStatus.PLACED); // no explicit update needed
// Hibernate compares snapshot vs current state on flush → generates UPDATE
// 4. DETACHED — entity no longer tracked
em.detach(order);
order.setStatus(OrderStatus.SHIPPED); // change NOT persisted
// 5. Re-attach with merge
Order managed = em.merge(order); // SELECT + UPDATE
managed.setStatus(OrderStatus.DELIVERED); // this change IS persisted
// 6. REMOVED — marked for deletion
em.remove(managed); // DELETE on flush
}
}Entity Lifecycle States
An entity moves through four states: transient (new, no ID), managed (tracked by context), detached (disconnected after context closes), and removed (scheduled for DELETE).
- ✓Four states: Transient (new, no context), Managed (tracked, changes auto-flushed), Detached (was managed, not tracked), Removed (DELETE pending).
- ✓Dirty checking: Hibernate compares managed entity fields to a snapshot at flush time — changed fields generate UPDATE automatically.
- ✓merge() returns a new managed reference; the detached instance passed in stays detached — always use the returned value.
- ✓You cannot remove a detached entity — call merge() first to re-attach it, then remove() on the managed copy.
- ✓@Transactional(readOnly=true) skips dirty checking and snapshot creation — use it for all read-only service methods for better performance.
- ✓LazyInitializationException occurs when a LAZY proxy is accessed after the persistence context (transaction) has closed.
@Service
@Transactional
public class OrderService {
@PersistenceContext
private EntityManager em;
public void demo() {
// 1. Transient — new object, no persistence context
Order order = new Order(); // state: TRANSIENT
order.setReference("ORD-1");
// 2. Managed — persist transitions to MANAGED
em.persist(order); // state: MANAGED; INSERT on flush
order.setStatus("PENDING"); // dirty check — UPDATE on flush (auto!)
// 3. Detached — clear all entities from context
em.flush(); // SQL executed now
em.detach(order); // state: DETACHED — changes no longer tracked
order.setStatus("SHIPPED"); // NOT tracked — no SQL
// 4. Re-attach — merge returns a MANAGED copy
Order managed = em.merge(order); // state: MANAGED (new reference!)
// The original 'order' is still DETACHED; use 'managed' going forward
// 5. Removed — schedule DELETE
em.remove(managed); // state: REMOVED; DELETE on flush
}
}@Entity, @Table, @Id
@Entity marks a class as a JPA-managed entity; @Table customises the table name and schema; @Id designates the primary key field; every entity must have exactly one @Id.
- ✓@Entity is required; @Table is optional — use it to customise table name or add constraints.
- ✓Every entity must have exactly one @Id field and a no-arg constructor.
- ✓Entities must not be final — Hibernate needs to subclass them for lazy proxying.
- ✓@IdClass and @EmbeddedId support composite primary keys.
- ✓Implement equals/hashCode based on a business key (natural ID), not the generated DB id.
- ✓Define unique constraints and indexes via @Table annotations for JPA-managed schema generation.
// Basic entity — table name defaults to "Product" (case-insensitive)
@Entity
public class Product {
@Id
private Long id;
private String name;
}
// Custom table name, schema, and unique constraint
@Entity
@Table(
name = "tbl_products",
schema = "shop",
uniqueConstraints = {
@UniqueConstraint(name = "uq_product_sku", columnNames = {"sku"}),
@UniqueConstraint(name = "uq_product_barcode", columnNames = {"barcode_type", "barcode_value"})
},
indexes = {
@Index(name = "idx_product_category", columnList = "category_id"),
@Index(name = "idx_product_name", columnList = "name")
}
)
public class Product {
@Id
private Long id;
@Column(nullable = false, length = 50)
private String sku;
@Column(name = "barcode_type", length = 10)
private String barcodeType;
@Column(name = "barcode_value", length = 50)
private String barcodeValue;
// JPA requires a no-arg constructor (public or protected)
protected Product() {}
public Product(Long id, String sku) { this.id = id; this.sku = sku; }
}@GeneratedValue Strategies
AUTO delegates to the provider; IDENTITY uses DB auto-increment; SEQUENCE uses a DB sequence (most efficient for batch inserts); TABLE uses a dedicated ID table (avoid in production).
- ✓IDENTITY uses DB auto-increment — simple but disables JDBC batch INSERT.
- ✓SEQUENCE pre-fetches ID blocks (allocationSize) — enables batching; use for bulk inserts.
- ✓TABLE strategy is portable but uses DB row locks — avoid in production.
- ✓UUID (@GeneratedValue(strategy=UUID)) is useful for distributed systems with no sequence.
- ✓UUIDv7 (time-ordered) avoids random B-tree index splits unlike random UUIDv4.
- ✓For bulk inserts: flush + clear every N entities to avoid OutOfMemoryError from L1 cache growth.
// IDENTITY — simple, works with MySQL AUTO_INCREMENT
// ✗ Disables JDBC batch inserts (Hibernate needs each insert's ID immediately)
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
// SEQUENCE — pre-fetches ID blocks; enables batch inserts
// Works with PostgreSQL natively; MySQL needs a workaround sequence table
@Entity
@SequenceGenerator(
name = "order_seq",
sequenceName = "order_id_seq", // DB sequence name
allocationSize = 50 // pre-allocate 50 IDs per round-trip
)
public class Order {
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "order_seq"
)
private Long id;
}
// With allocationSize=50:
// 1st ID fetch: SELECT nextval('order_id_seq') → 1
// Hibernate caches IDs 1-50 in memory — no DB call for the next 49 entities
// 51st entity: SELECT nextval → 51, caches 51-100, etc.@Column Mapping
@Column maps a field to a specific column with custom name, length, precision, nullable, and unique constraints that are honoured during schema generation and validation.
- ✓@Column without attributes uses field name (with configured naming strategy) as column name
- ✓nullable=false generates NOT NULL in DDL and is checked during Hibernate validation
- ✓Use precision and scale for DECIMAL columns — never map currency to a FLOAT
- ✓insertable=false / updatable=false exclude the column from INSERT / UPDATE SQL
- ✓columnDefinition provides raw DDL — useful for DB-specific types (TEXT, JSON, JSONB)
- ✓spring.jpa.hibernate.ddl-auto=validate checks your mappings against the actual schema at startup
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "product_name", nullable = false, length = 200)
private String name;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price; // maps to DECIMAL(10, 2)
@Column(name = "sku_code", unique = true, length = 50)
private String sku;
@Column(columnDefinition = "TEXT") // raw DDL type override
private String description;
@Column(updatable = false) // set on insert, never updated
private LocalDateTime createdAt;
}@Embeddable & @Embedded
@Embeddable defines a reusable value-type POJO; @Embedded includes its columns in the owning entity table. Use for Address, Money, or other value objects with no identity.
- ✓@Embeddable class columns are stored in the owning entity's table — no separate table or join
- ✓Value types have no identity; use value equality (equals/hashCode on all fields)
- ✓@AttributeOverrides is required when embedding the same type more than once in one entity
- ✓A null @Embedded field stores all its columns as NULL; all-NULL columns load back as null
- ✓@Embeddable classes need a no-arg constructor (can be protected/package-private)
- ✓Embeddables can nest other embeddables — keep nesting shallow for readability
@Embeddable
public class Address {
@Column(nullable = false, length = 200)
private String street;
@Column(nullable = false, length = 100)
private String city;
@Column(nullable = false, length = 10)
private String postcode;
@Column(nullable = false, length = 2)
private String countryCode;
// no-arg constructor required
protected Address() {}
public Address(String street, String city, String postcode, String countryCode) {
this.street = street; this.city = city;
this.postcode = postcode; this.countryCode = countryCode;
}
// equals() and hashCode() based on all fields (value equality)
}
@Entity
public class Customer {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Embedded // columns: street, city, postcode, country_code
private Address billingAddress;
}