Add Hibernate 7 batches 2-6, batch 7, and batch 8: mapping styles, JPA annotations, natural IDs, @Immutable, stored procedures, in-memory test databases, JNDI mocking, proxies, associations, temporal mapping, named queries, HQL, Criteria API, EntityManager bootstrapping, Ehcache 3 L2 cache configuration, HikariCP connection pooling, Hibernate Validator CDI integration, aggregate functions, sorting, pagination, interceptors, and Hibernate Search 8 (Hibernate 7.4.5.Final + Spring Boot 4.1.1 + JDK 25)

This commit is contained in:
2026-09-20 06:06:42 +00:00
committed by Claude
commit 8568c0ce6c
330 changed files with 23668 additions and 0 deletions
@@ -0,0 +1,27 @@
package com.ankurm.hibernatedemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Entry point for the companion demos behind three ankurm.com Hibernate 7 posts:
*
* <ul>
* <li>{@code get-vs-load} &mdash; docs/01-get-vs-load.md, {@link com.ankurm.hibernatedemo.scenario.GetVsLoadRunner}</li>
* <li>{@code merge-vs-refresh} &mdash; docs/02-merge-vs-refresh.md, {@link com.ankurm.hibernatedemo.scenario.MergeVsRefreshRunner}</li>
* <li>{@code insert-identity} / {@code insert-sequence} &mdash; docs/03-inserting-objects.md,
* {@link com.ankurm.hibernatedemo.scenario.InsertIdentityRunner} and
* {@link com.ankurm.hibernatedemo.scenario.InsertSequenceRunner}</li>
* </ul>
*
* Each scenario is a profile-gated {@link org.springframework.boot.CommandLineRunner} that runs
* once against an in-memory H2 database and exits &mdash; there is no web server to keep alive,
* so {@code scripts/run.sh &lt;profile&gt;} is a plain foreground {@code mvn spring-boot:run} call.
*/
@SpringBootApplication
public class HibernateDemoApplication {
public static void main(String[] args) {
SpringApplication.run(HibernateDemoApplication.class, args);
}
}
@@ -0,0 +1,11 @@
package com.ankurm.hibernatedemo.aggregate;
/**
* A Java record used as a {@code select new} constructor-expression target -- Hibernate 7 accepts
* a record's canonical constructor here exactly like it accepts a class constructor, so a
* GROUP BY summary can come back as a real typed record instead of an {@code Object[]}.
*
* <p>Docs: docs/21-aggregate-functions.md.
*/
public record CategorySummary(String category, long productCount, double averagePrice) {
}
@@ -0,0 +1,61 @@
package com.ankurm.hibernatedemo.aggregate;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
/**
* A deliberately plain entity for chapter 21's aggregate-function tests: one numeric column
* ({@code price}) and one nullable numeric column ({@code stockQuantity}) so the null-handling
* behavior of {@code AVG}/{@code SUM} over a partially-null column has something real to bite on.
*
* <p>Docs: docs/21-aggregate-functions.md.
*/
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String category;
private String name;
private Double price;
/** Deliberately nullable -- a discontinued product with unknown stock is modelled as NULL, not 0. */
private Integer stockQuantity;
protected Product() {
// for Hibernate
}
public Product(String category, String name, Double price, Integer stockQuantity) {
this.category = category;
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
}
public Long getId() {
return id;
}
public String getCategory() {
return category;
}
public String getName() {
return name;
}
public Double getPrice() {
return price;
}
public Integer getStockQuantity() {
return stockQuantity;
}
}
@@ -0,0 +1,53 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.NamedEntityGraph;
import jakarta.persistence.NamedAttributeNode;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
/**
* Plain (non-batched) author used for the N+1 / fetch-join / entity-graph comparison in
* docs/12-association-mappings.md, chapter "Counting the N+1". No {@code @BatchSize} here on
* purpose &mdash; {@link BatchAuthor} is the batched twin used for the fourth number.
*/
@Entity
@Table(name = "assoc_author")
@NamedEntityGraph(name = "AssocAuthor.books", attributeNodes = @NamedAttributeNode("books"))
public class AssocAuthor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "author", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
private List<AssocBook> books = new ArrayList<>();
protected AssocAuthor() {
}
public AssocAuthor(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public List<AssocBook> getBooks() {
return books;
}
}
@@ -0,0 +1,50 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
/** Owning side (holds the FK) of {@link AssocAuthor#getBooks()}. Docs: 12-association-mappings.md. */
@Entity
@Table(name = "assoc_book")
public class AssocBook {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private AssocAuthor author;
protected AssocBook() {
}
public AssocBook(String title, AssocAuthor author) {
this.title = title;
this.author = author;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public AssocAuthor getAuthor() {
return author;
}
public void setAuthor(AssocAuthor author) {
this.author = author;
}
}
@@ -0,0 +1,53 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
/**
* Deliberately has TWO {@code List} (bag) collections so that fetch-joining both in one JPQL
* query reproduces {@code org.hibernate.loader.MultipleBagFetchException}. Docs: 12-association-mappings.md,
* chapter "MultipleBagFetchException".
*/
@Entity
@Table(name = "bag_author_list")
public class BagAuthorList {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "author", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
private List<BagBookL> books = new ArrayList<>();
@OneToMany(mappedBy = "author", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
private List<BagAwardL> awards = new ArrayList<>();
protected BagAuthorList() {
}
public BagAuthorList(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public List<BagBookL> getBooks() {
return books;
}
public List<BagAwardL> getAwards() {
return awards;
}
}
@@ -0,0 +1,53 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.HashSet;
import java.util.Set;
/**
* Same shape as {@link BagAuthorList} but with {@code Set} collections &mdash; fetch-joining
* both does NOT throw {@code MultipleBagFetchException} (that is Fix #1), but it does reproduce
* the cartesian-product row explosion: one SQL row per (book, award) pair per author.
*/
@Entity
@Table(name = "bag_author_set")
public class BagAuthorSet {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "author", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
private Set<BagBookS> books = new HashSet<>();
@OneToMany(mappedBy = "author", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
private Set<BagAwardS> awards = new HashSet<>();
protected BagAuthorSet() {
}
public BagAuthorSet(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public Set<BagBookS> getBooks() {
return books;
}
public Set<BagAwardS> getAwards() {
return awards;
}
}
@@ -0,0 +1,37 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "bag_award_l")
public class BagAwardL {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private BagAuthorList author;
protected BagAwardL() {
}
public BagAwardL(String name, BagAuthorList author) {
this.name = name;
this.author = author;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,37 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "bag_award_s")
public class BagAwardS {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private BagAuthorSet author;
protected BagAwardS() {
}
public BagAwardS(String name, BagAuthorSet author) {
this.name = name;
this.author = author;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,37 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "bag_book_l")
public class BagBookL {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private BagAuthorList author;
protected BagBookL() {
}
public BagBookL(String title, BagAuthorList author) {
this.title = title;
this.author = author;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,37 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "bag_book_s")
public class BagBookS {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private BagAuthorSet author;
protected BagBookS() {
}
public BagBookS(String title, BagAuthorSet author) {
this.title = title;
this.author = author;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,47 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.annotations.BatchSize;
/**
* Same shape as {@link AssocAuthor} but the {@code books} collection carries
* {@code @BatchSize(size = 10)} &mdash; the fourth number in the N+1 comparison table.
*/
@Entity
@Table(name = "batch_author")
public class BatchAuthor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@BatchSize(size = 10)
@OneToMany(mappedBy = "author", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
private List<BatchBook> books = new ArrayList<>();
protected BatchAuthor() {
}
public BatchAuthor(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public List<BatchBook> getBooks() {
return books;
}
}
@@ -0,0 +1,37 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "batch_book")
public class BatchBook {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private BatchAuthor author;
protected BatchBook() {
}
public BatchBook(String title, BatchAuthor author) {
this.title = title;
this.author = author;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,55 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
/**
* {@code cascade = ALL, orphanRemoval = true}: the "developer did not expect this" cascade
* trap. Docs: 12-association-mappings.md, chapter "Cascade and orphanRemoval".
*/
@Entity
@Table(name = "cascade_author")
public class CascadeAuthor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<CascadeBook> books = new ArrayList<>();
protected CascadeAuthor() {
}
public CascadeAuthor(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public List<CascadeBook> getBooks() {
return books;
}
/**
* The bug: a developer "resets" the list by assigning a brand new collection instead of
* mutating the existing one (a common pattern when mapping from a DTO). With
* {@code orphanRemoval = true}, Hibernate sees every previously-owned book missing from the
* new collection and deletes all of them on flush.
*/
public void replaceBooksWithNewList(List<CascadeBook> newBooks) {
this.books = new ArrayList<>(newBooks);
}
}
@@ -0,0 +1,41 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "cascade_book")
public class CascadeBook {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private CascadeAuthor author;
protected CascadeBook() {
}
public CascadeBook(String title, CascadeAuthor author) {
this.title = title;
this.author = author;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
}
@@ -0,0 +1,38 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
/** Owning side (holds {@code user_id} FK) of {@link LazyUser#getProfile()}. */
@Entity
@Table(name = "lazy_profile")
public class LazyProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String bio;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private LazyUser user;
protected LazyProfile() {
}
public LazyProfile(String bio, LazyUser user) {
this.bio = bio;
this.user = user;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,50 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
/**
* Non-owning ("mappedBy") side of an optional {@code @OneToOne}. Docs: 12-association-mappings.md,
* chapter "The @OneToOne lazy trap". Even though {@link #profile} is declared
* {@code FetchType.LAZY}, Hibernate cannot build a proxy for it here without bytecode
* enhancement: it does not hold the foreign key, so it cannot know whether a profile row
* exists without querying. The result is an eager extra SELECT on every load of LazyUser.
*/
@Entity
@Table(name = "lazy_user")
public class LazyUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
@OneToOne(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.ALL, optional = true)
private LazyProfile profile;
protected LazyUser() {
}
public LazyUser(String username) {
this.username = username;
}
public Long getId() {
return id;
}
public LazyProfile getProfile() {
return profile;
}
public void setProfile(LazyProfile profile) {
this.profile = profile;
}
}
@@ -0,0 +1,36 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.MapsId;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
/** Owning side using {@code @MapsId}: its {@code @Id} IS the {@code user_id} FK value. */
@Entity
@Table(name = "mi_profile")
public class MiProfile {
@Id
private Long id;
private String bio;
@MapsId
@OneToOne
@JoinColumn(name = "id")
private MiUser user;
protected MiProfile() {
}
public MiProfile(String bio, MiUser user) {
this.bio = bio;
this.user = user;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,35 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
/**
* The @MapsId fix: this entity carries NO inverse {@code @OneToOne} field at all. A profile is
* looked up on demand with {@code session.find(MiProfile.class, userId)} because it shares the
* same primary key value as the user (see {@link MiProfile}), so there is nothing to proxy and
* nothing forces an eager join when loading a user.
*/
@Entity
@Table(name = "mi_user")
public class MiUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
protected MiUser() {
}
public MiUser(String username) {
this.username = username;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,47 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
/**
* {@code cascade = {PERSIST, MERGE}}, no {@code orphanRemoval}. Used for two things:
* (1) removing a child from the in-memory collection and flushing does nothing to the row
* (docs: "orphanRemoval off"), and (2) mutating only this inverse side (the collection) without
* touching {@link NoOrphanBook#setAuthor} never writes the FK (docs: "owning side").
*/
@Entity
@Table(name = "no_orphan_author")
public class NoOrphanAuthor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "author", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
private List<NoOrphanBook> books = new ArrayList<>();
protected NoOrphanAuthor() {
}
public NoOrphanAuthor(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public List<NoOrphanBook> getBooks() {
return books;
}
}
@@ -0,0 +1,45 @@
package com.ankurm.hibernatedemo.association;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "no_orphan_book")
public class NoOrphanBook {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private NoOrphanAuthor author;
protected NoOrphanBook() {
}
public NoOrphanBook(String title, NoOrphanAuthor author) {
this.title = title;
this.author = author;
}
public Long getId() {
return id;
}
public NoOrphanAuthor getAuthor() {
return author;
}
public void setAuthor(NoOrphanAuthor author) {
this.author = author;
}
}
@@ -0,0 +1,54 @@
package com.ankurm.hibernatedemo.bootstrap;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
/**
* Backs ankurm.com post 4855 (bootstrapping EntityManager). Docs: docs/17-entitymanager-bootstrap.md.
*
* <p>Deliberately NOT scanned by Spring's own auto-configured {@code EntityManagerFactory}
* (see {@code META-INF/persistence.xml} under {@code src/test/resources}, which is what
* {@link EntityManagerBootstrapTest} actually bootstraps against) -- this chapter is about raw
* JPA bootstrapping, deliberately bypassing Spring Boot's autoconfiguration entirely so the two
* paths the fictional original article described (XML vs {@code PersistenceConfiguration}) are
* both exercised for real.
*/
@Entity
public class BootstrapUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
protected BootstrapUser() {
// JPA
}
public BootstrapUser(String name, String email) {
this.name = name;
this.email = email;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
@Override
public String toString() {
return "BootstrapUser{id=%s, name=%s, email=%s}".formatted(id, name, email);
}
}
@@ -0,0 +1,59 @@
package com.ankurm.hibernatedemo.cache;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
/**
* Entity-level L2 cache target for the Ehcache 3 configuration chapter. Unlike
* {@code CachedNaturalIdProduct} (chapter 06), this entity has no natural id at all --
* {@code @Cacheable}/{@code @Cache} here caches lookups by primary key, the ordinary case the
* original article's {@code Product} class demonstrated.
*
* <p>Region name matches the {@code productCache} alias configured in
* {@code src/test/resources/ehcache-chapter18.xml}, deliberately, so a typo here shows up as a
* cache miss rather than a silent fallback to Ehcache's programmatic default.
*
* <p>Docs: docs/18-ehcache-l2-configuration.md
*/
@Entity
@jakarta.persistence.Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "productCache")
public class CacheProduct {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String name;
private Double price;
public CacheProduct() {
}
public CacheProduct(String name, Double price) {
this.name = name;
this.price = price;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
}
@@ -0,0 +1,40 @@
package com.ankurm.hibernatedemo.cache;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
/**
* Same shape as {@link CacheProduct}, deliberately with no {@code @Cacheable}/{@code @Cache} at
* all. Used only by {@code QueryCacheWithoutEntityCacheTest} to demonstrate the "Query Cache
* without Entity Cache causes N+1 selects" pitfall the original article listed but never
* measured.
*
* <p>Docs: docs/18-ehcache-l2-configuration.md
*/
@Entity
public class UncachedProduct {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String name;
public UncachedProduct() {
}
public UncachedProduct(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
@@ -0,0 +1,58 @@
package com.ankurm.hibernatedemo.datetime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
import java.time.LocalDateTime;
/** Docs: 13-date-and-time-mapping.md, chapter "Second-precision / truncation". */
@Entity
@Table(name = "nano_precision")
public class NanoPrecisionEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private LocalDateTime plainLocalDateTime;
@Column(precision = 9)
private LocalDateTime highPrecisionLocalDateTime;
private Instant plainInstant;
protected NanoPrecisionEntity() {
}
public Long getId() {
return id;
}
public LocalDateTime getPlainLocalDateTime() {
return plainLocalDateTime;
}
public void setPlainLocalDateTime(LocalDateTime v) {
this.plainLocalDateTime = v;
}
public LocalDateTime getHighPrecisionLocalDateTime() {
return highPrecisionLocalDateTime;
}
public void setHighPrecisionLocalDateTime(LocalDateTime v) {
this.highPrecisionLocalDateTime = v;
}
public Instant getPlainInstant() {
return plainInstant;
}
public void setPlainInstant(Instant v) {
this.plainInstant = v;
}
}
@@ -0,0 +1,42 @@
package com.ankurm.hibernatedemo.datetime;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import java.time.Instant;
/**
* Deliberately misuses the deprecated {@code @Temporal} annotation on a {@code java.time.Instant}
* field -- not portable per the Jakarta Persistence 3.2 spec, but does Hibernate 7.4.5 actually
* reject it? Docs: 13-date-and-time-mapping.md, chapter "@Temporal verified".
*/
@Entity
@Table(name = "temporal_on_java_time")
public class TemporalOnJavaTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Temporal(TemporalType.TIMESTAMP)
private Instant instantWithTemporalAnnotation;
protected TemporalOnJavaTimeEntity() {
}
public Long getId() {
return id;
}
public Instant getInstantWithTemporalAnnotation() {
return instantWithTemporalAnnotation;
}
public void setInstantWithTemporalAnnotation(Instant v) {
this.instantWithTemporalAnnotation = v;
}
}
@@ -0,0 +1,137 @@
package com.ankurm.hibernatedemo.datetime;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;
/**
* Every basic temporal type in one entity so a single {@code show-create-table} run captures
* the DDL Hibernate 7.4.5 generates for each. Docs: 13-date-and-time-mapping.md, chapter "Basic temporal
* types round trip". {@code legacyDateNoTemporal} deliberately has NO {@code @Temporal}
* annotation to show what Hibernate does by default for {@code java.util.Date}.
*/
@Entity
@Table(name = "temporal_types")
public class TemporalTypesEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private LocalDate localDate;
private LocalDateTime localDateTime;
private LocalTime localTime;
private Instant instant;
private OffsetDateTime offsetDateTime;
private ZonedDateTime zonedDateTime;
@Temporal(TemporalType.DATE)
private Date legacyDateAsDate;
@Temporal(TemporalType.TIMESTAMP)
private Date legacyDateAsTimestamp;
private Date legacyDateNoTemporal;
@Temporal(TemporalType.TIMESTAMP)
private Calendar legacyCalendar;
protected TemporalTypesEntity() {
}
public Long getId() {
return id;
}
public LocalDate getLocalDate() {
return localDate;
}
public void setLocalDate(LocalDate localDate) {
this.localDate = localDate;
}
public LocalDateTime getLocalDateTime() {
return localDateTime;
}
public void setLocalDateTime(LocalDateTime localDateTime) {
this.localDateTime = localDateTime;
}
public LocalTime getLocalTime() {
return localTime;
}
public void setLocalTime(LocalTime localTime) {
this.localTime = localTime;
}
public Instant getInstant() {
return instant;
}
public void setInstant(Instant instant) {
this.instant = instant;
}
public OffsetDateTime getOffsetDateTime() {
return offsetDateTime;
}
public void setOffsetDateTime(OffsetDateTime offsetDateTime) {
this.offsetDateTime = offsetDateTime;
}
public ZonedDateTime getZonedDateTime() {
return zonedDateTime;
}
public void setZonedDateTime(ZonedDateTime zonedDateTime) {
this.zonedDateTime = zonedDateTime;
}
public Date getLegacyDateAsDate() {
return legacyDateAsDate;
}
public void setLegacyDateAsDate(Date legacyDateAsDate) {
this.legacyDateAsDate = legacyDateAsDate;
}
public Date getLegacyDateAsTimestamp() {
return legacyDateAsTimestamp;
}
public void setLegacyDateAsTimestamp(Date legacyDateAsTimestamp) {
this.legacyDateAsTimestamp = legacyDateAsTimestamp;
}
public Date getLegacyDateNoTemporal() {
return legacyDateNoTemporal;
}
public void setLegacyDateNoTemporal(Date legacyDateNoTemporal) {
this.legacyDateNoTemporal = legacyDateNoTemporal;
}
public Calendar getLegacyCalendar() {
return legacyCalendar;
}
public void setLegacyCalendar(Calendar legacyCalendar) {
this.legacyCalendar = legacyCalendar;
}
}
@@ -0,0 +1,106 @@
package com.ankurm.hibernatedemo.datetime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.OffsetDateTime;
import org.hibernate.annotations.TimeZoneStorage;
import org.hibernate.annotations.TimeZoneStorageType;
/**
* One {@code OffsetDateTime} column per {@code @TimeZoneStorage} mode plus one with NO
* annotation at all (to observe Hibernate 7.4.5's actual default). Docs: 13-date-and-time-mapping.md,
* chapter "The central experiment". Run alongside {@code javap
* org.hibernate.annotations.TimeZoneStorageType} to confirm the enum constants independently
* of this entity.
*/
@Entity
@Table(name = "tz_storage")
public class TimeZoneStorageEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// No @TimeZoneStorage annotation at all -- whatever Hibernate 7.4.5 defaults to.
@Column(name = "no_annotation_col")
private OffsetDateTime noAnnotation;
@TimeZoneStorage(TimeZoneStorageType.NATIVE)
@Column(name = "native_col")
private OffsetDateTime nativeMode;
@TimeZoneStorage(TimeZoneStorageType.NORMALIZE)
@Column(name = "normalize_col")
private OffsetDateTime normalizeMode;
@TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC)
@Column(name = "normalize_utc_col")
private OffsetDateTime normalizeUtcMode;
@TimeZoneStorage(TimeZoneStorageType.COLUMN)
@Column(name = "column_mode_col")
private OffsetDateTime columnMode;
@TimeZoneStorage(TimeZoneStorageType.AUTO)
@Column(name = "auto_col")
private OffsetDateTime autoMode;
protected TimeZoneStorageEntity() {
}
public Long getId() {
return id;
}
public OffsetDateTime getNoAnnotation() {
return noAnnotation;
}
public void setNoAnnotation(OffsetDateTime v) {
this.noAnnotation = v;
}
public OffsetDateTime getNativeMode() {
return nativeMode;
}
public void setNativeMode(OffsetDateTime v) {
this.nativeMode = v;
}
public OffsetDateTime getNormalizeMode() {
return normalizeMode;
}
public void setNormalizeMode(OffsetDateTime v) {
this.normalizeMode = v;
}
public OffsetDateTime getNormalizeUtcMode() {
return normalizeUtcMode;
}
public void setNormalizeUtcMode(OffsetDateTime v) {
this.normalizeUtcMode = v;
}
public OffsetDateTime getColumnMode() {
return columnMode;
}
public void setColumnMode(OffsetDateTime v) {
this.columnMode = v;
}
public OffsetDateTime getAutoMode() {
return autoMode;
}
public void setAutoMode(OffsetDateTime v) {
this.autoMode = v;
}
}
@@ -0,0 +1,39 @@
package com.ankurm.hibernatedemo.hikari;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
/**
* A minimal entity used only to force a real JDBC connection checkout through whichever
* connection pool is configured -- this chapter is about the pool, not the mapping, so the
* entity itself is deliberately trivial.
*
* <p>Docs: docs/19-hikaricp-connection-pooling.md
*/
@Entity
public class PoolProbe {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String label;
public PoolProbe() {
}
public PoolProbe(String label) {
this.label = label;
}
public Long getId() {
return id;
}
public String getLabel() {
return label;
}
}
@@ -0,0 +1,44 @@
package com.ankurm.hibernatedemo.immutable;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
/** Child row for {@link RateWithAuditTrail}'s @Immutable collection. */
@Entity
public class AuditTrail {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "audit_seq")
private Long id;
private String note;
@ManyToOne
@JoinColumn(name = "rate_with_audit_id")
private RateWithAuditTrail rateWithAuditTrail;
protected AuditTrail() {
// JPA
}
public AuditTrail(String note) {
this.note = note;
}
public Long getId() {
return id;
}
public String getNote() {
return note;
}
@Override
public String toString() {
return "AuditTrail{id=%s, note=%s}".formatted(id, note);
}
}
@@ -0,0 +1,58 @@
package com.ankurm.hibernatedemo.immutable;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.math.BigDecimal;
import org.hibernate.annotations.Immutable;
/**
* The headline case for docs/07-immutable-entities.md: an {@code @Immutable} entity with a mutable
* Java setter. Nothing stops the field mutation in the JVM -- the point is what happens (or
* doesn't) when the mutated instance is flushed.
*/
@Entity
@Immutable
public class ExchangeRate {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "rate_seq")
private Long id;
private String pair;
@jakarta.persistence.Column(precision = 19, scale = 4)
private BigDecimal rate;
protected ExchangeRate() {
// JPA
}
public ExchangeRate(String pair, BigDecimal rate) {
this.pair = pair;
this.rate = rate;
}
public Long getId() {
return id;
}
public String getPair() {
return pair;
}
public BigDecimal getRate() {
return rate;
}
/** Ordinary setter -- @Immutable is a Hibernate-engine concept, not a Java one. */
public void setRate(BigDecimal rate) {
this.rate = rate;
}
@Override
public String toString() {
return "ExchangeRate{id=%s, pair=%s, rate=%s}".formatted(id, pair, rate);
}
}
@@ -0,0 +1,56 @@
package com.ankurm.hibernatedemo.immutable;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
import java.math.BigDecimal;
import org.hibernate.annotations.Immutable;
/**
* Same shape as {@link ExchangeRate} but carries a {@code @Version} column, to check whether
* Hibernate 7.4.5 rejects the {@code @Immutable} + {@code @Version} combination outright, and
* whether the version column ever increments if it doesn't.
*/
@Entity
@Immutable
public class ExchangeRateVersioned {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "rate_v_seq")
private Long id;
private String pair;
@jakarta.persistence.Column(precision = 19, scale = 4)
private BigDecimal rate;
@Version
private Long version;
protected ExchangeRateVersioned() {
// JPA
}
public ExchangeRateVersioned(String pair, BigDecimal rate) {
this.pair = pair;
this.rate = rate;
}
public Long getId() {
return id;
}
public BigDecimal getRate() {
return rate;
}
public void setRate(BigDecimal rate) {
this.rate = rate;
}
public Long getVersion() {
return version;
}
}
@@ -0,0 +1,47 @@
package com.ankurm.hibernatedemo.immutable;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.math.BigDecimal;
/**
* A perfectly ordinary, non-{@code @Immutable} entity, used as the control group when
* comparing {@code @Immutable} against {@code Session.setDefaultReadOnly(true)} and
* {@code Session.setReadOnly(entity, true)} -- those are Session/query-scoped read-only knobs
* that apply to entities that were never annotated at all.
*/
@Entity
public class PlainRate {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "plain_rate_seq")
private Long id;
private String pair;
@jakarta.persistence.Column(precision = 19, scale = 4)
private BigDecimal rate;
protected PlainRate() {
// JPA
}
public PlainRate(String pair, BigDecimal rate) {
this.pair = pair;
this.rate = rate;
}
public Long getId() {
return id;
}
public BigDecimal getRate() {
return rate;
}
public void setRate(BigDecimal rate) {
this.rate = rate;
}
}
@@ -0,0 +1,51 @@
package com.ankurm.hibernatedemo.immutable;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.annotations.Immutable;
/**
* A MUTABLE parent entity (no {@code @Immutable} on the class) whose collection is marked
* {@code @Immutable}. This isolates the collection-level annotation's own behaviour, per the
* article's "Advanced Usage: Immutable Collections" section, from the entity-level one.
*/
@Entity
public class RateWithAuditTrail {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "rate_audit_seq")
private Long id;
private String pair;
@Immutable
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<AuditTrail> auditTrails = new ArrayList<>();
protected RateWithAuditTrail() {
// JPA
}
public RateWithAuditTrail(String pair) {
this.pair = pair;
}
public Long getId() {
return id;
}
public String getPair() {
return pair;
}
public List<AuditTrail> getAuditTrails() {
return auditTrails;
}
}
@@ -0,0 +1,90 @@
package com.ankurm.hibernatedemo.immutable;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import org.hibernate.annotations.Immutable;
/**
* Same 12-column shape as {@link WideMutableRow}, but @Immutable, for the flush-cost
* comparison in docs/07-immutable-entities.md.
*/
@Entity
@Immutable
public class WideImmutableRow {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "wide_immutable_seq")
private Long id;
private String f1;
private String f2;
private String f3;
private String f4;
private String f5;
private String f6;
private String f7;
private String f8;
private String f9;
private String f10;
private String f11;
private String f12;
protected WideImmutableRow() {
// JPA
}
public WideImmutableRow(String seed) {
this.f1 = seed;
this.f2 = seed;
this.f3 = seed;
this.f4 = seed;
this.f5 = seed;
this.f6 = seed;
this.f7 = seed;
this.f8 = seed;
this.f9 = seed;
this.f10 = seed;
this.f11 = seed;
this.f12 = seed;
}
public Long getId() { return id; }
public String getF1() { return f1; }
public void setF1(String v) { this.f1 = v; }
public String getF2() { return f2; }
public void setF2(String v) { this.f2 = v; }
public String getF3() { return f3; }
public void setF3(String v) { this.f3 = v; }
public String getF4() { return f4; }
public void setF4(String v) { this.f4 = v; }
public String getF5() { return f5; }
public void setF5(String v) { this.f5 = v; }
public String getF6() { return f6; }
public void setF6(String v) { this.f6 = v; }
public String getF7() { return f7; }
public void setF7(String v) { this.f7 = v; }
public String getF8() { return f8; }
public void setF8(String v) { this.f8 = v; }
public String getF9() { return f9; }
public void setF9(String v) { this.f9 = v; }
public String getF10() { return f10; }
public void setF10(String v) { this.f10 = v; }
public String getF11() { return f11; }
public void setF11(String v) { this.f11 = v; }
public String getF12() { return f12; }
public void setF12(String v) { this.f12 = v; }
}
@@ -0,0 +1,90 @@
package com.ankurm.hibernatedemo.immutable;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
/**
* A wide (12-column) MUTABLE entity, used only to give per-entity dirty checking something
* non-trivial to compare on a full flush -- with 1-2 fields the per-field snapshot comparison
* cost is too small to distinguish from measurement noise. See docs/07-immutable-entities.md,
* "@Version and dirty-check cost".
*/
@Entity
public class WideMutableRow {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "wide_mutable_seq")
private Long id;
private String f1;
private String f2;
private String f3;
private String f4;
private String f5;
private String f6;
private String f7;
private String f8;
private String f9;
private String f10;
private String f11;
private String f12;
protected WideMutableRow() {
// JPA
}
public WideMutableRow(String seed) {
this.f1 = seed;
this.f2 = seed;
this.f3 = seed;
this.f4 = seed;
this.f5 = seed;
this.f6 = seed;
this.f7 = seed;
this.f8 = seed;
this.f9 = seed;
this.f10 = seed;
this.f11 = seed;
this.f12 = seed;
}
public Long getId() { return id; }
public String getF1() { return f1; }
public void setF1(String v) { this.f1 = v; }
public String getF2() { return f2; }
public void setF2(String v) { this.f2 = v; }
public String getF3() { return f3; }
public void setF3(String v) { this.f3 = v; }
public String getF4() { return f4; }
public void setF4(String v) { this.f4 = v; }
public String getF5() { return f5; }
public void setF5(String v) { this.f5 = v; }
public String getF6() { return f6; }
public void setF6(String v) { this.f6 = v; }
public String getF7() { return f7; }
public void setF7(String v) { this.f7 = v; }
public String getF8() { return f8; }
public void setF8(String v) { this.f8 = v; }
public String getF9() { return f9; }
public void setF9(String v) { this.f9 = v; }
public String getF10() { return f10; }
public void setF10(String v) { this.f10 = v; }
public String getF11() { return f11; }
public void setF11(String v) { this.f11 = v; }
public String getF12() { return f12; }
public void setF12(String v) { this.f12 = v; }
}
@@ -0,0 +1,49 @@
package com.ankurm.hibernatedemo.interceptor;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
/**
* Chapter 24's interceptor playground -- a plain entity whose {@code name} field an
* {@code Interceptor} mutates in place before it hits the database.
*
* <p>Docs: docs/24-interceptors.md.
*/
@Entity
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int priority;
protected Task() {
// for Hibernate
}
public Task(String name, int priority) {
this.name = name;
this.priority = priority;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPriority() {
return priority;
}
}
@@ -0,0 +1,64 @@
package com.ankurm.hibernatedemo.interceptor;
import java.util.concurrent.atomic.AtomicInteger;
import org.hibernate.Interceptor;
import org.hibernate.type.Type;
/**
* Implements {@link Interceptor} DIRECTLY rather than extending {@code org.hibernate.
* EmptyInterceptor} -- there is no longer a reason to extend anything. Every method on
* {@code Interceptor} is a {@code default} method as of Hibernate 6+ (verified with {@code
* javap} against the 7.4.5.Final jar before writing this class), so overriding just the two
* callbacks this class actually needs is enough; the old {@code org.hibernate.EmptyInterceptor}
* base class still exists in the 7.4.5.Final jar, but only as {@code org.hibernate.internal.
* EmptyInterceptor} -- a package-private-looking, {@code final}, singleton-only class that isn't
* meant to be extended by application code any more.
*
* <p>Both {@link #onSave} and {@link #onFlushDirty} mutate the {@code state} array in place and
* return {@code true} -- that {@code true} is the contract: it tells Hibernate the state array
* was actually changed, so the (possibly mutated) values get flushed, not silently dropped.
*
* <p>Docs: docs/24-interceptors.md.
*/
public class UppercasingInterceptor implements Interceptor {
private final AtomicInteger onSaveCalls = new AtomicInteger();
private final AtomicInteger onFlushDirtyCalls = new AtomicInteger();
@Override
public boolean onSave(Object entity, Object id, Object[] state, String[] propertyNames, Type[] types) {
// 'id' is typed java.lang.Object here, not java.io.Serializable -- Hibernate 6 widened
// every identifier parameter on this interface from Serializable to Object, since an
// application is free to use a non-Serializable identifier type.
onSaveCalls.incrementAndGet();
return uppercaseNameIfPresent(state, propertyNames);
}
@Override
public boolean onFlushDirty(Object entity, Object id, Object[] currentState, Object[] previousState,
String[] propertyNames, Type[] types) {
onFlushDirtyCalls.incrementAndGet();
return uppercaseNameIfPresent(currentState, propertyNames);
}
private boolean uppercaseNameIfPresent(Object[] state, String[] propertyNames) {
for (int i = 0; i < propertyNames.length; i++) {
if ("name".equals(propertyNames[i]) && state[i] instanceof String s) {
String upper = s.toUpperCase(java.util.Locale.ROOT);
if (!upper.equals(s)) {
state[i] = upper;
return true; // tells Hibernate: yes, I changed the state array, flush it
}
}
}
return false;
}
public int getOnSaveCalls() {
return onSaveCalls.get();
}
public int getOnFlushDirtyCalls() {
return onFlushDirtyCalls.get();
}
}
@@ -0,0 +1,46 @@
package com.ankurm.hibernatedemo.mappingstyle;
/**
* Deliberately annotation-free POJO, mapped only through
* {@code HbmEmployee.hbm.xml} (legacy Hibernate mapping format).
*
* <p>Backs the empirical hbm.xml probe in {@code HbmXmlBootTest}. Docs: docs/04-annotations-vs-xml.md.
*/
public class HbmEmployee {
private Long id;
private String firstName;
private String email;
public HbmEmployee() {
}
public HbmEmployee(String firstName, String email) {
this.firstName = firstName;
this.email = email;
}
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 getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
@@ -0,0 +1,39 @@
package com.ankurm.hibernatedemo.mappingstyle;
/**
* Zero annotations. Mapped entirely by {@code mapping-xml-natural-id.xml}, using Hibernate's
* native "mapping.xml" XML dialect (namespace {@code http://www.hibernate.org/xsd/orm/mapping},
* schema {@code mapping-7.0.xsd}) -- NOT the JPA-standard orm.xml dialect, which has no
* &lt;natural-id&gt; element at all. Proves a Hibernate-only concept (@NaturalId) can be expressed
* in XML, just not in the portable JPA orm.xml XSD.
*
* <p>Docs: docs/04-annotations-vs-xml.md, Topic 1 ("what XML can do that annotations cannot" --
* inverted: what one XML dialect can do that the other XML dialect and annotations both cannot
* express the same way).
*/
public class MappingXmlNaturalIdEntity {
private Long id;
private String sku;
private String name;
public MappingXmlNaturalIdEntity() {
}
public MappingXmlNaturalIdEntity(String sku, String name) {
this.sku = sku;
this.name = name;
}
public Long getId() {
return id;
}
public String getSku() {
return sku;
}
public String getName() {
return name;
}
}
@@ -0,0 +1,38 @@
package com.ankurm.hibernatedemo.mappingstyle;
/**
* Deliberately carries NO JPA/Hibernate annotations at all -- not even {@code @Entity}.
* Its only mapping is {@code orm-xml-only-mapping.xml}, registered via
* {@code spring.jpa.mapping-resources}. If a query against {@code xml_only_widgets} succeeds,
* orm.xml alone is enough to make this a managed entity.
*
* <p>Docs: docs/04-annotations-vs-xml.md, Topic 1.
*/
public class OrmXmlOnlyEntity {
private Long id;
private String label;
public OrmXmlOnlyEntity() {
}
public OrmXmlOnlyEntity(String label) {
this.label = label;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
}
@@ -0,0 +1,39 @@
package com.ankurm.hibernatedemo.mappingstyle;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
/**
* Annotated with {@code @Column(name = "annotation_name")}. A matching orm.xml entry
* (see {@code orm-xml-override-mapping.xml}) maps the SAME field to {@code xml_name} instead.
* Whichever name shows up in the generated DDL/SQL is the winner.
*
* <p>Docs: docs/04-annotations-vs-xml.md, Topic 1 (merge/override semantics).
*/
@Entity
public class OverrideEntity {
@Id
@GeneratedValue
private Long id;
@Column(name = "annotation_name")
private String value;
public OverrideEntity() {
}
public OverrideEntity(String value) {
this.value = value;
}
public Long getId() {
return id;
}
public String getValue() {
return value;
}
}
+97
View File
@@ -0,0 +1,97 @@
package com.ankurm.hibernatedemo.model;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Version;
import java.util.ArrayList;
import java.util.List;
/**
* The entity used by {@code get-vs-load} and {@code merge-vs-refresh}.
*
* <p>Docs: docs/01-get-vs-load.md, docs/02-merge-vs-refresh.md.
*
* <p>Carries a {@code @Version} column on purpose &mdash; the merge/refresh tests need a real
* optimistic-lock field to show what merge() does when the version it is holding is stale, not
* just what it does to a plain column. The {@code status} field exists specifically for
* {@code MergeRefreshTest#refreshDiscardsUnflushedEditSilently}, framed as the
* &quot;USER_EDIT&quot; vs &quot;ADMIN_EDIT&quot; scenario in docs/02-merge-vs-refresh.md. The
* {@code notes} collection is LAZY and cascades MERGE only &mdash; it exists solely for
* {@code MergeRefreshTest#mergeDoesNotRequireLazyCollectionToBeInitialized}, which shows that an
* unfetched collection is never navigated during merge().
*/
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_seq")
private Long id;
private String title;
private String author;
private String status;
@Version
private Long version;
@OneToMany(mappedBy = "book", cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
private List<Note> notes = new ArrayList<>();
protected Book() {
// JPA
}
public Book(String title, String author) {
this.title = title;
this.author = author;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Long getVersion() {
return version;
}
public List<Note> getNotes() {
return notes;
}
@Override
public String toString() {
return "Book{id=%s, title=%s, author=%s, status=%s, version=%s}"
.formatted(id, title, author, status, version);
}
}
+45
View File
@@ -0,0 +1,45 @@
package com.ankurm.hibernatedemo.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
/**
* Child of {@link Book}, used only by {@code MergeRefreshTest#mergeDoesNotRequireLazyCollectionToBeInitialized}
* (docs/02-merge-vs-refresh.md) to show that an unfetched {@code LAZY} collection is not
* navigated -- and therefore cannot fail with {@code LazyInitializationException} -- during
* {@code merge()} of the owning detached entity.
*/
@Entity
public class Note {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String text;
@ManyToOne
@JoinColumn(name = "book_id")
private Book book;
protected Note() {
// JPA
}
public Note(String text, Book book) {
this.text = text;
this.book = book;
}
public Long getId() {
return id;
}
public String getText() {
return text;
}
}
@@ -0,0 +1,37 @@
package com.ankurm.hibernatedemo.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
/**
* One of four otherwise-identical entities ({@link WidgetAlloc1}, {@link WidgetAlloc10},
* {@link WidgetAlloc25}, {@link WidgetAlloc50}) used only by
* {@code AllocationSizeSweepTest} to isolate the effect of {@code allocationSize} on
* {@code prepareStatementCount} while {@code hibernate.jdbc.batch_size} is held fixed at 25.
* Docs: docs/03-inserting-objects.md.
*/
@Entity
public class WidgetAlloc1 {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_alloc1_seq")
@SequenceGenerator(name = "widget_alloc1_seq", sequenceName = "widget_alloc1_seq", allocationSize = 1)
private Long id;
private String name;
protected WidgetAlloc1() {
// JPA
}
public WidgetAlloc1(String name) {
this.name = name;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,34 @@
package com.ankurm.hibernatedemo.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
/**
* See {@link WidgetAlloc1} &mdash; same purpose, {@code allocationSize = 10}.
* Docs: docs/03-inserting-objects.md.
*/
@Entity
public class WidgetAlloc10 {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_alloc10_seq")
@SequenceGenerator(name = "widget_alloc10_seq", sequenceName = "widget_alloc10_seq", allocationSize = 10)
private Long id;
private String name;
protected WidgetAlloc10() {
// JPA
}
public WidgetAlloc10(String name) {
this.name = name;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,35 @@
package com.ankurm.hibernatedemo.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
/**
* See {@link WidgetAlloc1} &mdash; same purpose, {@code allocationSize = 25} (matches
* {@code hibernate.jdbc.batch_size} in the sweep, same as {@link WidgetSequence}).
* Docs: docs/03-inserting-objects.md.
*/
@Entity
public class WidgetAlloc25 {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_alloc25_seq")
@SequenceGenerator(name = "widget_alloc25_seq", sequenceName = "widget_alloc25_seq", allocationSize = 25)
private Long id;
private String name;
protected WidgetAlloc25() {
// JPA
}
public WidgetAlloc25(String name) {
this.name = name;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,35 @@
package com.ankurm.hibernatedemo.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
/**
* See {@link WidgetAlloc1} &mdash; same purpose, {@code allocationSize = 50} (JPA's default).
* Reused by {@code BatchSizeSweepTest} to hold {@code allocationSize} fixed at 50 while
* {@code hibernate.jdbc.batch_size} varies. Docs: docs/03-inserting-objects.md.
*/
@Entity
public class WidgetAlloc50 {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_alloc50_seq")
@SequenceGenerator(name = "widget_alloc50_seq", sequenceName = "widget_alloc50_seq", allocationSize = 50)
private Long id;
private String name;
protected WidgetAlloc50() {
// JPA
}
public WidgetAlloc50(String name) {
this.name = name;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,35 @@
package com.ankurm.hibernatedemo.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
/**
* Used exclusively by {@code BatchSizeSweepTest.BatchSize1}. See {@link WidgetBatchSweep50}'s
* Javadoc for why each batch_size sweep point gets its own entity and sequence rather than
* sharing one across nested test classes. Docs: docs/03-inserting-objects.md.
*/
@Entity
public class WidgetBatchSweep1 {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_batch_sweep1_seq")
@SequenceGenerator(name = "widget_batch_sweep1_seq", sequenceName = "widget_batch_sweep1_seq", allocationSize = 50)
private Long id;
private String name;
protected WidgetBatchSweep1() {
// JPA
}
public WidgetBatchSweep1(String name) {
this.name = name;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,35 @@
package com.ankurm.hibernatedemo.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
/**
* Used exclusively by {@code BatchSizeSweepTest.BatchSize10}. See {@link WidgetBatchSweep50}'s
* Javadoc for why each batch_size sweep point gets its own entity and sequence rather than
* sharing one across nested test classes. Docs: docs/03-inserting-objects.md.
*/
@Entity
public class WidgetBatchSweep10 {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_batch_sweep1_seq")
@SequenceGenerator(name = "widget_batch_sweep1_seq", sequenceName = "widget_batch_sweep1_seq", allocationSize = 50)
private Long id;
private String name;
protected WidgetBatchSweep10() {
// JPA
}
public WidgetBatchSweep10(String name) {
this.name = name;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,35 @@
package com.ankurm.hibernatedemo.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
/**
* Used exclusively by {@code BatchSizeSweepTest.BatchSize25}. See {@link WidgetBatchSweep50}'s
* Javadoc for why each batch_size sweep point gets its own entity and sequence rather than
* sharing one across nested test classes. Docs: docs/03-inserting-objects.md.
*/
@Entity
public class WidgetBatchSweep25 {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_batch_sweep1_seq")
@SequenceGenerator(name = "widget_batch_sweep1_seq", sequenceName = "widget_batch_sweep1_seq", allocationSize = 50)
private Long id;
private String name;
protected WidgetBatchSweep25() {
// JPA
}
public WidgetBatchSweep25(String name) {
this.name = name;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,35 @@
package com.ankurm.hibernatedemo.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
/**
* Used exclusively by {@code BatchSizeSweepTest.BatchSize50}. See {@link WidgetBatchSweep50}'s
* Javadoc for why each batch_size sweep point gets its own entity and sequence rather than
* sharing one across nested test classes. Docs: docs/03-inserting-objects.md.
*/
@Entity
public class WidgetBatchSweep50 {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_batch_sweep1_seq")
@SequenceGenerator(name = "widget_batch_sweep1_seq", sequenceName = "widget_batch_sweep1_seq", allocationSize = 50)
private Long id;
private String name;
protected WidgetBatchSweep50() {
// JPA
}
public WidgetBatchSweep50(String name) {
this.name = name;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,41 @@
package com.ankurm.hibernatedemo.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
/**
* Insert-scenario twin of {@link WidgetSequence}, identical except for the id generation
* strategy. Docs: docs/03-inserting-objects.md.
*
* <p>{@code IDENTITY} requires the database to hand back the generated key on every single
* insert, which is exactly why it defeats JDBC batching &mdash; see the captured output in
* docs/output/insert-identity.txt versus docs/output/insert-sequence.txt for the same
* {@code hibernate.jdbc.batch_size} setting producing very different behaviour.
*/
@Entity
public class WidgetIdentity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
protected WidgetIdentity() {
// JPA
}
public WidgetIdentity(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
@@ -0,0 +1,42 @@
package com.ankurm.hibernatedemo.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
/**
* Insert-scenario twin of {@link WidgetIdentity}. Docs: docs/03-inserting-objects.md.
*
* <p>{@code allocationSize} matches {@code hibernate.jdbc.batch_size} in
* {@code application-insert-sequence.yml} on purpose: a mismatched allocation size is its own
* classic footgun (extra round trips to refill the sequence pool mid-batch) and not one this
* repo is trying to demonstrate here.
*/
@Entity
public class WidgetSequence {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_seq")
@SequenceGenerator(name = "widget_seq", sequenceName = "widget_seq", allocationSize = 25)
private Long id;
private String name;
protected WidgetSequence() {
// JPA
}
public WidgetSequence(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
@@ -0,0 +1,25 @@
package com.ankurm.hibernatedemo.namedquery;
/** JPQL/native constructor-result projection target. Plain class version. */
public class EmployeeDto {
private final Long id;
private final String firstName;
public EmployeeDto(Long id, String firstName) {
this.id = id;
this.firstName = firstName;
}
public Long getId() {
return id;
}
public String getFirstName() {
return firstName;
}
@Override
public String toString() {
return "EmployeeDto{id=%s, firstName=%s}".formatted(id, firstName);
}
}
@@ -0,0 +1,8 @@
package com.ankurm.hibernatedemo.namedquery;
/**
* Jakarta Persistence 3.2 alternative to a hand-written DTO class: does a Java {@code record}
* work directly as a JPQL constructor-expression result? Tested in NamedQueryExecutionTest.
*/
public record EmployeeRecordDto(Long id, String firstName) {
}
@@ -0,0 +1,38 @@
package com.ankurm.hibernatedemo.namedquery;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.NamedQuery;
/**
* Uses {@code org.hibernate.annotations.NamedQuery} (the Hibernate extension, not the JPA
* standard one) specifically to exercise an extra it offers that JPA's does not:
* {@code cacheable = true}. Docs: 14-named-queries.md, chapter "jakarta vs hibernate NamedQuery".
*/
@Entity
@Table(name = "hib_extra_employee")
@NamedQuery(name = "HibernateExtraEmployee.cacheableFindAll",
query = "SELECT e FROM HibernateExtraEmployee e",
cacheable = true)
public class HibernateExtraEmployee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
protected HibernateExtraEmployee() {
}
public HibernateExtraEmployee(String name) {
this.name = name;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,72 @@
package com.ankurm.hibernatedemo.namedquery;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.NamedNativeQueries;
import jakarta.persistence.NamedNativeQuery;
import jakarta.persistence.SqlResultSetMapping;
import jakarta.persistence.ConstructorResult;
import jakarta.persistence.ColumnResult;
import jakarta.persistence.Table;
/**
* Backs ankurm.com post 4877. Docs: 14-named-queries.md.
*
* <p>{@code Employee.findByName} is a valid JPA {@code @NamedQuery}, {@code Employee.byNativeDto}
* is a {@code @NamedNativeQuery} + {@code @SqlResultSetMapping} into {@link EmployeeDto} via
* {@code @ConstructorResult}.
*/
@Entity
@Table(name = "nq_employee")
@NamedQueries({
@NamedQuery(name = "Employee.findByName", query = "SELECT e FROM NqEmployee e WHERE e.firstName = :name"),
@NamedQuery(name = "Employee.findAllActive", query = "SELECT e FROM NqEmployee e WHERE e.status = 'ACTIVE' ORDER BY e.id DESC")
})
@NamedNativeQueries({
@NamedNativeQuery(
name = "Employee.byNativeDto",
query = "SELECT id, first_name AS firstName FROM nq_employee WHERE status = :status",
resultSetMapping = "EmployeeDtoMapping")
})
@SqlResultSetMapping(
name = "EmployeeDtoMapping",
classes = @ConstructorResult(
targetClass = EmployeeDto.class,
columns = {
@ColumnResult(name = "id", type = Long.class),
@ColumnResult(name = "firstName", type = String.class)
}))
public class NqEmployee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String status;
protected NqEmployee() {
}
public NqEmployee(String firstName, String status) {
this.firstName = firstName;
this.status = status;
}
public Long getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public String getStatus() {
return status;
}
}
@@ -0,0 +1,46 @@
package com.ankurm.hibernatedemo.namedquery;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.Table;
/**
* Has one {@code @NamedQuery} defined via annotation ({@code XmlQueryEmployee.findBySalaryAbove})
* AND one defined via {@code orm.xml} ({@code XmlQueryEmployee.findBySalaryAboveXml}), plus an
* XML-defined query that OVERRIDES a same-named annotated one
* ({@code XmlQueryEmployee.overridden}) -- see src/main/resources/META-INF/orm.xml. Docs:
* 14-named-queries.md, chapter "Named queries in orm.xml".
*/
@Entity
@Table(name = "xml_query_employee")
@NamedQuery(name = "XmlQueryEmployee.findBySalaryAbove", query = "SELECT e FROM XmlQueryEmployee e WHERE e.salary > :min")
@NamedQuery(name = "XmlQueryEmployee.overridden", query = "SELECT e FROM XmlQueryEmployee e WHERE e.salary < 0") // deliberately wrong; orm.xml should win
public class XmlQueryEmployee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double salary;
protected XmlQueryEmployee() {
}
public XmlQueryEmployee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
@@ -0,0 +1,54 @@
package com.ankurm.hibernatedemo.naturalid;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.NaturalId;
import org.hibernate.annotations.NaturalIdCache;
/**
* Same shape as {@link NaturalIdProduct}, but with {@code @Cacheable} + {@code @Cache} (entity
* L2 cache) AND {@code @NaturalIdCache} (the SEPARATE natural-id-to-PK resolution L2 cache
* region -- these are two different cache regions, not one).
*
* <p>Docs: docs/06-natural-ids.md, Topic 3.
*/
@Entity
@jakarta.persistence.Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@NaturalIdCache
public class CachedNaturalIdProduct {
@Id
@GeneratedValue
private Long id;
@NaturalId
@Column(nullable = false, unique = true, updatable = false)
private String sku;
private String name;
public CachedNaturalIdProduct() {
}
public CachedNaturalIdProduct(String sku, String name) {
this.sku = sku;
this.name = name;
}
public Long getId() {
return id;
}
public String getSku() {
return sku;
}
public String getName() {
return name;
}
}
@@ -0,0 +1,35 @@
package com.ankurm.hibernatedemo.naturalid;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
/**
* Parent side of the composite natural id demo -- see {@link Department}.
*
* <p>Docs: docs/06-natural-ids.md, Topic 3.
*/
@Entity
public class Company {
@Id
@GeneratedValue
private Long id;
private String name;
public Company() {
}
public Company(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
@@ -0,0 +1,58 @@
package com.ankurm.hibernatedemo.naturalid;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import org.hibernate.annotations.NaturalId;
/**
* A COMPOSITE natural id: (company, deptCode) together must be unique, not either field alone.
* Matches the article's example -- verified to actually load via
* {@code session.byNaturalId(Department.class).using(...).using(...).load()}.
*
* <p>Docs: docs/06-natural-ids.md, Topic 3.
*/
@Entity
public class Department {
@Id
@GeneratedValue
private Long id;
@NaturalId
@ManyToOne
@JoinColumn(name = "company_id")
private Company company;
@NaturalId
private String deptCode;
private String name;
public Department() {
}
public Department(Company company, String deptCode, String name) {
this.company = company;
this.deptCode = deptCode;
this.name = name;
}
public Long getId() {
return id;
}
public Company getCompany() {
return company;
}
public String getDeptCode() {
return deptCode;
}
public String getName() {
return name;
}
}
@@ -0,0 +1,44 @@
package com.ankurm.hibernatedemo.naturalid;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import org.hibernate.annotations.NaturalId;
/**
* {@code @NaturalId} with NO {@code mutable} attribute -- defaults to {@code mutable = false}
* (immutable). Deliberately does NOT add {@code @Column(updatable = false)}, so if Hibernate
* lets an UPDATE through at the SQL level, that is Hibernate's own natural-id immutability
* enforcement failing to stop it, not a JPA column-level guard doing the stopping.
*
* <p>Docs: docs/06-natural-ids.md, Topic 3.
*/
@Entity
public class ImmutableNaturalIdEntity {
@Id
@GeneratedValue
private Long id;
@NaturalId // mutable defaults to false
private String code;
public ImmutableNaturalIdEntity() {
}
public ImmutableNaturalIdEntity(String code) {
this.code = code;
}
public Long getId() {
return id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
@@ -0,0 +1,42 @@
package com.ankurm.hibernatedemo.naturalid;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import org.hibernate.annotations.NaturalId;
/**
* {@code @NaturalId(mutable = true)} -- the explicit opt-in for a natural id that IS allowed
* to change (e.g. an email address).
*
* <p>Docs: docs/06-natural-ids.md, Topic 3.
*/
@Entity
public class MutableNaturalIdEntity {
@Id
@GeneratedValue
private Long id;
@NaturalId(mutable = true)
private String code;
public MutableNaturalIdEntity() {
}
public MutableNaturalIdEntity(String code) {
this.code = code;
}
public Long getId() {
return id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
@@ -0,0 +1,62 @@
package com.ankurm.hibernatedemo.naturalid;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import java.util.Objects;
import org.hibernate.Hibernate;
import org.hibernate.annotations.NaturalId;
/**
* equals()/hashCode() based on the IMMUTABLE natural id (sku), assigned in the constructor --
* exactly the pattern posts 4864/4865 recommend. Since sku is set before the object ever enters
* a Set/Map (unlike a surrogate id, which is null until flush), the hash code should be STABLE
* across persist(). This entity exists to test whether that advice actually holds up.
*
* <p>Docs: docs/06-natural-ids.md, Topic 3.
*/
@Entity
public class NaturalIdEqualsEntity {
@Id
@GeneratedValue
private Long id;
@NaturalId
@Column(nullable = false, unique = true, updatable = false)
private final String sku;
private String name;
protected NaturalIdEqualsEntity() {
this.sku = null; // JPA no-arg constructor requirement
}
public NaturalIdEqualsEntity(String sku, String name) {
this.sku = sku;
this.name = name;
}
public Long getId() {
return id;
}
public String getSku() {
return sku;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if (Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
NaturalIdEqualsEntity that = (NaturalIdEqualsEntity) o;
return Objects.equals(sku, that.sku);
}
@Override
public int hashCode() {
return Objects.hash(sku);
}
}
@@ -0,0 +1,52 @@
package com.ankurm.hibernatedemo.naturalid;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import org.hibernate.annotations.NaturalId;
/**
* A single-field, IMMUTABLE natural id (the JPA/Hibernate default for {@code @NaturalId}).
* No {@code @Cache}/{@code @NaturalIdCache} here -- this is the baseline entity for the
* "does bySimpleNaturalId save a query without L2 cache" experiment.
*
* <p>Docs: docs/06-natural-ids.md, Topic 3.
*/
@Entity
public class NaturalIdProduct {
@Id
@GeneratedValue
private Long id;
@NaturalId
@Column(nullable = false, unique = true, updatable = false)
private String sku;
private String name;
public NaturalIdProduct() {
}
public NaturalIdProduct(String sku, String name) {
this.sku = sku;
this.name = name;
}
public Long getId() {
return id;
}
public String getSku() {
return sku;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@@ -0,0 +1,62 @@
package com.ankurm.hibernatedemo.pagination;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import java.util.ArrayList;
import java.util.List;
/**
* Chapter 23's pagination playground. {@code comments} exists specifically so a {@code join
* fetch} + pagination combination has a real collection to trigger Hibernate's in-memory
* fallback warning against.
*
* <p>Docs: docs/23-pagination.md.
*/
@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private int sequence;
@OneToMany(mappedBy = "article", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
protected Article() {
// for Hibernate
}
public Article(String title, int sequence) {
this.title = title;
this.sequence = sequence;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public int getSequence() {
return sequence;
}
public List<Comment> getComments() {
return comments;
}
public void addComment(Comment comment) {
comment.setArticle(this);
comments.add(comment);
}
}
@@ -0,0 +1,49 @@
package com.ankurm.hibernatedemo.pagination;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
/**
* Docs: docs/23-pagination.md.
*/
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String body;
@ManyToOne
@JoinColumn(name = "article_id")
private Article article;
protected Comment() {
// for Hibernate
}
public Comment(String body) {
this.body = body;
}
public Long getId() {
return id;
}
public String getBody() {
return body;
}
public Article getArticle() {
return article;
}
public void setArticle(Article article) {
this.article = article;
}
}
@@ -0,0 +1,49 @@
package com.ankurm.hibernatedemo.persistenceannotations;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
/**
* {@code status} has NO {@code @Enumerated} at all -- JPA's default is ORDINAL. This entity
* exists purely to demonstrate what breaks: reordering (or inserting into the middle of) the
* enum silently repoints every stored ordinal at the wrong constant.
*
* <p>Docs: docs/05-jpa-persistence-annotations.md, Topic 2.
*/
@Entity
@Table(name = "enum_default_ordinal_entity")
public class EnumDefaultOrdinalEntity {
@Id
@GeneratedValue
private Long id;
// No @Enumerated -- JPA default is ORDINAL.
private OrderStatus status;
public EnumDefaultOrdinalEntity() {
}
public EnumDefaultOrdinalEntity(OrderStatus status) {
this.status = status;
}
public Long getId() {
return id;
}
public OrderStatus getStatus() {
return status;
}
public void setStatus(OrderStatus status) {
this.status = status;
}
/** V1 order of constants: NEW=0, SHIPPED=1, DELIVERED=2. */
public enum OrderStatus {
NEW, SHIPPED, DELIVERED
}
}
@@ -0,0 +1,40 @@
package com.ankurm.hibernatedemo.persistenceannotations;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
/**
* Maps the SAME table ({@code enum_default_ordinal_entity}) as {@link EnumDefaultOrdinalEntity},
* but with a status enum that has an EXTRA constant inserted before {@code SHIPPED}. This
* simulates "someone added a constant to the middle of the enum" without a migration --
* the classic ORDINAL trap. No @Enumerated here either (default ORDINAL), matching the
* original mapping exactly.
*
* <p>Docs: docs/05-jpa-persistence-annotations.md, Topic 2.
*/
@Entity
@Table(name = "enum_default_ordinal_entity")
public class EnumReorderedV2Entity {
@Id
private Long id;
@Column(name = "status")
private ReorderedStatus status;
public Long getId() {
return id;
}
public ReorderedStatus getStatus() {
return status;
}
/** V2: a constant (PENDING_REVIEW) was inserted BEFORE SHIPPED -- ordinal 1 now means something else. */
public enum ReorderedStatus {
NEW, PENDING_REVIEW, SHIPPED, DELIVERED
}
}
@@ -0,0 +1,61 @@
package com.ankurm.hibernatedemo.persistenceannotations;
import jakarta.persistence.Entity;
import jakarta.persistence.Enumerated;
import jakarta.persistence.EnumType;
import jakarta.persistence.EnumeratedValue;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
/**
* {@code @EnumeratedValue} is NEW in Jakarta Persistence 3.2 (confirmed present via javap on
* jakarta.persistence-api 3.2.0 -- it does not exist in 3.1). It lets an enum control its own
* persisted representation (a custom code), instead of ORDINAL or STRING.
*
* <p>Docs: docs/05-jpa-persistence-annotations.md, Topic 2.
*/
@Entity
public class EnumeratedValueEntity {
@Id
@GeneratedValue
private Long id;
// @EnumeratedValue's own field type (String) determines the persisted column type, but
// Hibernate still needs to be told this is a STRING-shaped enum, not the ORDINAL default --
// otherwise boot fails with "@EnumeratedValue for EnumType.ORDINAL must be placed on a
// field whose type is byte, short, or int".
@Enumerated(EnumType.STRING)
private Priority priority;
public EnumeratedValueEntity() {
}
public EnumeratedValueEntity(Priority priority) {
this.priority = priority;
}
public Long getId() {
return id;
}
public Priority getPriority() {
return priority;
}
/** Persisted representation is the "code" string, NOT the enum's ordinal or name(). */
public enum Priority {
LOW("L"), MEDIUM("M"), HIGH("H");
@EnumeratedValue
private final String code;
Priority(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
}
@@ -0,0 +1,47 @@
package com.ankurm.hibernatedemo.persistenceannotations;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import java.util.Objects;
/**
* The anti-pattern the article warns about in Q5: {@code equals()}/{@code hashCode()} based on
* the SURROGATE {@code @GeneratedValue} id. Before the first flush, {@code id} is null, so the
* object's hash code is fixed at "hash of null" -- then flush() mutates {@code id}, changing the
* hash code out from under any HashSet/HashMap the object is already sitting in.
*
* <p>Docs: docs/05-jpa-persistence-annotations.md, Topic 2.
*/
@Entity
public class IdBasedEqualsEntity {
@Id
@GeneratedValue
private Long id;
private String label;
public IdBasedEqualsEntity() {
}
public IdBasedEqualsEntity(String label) {
this.label = label;
}
public Long getId() {
return id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof IdBasedEqualsEntity that)) return false;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
@@ -0,0 +1,41 @@
package com.ankurm.hibernatedemo.persistenceannotations;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
/**
* Deliberately relies on the JPA/Hibernate DEFAULT {@code equals()}/{@code hashCode()}
* (identity-based, inherited from Object) with a surrogate {@code @GeneratedValue} id that is
* null until the first flush. This is the entity used to reproduce the classic
* "HashSet.contains() returns false after persist()" symptom.
*
* <p>Docs: docs/05-jpa-persistence-annotations.md, Topic 2.
*/
@Entity
public class IdentityHashSetEntity {
@Id
@GeneratedValue
private Long id;
private String label;
public IdentityHashSetEntity() {
}
public IdentityHashSetEntity(String label) {
this.label = label;
}
public Long getId() {
return id;
}
public String getLabel() {
return label;
}
// Deliberately NOT overriding equals()/hashCode() -- uses Object identity.
// A second class below overrides them using the surrogate id, to show that trap too.
}
@@ -0,0 +1,40 @@
package com.ankurm.hibernatedemo.persistenceannotations;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import java.util.Map;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
/**
* {@code @JdbcTypeCode(SqlTypes.JSON)} on a {@code Map<String,Object>} field, tested against
* H2 2.4.240 -- the article recommends it without saying whether it actually works on H2.
*
* <p>Docs: docs/05-jpa-persistence-annotations.md, Topic 2.
*/
@Entity
public class JsonColumnEntity {
@Id
@GeneratedValue
private Long id;
@JdbcTypeCode(SqlTypes.JSON)
private Map<String, Object> details;
public JsonColumnEntity() {
}
public JsonColumnEntity(Map<String, Object> details) {
this.details = details;
}
public Long getId() {
return id;
}
public Map<String, Object> getDetails() {
return details;
}
}
@@ -0,0 +1,76 @@
package com.ankurm.hibernatedemo.persistenceannotations;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Transient;
/**
* Default access is FIELD (because {@code @Id} is annotated on the field). One property,
* {@code computedLabel}, is explicitly switched to PROPERTY access via {@code @Access} on its
* getter, with a derivation the FIELD side does not know about. This reproduces the classic
* "mixed access silently reads the wrong value" symptom: a raw field mutation is invisible to
* Hibernate once a property is PROPERTY-access, and vice versa.
*
* <p>Docs: docs/05-jpa-persistence-annotations.md, Topic 2.
*/
@Entity
public class MixedAccessEntity {
@Id
@GeneratedValue
private Long id;
private String rawValue;
// FIELD access is the entity default (because @Id is on a field). This backing field is
// deliberately never read directly by Hibernate for the "label" property -- only the
// PROPERTY-access getter below is.
@Transient
private int getterCallCount = 0;
public MixedAccessEntity() {
}
public MixedAccessEntity(String rawValue) {
this.rawValue = rawValue;
}
public Long getId() {
return id;
}
public String getRawValue() {
return rawValue;
}
public void setRawValue(String rawValue) {
this.rawValue = rawValue;
}
/**
* Explicitly PROPERTY access: Hibernate calls this getter (not a field read) to determine
* the persisted value for the "computedLabel" mapped property.
*/
@Access(AccessType.PROPERTY)
@Column(name = "computed_label")
public String getComputedLabel() {
getterCallCount++;
return rawValue == null ? null : rawValue.toUpperCase();
}
// Hibernate's default PROPERTY-access strategy REQUIRES a setter even for a logically
// read-only derived column -- omitting it throws PropertyNotFoundException at boot
// ("Could not locate setter method for property 'computedLabel'"). This setter is a
// deliberate no-op: it exists purely to satisfy that requirement.
public void setComputedLabel(String ignored) {
// no-op: computedLabel is derived from rawValue, never written directly
}
public int getGetterCallCount() {
return getterCallCount;
}
}
@@ -0,0 +1,11 @@
package com.ankurm.hibernatedemo.persistenceannotations;
/**
* A Java record used as a JPQL constructor-expression result type -- new in Jakarta
* Persistence 3.2 (JPQL constructor expressions may target a record, matching a canonical
* constructor by position/type).
*
* <p>Docs: docs/05-jpa-persistence-annotations.md, Topic 2.
*/
public record PriorityCountView(EnumeratedValueEntity.Priority priority, long total) {
}
@@ -0,0 +1,42 @@
package com.ankurm.hibernatedemo.persistenceannotations;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import java.time.LocalDate;
/**
* {@code @Temporal} is {@code @Deprecated(since = "3.2")} in jakarta.persistence-api 3.2.0
* (confirmed via javap on the annotation class file). This entity puts it on a
* {@code java.time.LocalDate} field anyway, to see whether Hibernate 7.4.5 ignores it,
* warns, or throws at boot.
*
* <p>Docs: docs/05-jpa-persistence-annotations.md, Topic 2.
*/
@Entity
public class TemporalOnLocalDateEntity {
@Id
@GeneratedValue
private Long id;
@Temporal(TemporalType.DATE)
private LocalDate eventDate;
public TemporalOnLocalDateEntity() {
}
public TemporalOnLocalDateEntity(LocalDate eventDate) {
this.eventDate = eventDate;
}
public Long getId() {
return id;
}
public LocalDate getEventDate() {
return eventDate;
}
}
@@ -0,0 +1,30 @@
package com.ankurm.hibernatedemo.procedure;
/**
* Plain DTO (NOT an @Entity) used as the {@code @ConstructorResult} target for
* {@code EmployeeSummaryMapping} in {@link ProcEmployee}, backing the "result set mapped to a
* DTO via @SqlResultSetMapping" requirement in docs/08-stored-procedures.md.
*/
public class EmployeeSummary {
private final Integer id;
private final String name;
public EmployeeSummary(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "EmployeeSummary{id=%s, name=%s}".formatted(id, name);
}
}
@@ -0,0 +1,87 @@
package com.ankurm.hibernatedemo.procedure;
import jakarta.persistence.Column;
import jakarta.persistence.ColumnResult;
import jakarta.persistence.ConstructorResult;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.NamedStoredProcedureQueries;
import jakarta.persistence.NamedStoredProcedureQuery;
import jakarta.persistence.ParameterMode;
import jakarta.persistence.SqlResultSetMapping;
import jakarta.persistence.SqlResultSetMappings;
import jakarta.persistence.StoredProcedureParameter;
import jakarta.persistence.Table;
import java.math.BigDecimal;
/**
* Entity for docs/08-stored-procedures.md (merged posts 4867 + 4881). Backed by HSQLDB 2.7.3
* real SQL/PSM stored procedures created in {@code ProcedureSchemaSupport} -- these are not
* described in prose, they are compiled and executed.
*/
@Entity
@Table(name = "PROC_EMPLOYEES")
@NamedStoredProcedureQueries({
@NamedStoredProcedureQuery(
name = "ProcEmployee.getTax",
procedureName = "GET_TAX",
parameters = {
@StoredProcedureParameter(mode = ParameterMode.IN, name = "emp_id", type = Integer.class),
@StoredProcedureParameter(mode = ParameterMode.OUT, name = "tax_amount", type = BigDecimal.class)
}
),
@NamedStoredProcedureQuery(
name = "ProcEmployee.listAll",
procedureName = "LIST_EMPLOYEES",
resultClasses = ProcEmployee.class
)
})
@SqlResultSetMappings({
@SqlResultSetMapping(
name = "EmployeeSummaryMapping",
classes = @ConstructorResult(
targetClass = EmployeeSummary.class,
columns = {
@ColumnResult(name = "ID", type = Integer.class),
@ColumnResult(name = "NAME", type = String.class)
}
)
)
})
public class ProcEmployee {
@Id
private Integer id;
private String name;
@Column(precision = 10, scale = 2)
private BigDecimal salary;
protected ProcEmployee() {
// JPA
}
public ProcEmployee(Integer id, String name, BigDecimal salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public BigDecimal getSalary() {
return salary;
}
@Override
public String toString() {
return "ProcEmployee{id=%s, name=%s, salary=%s}".formatted(id, name, salary);
}
}
@@ -0,0 +1,76 @@
package com.ankurm.hibernatedemo.proxy;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.NamedAttributeNode;
import jakarta.persistence.NamedEntityGraph;
import jakarta.persistence.OneToMany;
import java.util.ArrayList;
import java.util.List;
/**
* Backs docs/11-proxies-and-lazy-initialization.md (post 4870 rewrite). Two associations on purpose:
*
* <ul>
* <li>{@code publisher} -- {@code @ManyToOne} with the JPA-default fetch type, EAGER.
* Nothing in this class overrides it.</li>
* <li>{@code reviews} -- {@code @OneToMany}, explicitly LAZY, and the only attribute named
* in {@code Book.reviews-only}. This is the association the fetchgraph/loadgraph demo
* turns on and off.</li>
* </ul>
*
* <p>With a {@code jakarta.persistence.loadgraph} hint carrying {@code Book.reviews-only},
* {@code reviews} gets join-fetched AND {@code publisher} still gets its default EAGER join --
* loadgraph only promotes attributes, it never demotes ones the mapping already marks EAGER.
* With a {@code jakarta.persistence.fetchgraph} hint carrying the same named graph,
* {@code reviews} still gets join-fetched, but {@code publisher} is forced down to LAZY (a
* proxy, no join) even though the mapping says EAGER -- fetchgraph treats the graph as the
* complete fetch plan, not an addition to the mapping's own defaults.
*/
@Entity
@NamedEntityGraph(name = "Book.reviews-only", attributeNodes = @NamedAttributeNode("reviews"))
public class ProxyBook {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne
@JoinColumn(name = "publisher_id")
private ProxyPublisher publisher;
@OneToMany(mappedBy = "book", fetch = FetchType.LAZY)
private List<ProxyReview> reviews = new ArrayList<>();
protected ProxyBook() {
// JPA
}
public ProxyBook(String title, ProxyPublisher publisher) {
this.title = title;
this.publisher = publisher;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public ProxyPublisher getPublisher() {
return publisher;
}
public List<ProxyReview> getReviews() {
return reviews;
}
}
@@ -0,0 +1,39 @@
package com.ankurm.hibernatedemo.proxy;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
/**
* The default-fetch-type EAGER side of the entity-graph demo in docs/11-proxies-and-lazy-initialization.md
* (fetchgraph vs loadgraph). {@link ProxyBook#publisher} points here with the default
* {@code @ManyToOne} fetch type, which is EAGER -- deliberately, so that a
* {@code jakarta.persistence.fetchgraph} hint has something to force back to LAZY that a
* {@code jakarta.persistence.loadgraph} hint leaves alone.
*/
@Entity
public class ProxyPublisher {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
protected ProxyPublisher() {
// JPA
}
public ProxyPublisher(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
@@ -0,0 +1,44 @@
package com.ankurm.hibernatedemo.proxy;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
/**
* Child side of {@link ProxyBook#reviews}. See {@link ProxyBook} for why this association
* exists and how the entity-graph demo in docs/11-proxies-and-lazy-initialization.md uses it.
*/
@Entity
public class ProxyReview {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String comment;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "book_id")
private ProxyBook book;
protected ProxyReview() {
// JPA
}
public ProxyReview(String comment, ProxyBook book) {
this.comment = comment;
this.book = book;
}
public Long getId() {
return id;
}
public String getComment() {
return comment;
}
}
@@ -0,0 +1,49 @@
package com.ankurm.hibernatedemo.query;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
/**
* Shared by the HQL chapter (15) and the Criteria API chapter (16) &mdash; both posts use the
* same Employee/Department pair in their examples, so one real mapping backs both rather than
* two near-duplicates.
*
* <p>Named {@code QueryDept} (entity name, via {@code @Entity(name = ...)}), not {@code
* Department}, because chapter 06's {@code naturalid.Department} already claims that entity
* name in this same persistence unit -- Hibernate requires distinct entity names project-wide,
* not just distinct class names.
*
* <p>Docs: docs/15-hql-queries.md, docs/16-criteria-queries.md.
*/
@Entity(name = "QueryDept")
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
protected Department() {
// JPA
}
public Department(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Department{id=%s, name=%s}".formatted(id, name);
}
}
@@ -0,0 +1,97 @@
package com.ankurm.hibernatedemo.query;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import java.time.LocalDate;
/**
* Shared by the HQL chapter (15) and the Criteria API chapter (16).
*
* <p>Docs: docs/15-hql-queries.md, docs/16-criteria-queries.md.
*
* <p>{@code status} and {@code hireDate} exist specifically for the bulk-update/bulk-delete
* scenarios both chapters cover ({@code UPDATE ... WHERE lastLogin/hireDate < :cutoff}-shaped
* queries), and {@code department} is LAZY so the fetch-join chapter has a real N+1 to avoid
* rather than an asserted one.
*/
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private Double salary;
private String status;
private LocalDate hireDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
protected Employee() {
// JPA
}
public Employee(String firstName, String lastName, Double salary, String status, LocalDate hireDate, Department department) {
this.firstName = firstName;
this.lastName = lastName;
this.salary = salary;
this.status = status;
this.hireDate = hireDate;
this.department = department;
}
public Long getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public LocalDate getHireDate() {
return hireDate;
}
public Department getDepartment() {
return department;
}
@Override
public String toString() {
return "Employee{id=%s, firstName=%s, lastName=%s, salary=%s, status=%s}"
.formatted(id, firstName, lastName, salary, status);
}
}
@@ -0,0 +1,154 @@
package com.ankurm.hibernatedemo.scenario;
import com.ankurm.hibernatedemo.model.Book;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import org.hibernate.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
/**
* Backs ankurm.com post 4859 ("Hibernate 7: get() vs load()") and
* docs/01-get-vs-load.md. Captured verbatim into docs/output/get-vs-load.txt by
* {@code scripts/run.sh getvsload}.
*
* <p>Each step opens its own {@link EntityManager} deliberately, so the SQL log lines that
* bracket a step are unambiguously that step's own traffic &mdash; there is no shared session
* whose first-level cache could quietly answer a later {@code get()} for free.
*/
@Component
@Profile("getvsload")
public class GetVsLoadRunner implements CommandLineRunner {
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
private final EntityManagerFactory emf;
public GetVsLoadRunner(EntityManagerFactory emf) {
this.emf = emf;
}
@Override
public void run(String... args) {
Long existingId = seedOneBook();
long missingId = existingId + 999_000L;
step1_getExisting(existingId);
step2_getMissing(missingId);
step3_getReferenceExisting_noSelectUntilAccessed(existingId);
step4_getReferenceMissing_exceptionOnlyOnAccess(missingId);
step5_getReferenceThenSessionClosed_lazyInitException(existingId);
step6_proxyVsRealIdentity(existingId);
}
private Long seedOneBook() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Book book = new Book("Effective Java", "Joshua Bloch");
em.persist(book);
em.getTransaction().commit();
Long id = book.getId();
em.close();
DEMO.info("SEED: inserted Book id={}", id);
return id;
}
private void step1_getExisting(Long id) {
DEMO.info("--- Step 1: session.get() on an existing id ---");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Session session = em.unwrap(Session.class);
DEMO.info("about to call session.get(Book.class, {})", id);
Book book = session.get(Book.class, id);
DEMO.info("get() returned: {}", book);
em.getTransaction().commit();
em.close();
}
private void step2_getMissing(long missingId) {
DEMO.info("--- Step 2: session.get() on a missing id ---");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Session session = em.unwrap(Session.class);
DEMO.info("about to call session.get(Book.class, {})", missingId);
Book book = session.get(Book.class, missingId);
DEMO.info("get() returned: {} (no exception thrown)", book);
em.getTransaction().commit();
em.close();
}
private void step3_getReferenceExisting_noSelectUntilAccessed(Long id) {
DEMO.info("--- Step 3: session.getReference() on an existing id ---");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Session session = em.unwrap(Session.class);
Book proxy = session.getReference(Book.class, id);
DEMO.info("getReference() returned proxy of class {} -- no SELECT above this line", proxy.getClass().getName());
DEMO.info("now calling proxy.getTitle() ...");
String title = proxy.getTitle();
DEMO.info("getTitle() returned '{}' -- the SELECT for this ran just above this line", title);
em.getTransaction().commit();
em.close();
}
private void step4_getReferenceMissing_exceptionOnlyOnAccess(long missingId) {
DEMO.info("--- Step 4: session.getReference() on a missing id ---");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Session session = em.unwrap(Session.class);
Book proxy = session.getReference(Book.class, missingId);
DEMO.info("getReference() returned a proxy for a row that does not exist -- no exception yet: {}", proxy.getClass().getName());
try {
proxy.getTitle();
DEMO.info("no exception -- this line should be unreachable");
} catch (RuntimeException e) {
DEMO.info("accessing the proxy threw {}: {}", e.getClass().getName(), e.getMessage());
}
em.getTransaction().rollback();
em.close();
}
private void step5_getReferenceThenSessionClosed_lazyInitException(Long id) {
DEMO.info("--- Step 5: proxy accessed after its session is closed ---");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Session session = em.unwrap(Session.class);
Book proxy = session.getReference(Book.class, id);
em.getTransaction().commit();
em.close();
DEMO.info("session closed. proxy in hand: {}", proxy.getClass().getName());
try {
proxy.getTitle();
DEMO.info("no exception -- this line should be unreachable");
} catch (RuntimeException e) {
DEMO.info("accessing the proxy after close threw {}: {}", e.getClass().getName(), e.getMessage());
}
}
private void step6_proxyVsRealIdentity(Long id) {
DEMO.info("--- Step 6: proxy identity vs a real loaded instance ---");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Session session = em.unwrap(Session.class);
Book real = session.get(Book.class, id);
// second em/session so this is a genuinely separate proxy, not the same cached instance
EntityManager em2 = emf.createEntityManager();
em2.getTransaction().begin();
Session session2 = em2.unwrap(Session.class);
Book proxy = session2.getReference(Book.class, id);
DEMO.info("real.getClass() = {}", real.getClass().getName());
DEMO.info("proxy.getClass() = {}", proxy.getClass().getName());
DEMO.info("proxy instanceof Book.class: {}", Book.class.isInstance(proxy));
DEMO.info("real.getClass() == proxy.getClass(): {}", real.getClass() == proxy.getClass());
DEMO.info("real.equals(proxy) before proxy access: {}", real.equals(proxy));
em.getTransaction().commit();
em.close();
em2.getTransaction().commit();
em2.close();
}
}
@@ -0,0 +1,58 @@
package com.ankurm.hibernatedemo.scenario;
import com.ankurm.hibernatedemo.model.WidgetIdentity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import org.hibernate.SessionFactory;
import org.hibernate.stat.Statistics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
/**
* Backs ankurm.com post 4861 ("inserting objects efficiently") and
* docs/03-inserting-objects.md. Captured verbatim into docs/output/insert-identity.txt by
* {@code scripts/run.sh insert-identity}.
*
* <p>Same {@code hibernate.jdbc.batch_size} and {@code hibernate.order_inserts} settings as
* {@link InsertSequenceRunner} -- the only difference is {@link WidgetIdentity}'s
* {@code GenerationType.IDENTITY} strategy. Compare the two captured output files directly;
* the diff between them is the entire point of this pair.
*/
@Component
@Profile("insert-identity")
public class InsertIdentityRunner implements CommandLineRunner {
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
private static final int ROW_COUNT = 30;
private final EntityManagerFactory emf;
public InsertIdentityRunner(EntityManagerFactory emf) {
this.emf = emf;
}
@Override
public void run(String... args) {
SessionFactory sessionFactory = emf.unwrap(SessionFactory.class);
Statistics stats = sessionFactory.getStatistics();
stats.clear();
DEMO.info("--- inserting {} WidgetIdentity rows (GenerationType.IDENTITY) ---", ROW_COUNT);
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
for (int i = 1; i <= ROW_COUNT; i++) {
em.persist(new WidgetIdentity("identity-" + i));
}
em.getTransaction().commit();
em.close();
DEMO.info("entityInsertCount = {}", stats.getEntityInsertCount());
DEMO.info("prepareStatementCount = {}", stats.getPrepareStatementCount());
DEMO.info("(with IDENTITY, expect prepareStatementCount to land close to entityInsertCount --" +
" each insert has to go to the database immediately to hand back the generated key,"
+ " so there is nothing left for hibernate.jdbc.batch_size to batch)");
}
}
@@ -0,0 +1,55 @@
package com.ankurm.hibernatedemo.scenario;
import com.ankurm.hibernatedemo.model.WidgetSequence;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import org.hibernate.SessionFactory;
import org.hibernate.stat.Statistics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
/**
* Backs ankurm.com post 4861 ("inserting objects efficiently") and
* docs/03-inserting-objects.md. Captured verbatim into docs/output/insert-sequence.txt by
* {@code scripts/run.sh insert-sequence}.
*
* <p>Same {@code hibernate.jdbc.batch_size} and {@code hibernate.order_inserts} settings as
* {@link InsertIdentityRunner} -- see that class's Javadoc for what this pair is demonstrating.
*/
@Component
@Profile("insert-sequence")
public class InsertSequenceRunner implements CommandLineRunner {
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
private static final int ROW_COUNT = 30;
private final EntityManagerFactory emf;
public InsertSequenceRunner(EntityManagerFactory emf) {
this.emf = emf;
}
@Override
public void run(String... args) {
SessionFactory sessionFactory = emf.unwrap(SessionFactory.class);
Statistics stats = sessionFactory.getStatistics();
stats.clear();
DEMO.info("--- inserting {} WidgetSequence rows (GenerationType.SEQUENCE, allocationSize=25) ---", ROW_COUNT);
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
for (int i = 1; i <= ROW_COUNT; i++) {
em.persist(new WidgetSequence("sequence-" + i));
}
em.getTransaction().commit();
em.close();
DEMO.info("entityInsertCount = {}", stats.getEntityInsertCount());
DEMO.info("prepareStatementCount = {}", stats.getPrepareStatementCount());
DEMO.info("(with SEQUENCE, the id is known before the row is written, so Hibernate can" +
" defer and batch the inserts -- expect prepareStatementCount well below entityInsertCount)");
}
}
@@ -0,0 +1,113 @@
package com.ankurm.hibernatedemo.scenario;
import com.ankurm.hibernatedemo.model.Book;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.OptimisticLockException;
import org.hibernate.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
/**
* Backs ankurm.com post 4860 ("merge() vs refresh()") and docs/02-merge-vs-refresh.md.
* Captured verbatim into docs/output/merge-vs-refresh.txt by
* {@code scripts/run.sh mergerefresh}.
*
* <p>{@link Book} carries a {@code @Version} column specifically so this scenario can show what
* {@code merge()} does when the detached instance it is given is holding a version older than
* what is currently in the database &mdash; not just what it does to an un-versioned row.
*/
@Component
@Profile("mergerefresh")
public class MergeVsRefreshRunner implements CommandLineRunner {
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
private final EntityManagerFactory emf;
public MergeVsRefreshRunner(EntityManagerFactory emf) {
this.emf = emf;
}
@Override
public void run(String... args) {
Long id = seedOneBook();
Book detached = loadThenDetach(id);
simulateAnotherProcessEditingTheRow(id);
mergeStaleDetachedInstance(detached);
refreshSilentlyDiscardsUnflushedEdit(id);
}
private Long seedOneBook() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Book book = new Book("Clean Code", "Robert C. Martin");
em.persist(book);
em.getTransaction().commit();
Long id = book.getId();
em.close();
DEMO.info("SEED: inserted {}", book);
return id;
}
private Book loadThenDetach(Long id) {
DEMO.info("--- Step 1: load the row, then close the session (entity is now detached) ---");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Session session = em.unwrap(Session.class);
Book book = session.get(Book.class, id);
em.getTransaction().commit();
em.close();
DEMO.info("detached instance in hand: {}", book);
return book;
}
private void simulateAnotherProcessEditingTheRow(Long id) {
DEMO.info("--- Step 2: a second, independent session edits the same row and commits ---");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Session session = em.unwrap(Session.class);
Book book = session.get(Book.class, id);
book.setTitle("Clean Code (2nd Edition)");
em.getTransaction().commit();
em.close();
DEMO.info("second session committed: {} -- version column has now advanced in the database", book);
}
private void mergeStaleDetachedInstance(Book detached) {
DEMO.info("--- Step 3: mutate the ORIGINAL detached instance (still holding the OLD version) and merge() it ---");
detached.setAuthor("Robert C. Martin (Uncle Bob)");
DEMO.info("detached instance before merge (note the version and title are both stale): {}", detached);
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Session session = em.unwrap(Session.class);
try {
Book merged = session.merge(detached);
em.getTransaction().commit();
DEMO.info("merge() succeeded, returned managed instance: {}", merged);
} catch (OptimisticLockException e) {
em.getTransaction().rollback();
DEMO.info("merge() threw {}: {}", e.getClass().getName(), e.getMessage());
DEMO.info("the title change from Step 2 survives untouched -- merge() refused to apply a write built on a stale version");
} finally {
em.close();
}
}
private void refreshSilentlyDiscardsUnflushedEdit(Long id) {
DEMO.info("--- Step 4: refresh() on a MANAGED entity with an unflushed local edit ---");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Session session = em.unwrap(Session.class);
Book book = session.get(Book.class, id);
book.setAuthor("SOMEONE ELSE ENTIRELY (never flushed)");
DEMO.info("before refresh(): {}", book);
session.refresh(book);
DEMO.info("after refresh(): {} -- the local edit is gone, no exception was thrown", book);
em.getTransaction().commit();
em.close();
}
}
@@ -0,0 +1,38 @@
package com.ankurm.hibernatedemo.search;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.KeywordField;
/**
* Not itself {@code @Indexed} -- it is only ever indexed as an embedded part of {@link Movie},
* via {@code @IndexedEmbedded}. Docs: docs/25-hibernate-search.md.
*/
@Entity
public class Director {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@KeywordField
private String name;
protected Director() {
// for Hibernate
}
public Director(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
@@ -0,0 +1,96 @@
package com.ankurm.hibernatedemo.search;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import org.hibernate.search.engine.backend.types.Sortable;
import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexedEmbedded;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDependency;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.KeywordField;
/**
* Chapter 25's Hibernate Search entity, pinned to {@code hibernate-search-mapper-orm}
* {@code 8.4.0.Final} (see {@code pom.xml} for why -- verified against Maven Central, NOT the
* {@code 7.3.2.Final} this repo's blog post previously claimed).
*
* <p>{@code title} is {@code @FullTextField} (analyzed, tokenized, fuzzy-matchable),
* {@code genre} is {@code @KeywordField} (stored whole, for exact-match filtering, never
* tokenized), {@code releaseYear} is a plain {@code @GenericField} marked sortable, and
* {@code director} is {@code @IndexedEmbedded} so a search on the director's name matches the
* movie without {@code Director} itself needing to be {@code @Indexed}.
*
* <p>Docs: docs/25-hibernate-search.md.
*/
@Entity
@Indexed
public class Movie {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// No explicit analyzer name: the Lucene backend ships no predefined "english" analyzer out
// of the box (confirmed the hard way -- naming one here without registering it via a
// LuceneAnalysisConfigurer bean fails the whole application context at startup with
// HSEARCH000353 "Unknown analyzer"). Omitting the attribute uses Hibernate Search's own
// built-in default full-text analyzer, which is enough for this chapter's examples.
@FullTextField
private String title;
@KeywordField
private String genre;
@GenericField(sortable = Sortable.YES)
private int releaseYear;
// @IndexedEmbedded on a @ManyToOne with no inverse side fails bootstrap outright
// (HSEARCH700020: "Unable to find the inverse side of the association") -- Hibernate Search
// needs to know how to find every Movie that embeds a given Director so it can reindex them
// when that Director changes. Director has no @OneToMany back-reference in this repo's
// model, and reindexing-on-director-update isn't something this chapter's tests exercise, so
// @IndexingDependency(reindexOnUpdate = SHALLOW) opts out of that automatic reindexing
// instead of adding a back-reference this model doesn't otherwise need.
@ManyToOne
@JoinColumn(name = "director_id")
@IndexedEmbedded
@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW)
private Director director;
protected Movie() {
// for Hibernate
}
public Movie(String title, String genre, int releaseYear, Director director) {
this.title = title;
this.genre = genre;
this.releaseYear = releaseYear;
this.director = director;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public String getGenre() {
return genre;
}
public int getReleaseYear() {
return releaseYear;
}
public Director getDirector() {
return director;
}
}
@@ -0,0 +1,24 @@
package com.ankurm.hibernatedemo.sorting;
import java.io.Serializable;
import java.util.Comparator;
/**
* A custom ordering for {@code @SortComparator} on {@link Playlist#getGenres()}: shortest name
* first, alphabetical as the tiebreaker. Hibernate instantiates this with a no-arg constructor
* via reflection, so it needs one (implicit here) and needs to be {@link Serializable} the same
* way any object that might end up in a Hibernate second-level cache entry does.
*
* <p>Docs: docs/22-sorting.md.
*/
public class LengthThenAlphaComparator implements Comparator<String>, Serializable {
@Override
public int compare(String a, String b) {
int byLength = Integer.compare(a.length(), b.length());
if (byLength != 0) {
return byLength;
}
return a.compareTo(b);
}
}
@@ -0,0 +1,101 @@
package com.ankurm.hibernatedemo.sorting;
import jakarta.persistence.CascadeType;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderBy;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import org.hibernate.annotations.SortComparator;
import org.hibernate.annotations.SortNatural;
/**
* Chapter 22's sorting playground: a {@code @OrderBy}-sorted list of {@link Song}, one
* {@code SortedSet} sorted by natural ordering, and one sorted by a custom comparator.
*
* <p>Docs: docs/22-sorting.md.
*/
@Entity
public class Playlist {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
/**
* {@code @OrderBy("title asc")} names the ENTITY PROPERTY {@code title}, not the mapped
* column {@link Song#getTitle()} is stored under ({@code song_title} -- deliberately
* different, see {@link Song}). Hibernate translates the property name to the right column
* itself; a raw column name here would be a coincidence at best and wrong the moment the
* column is renamed.
*/
@OneToMany(mappedBy = "playlist", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("title asc")
private List<Song> songs = new ArrayList<>();
/**
* Natural ordering (plain {@code String.compareTo}) -- {@code @SortNatural} tells Hibernate
* to keep this as a real, server-independent {@code TreeSet} in memory, not to add an
* {@code ORDER BY} to the collection's own fetch (there is no single "row order" for a
* many-valued element collection to sort by until it's loaded).
*/
@ElementCollection
@CollectionTable(name = "playlist_tag", joinColumns = @JoinColumn(name = "playlist_id"))
@Column(name = "tag")
@SortNatural
private SortedSet<String> tags = new TreeSet<>();
/**
* Same mechanism, a caller-supplied ordering instead of natural ordering: shortest name
* first, alphabetical as the tiebreaker.
*/
@ElementCollection
@CollectionTable(name = "playlist_genre", joinColumns = @JoinColumn(name = "playlist_id"))
@Column(name = "genre")
@SortComparator(LengthThenAlphaComparator.class)
private SortedSet<String> genres = new TreeSet<>(new LengthThenAlphaComparator());
protected Playlist() {
// for Hibernate
}
public Playlist(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public List<Song> getSongs() {
return songs;
}
public void addSong(Song song) {
song.setPlaylist(this);
songs.add(song);
}
public SortedSet<String> getTags() {
return tags;
}
public SortedSet<String> getGenres() {
return genres;
}
}
@@ -0,0 +1,73 @@
package com.ankurm.hibernatedemo.sorting;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
/**
* The entity's property is {@code title}; the column it's stored under is {@code song_title},
* on purpose -- this is what proves {@code @OrderBy("title asc")} on {@link Playlist} names the
* property, not the column.
*
* <p>{@code rating} is nullable: some songs are unrated, which is what chapter 22's null-
* precedence section needs a real column for.
*
* <p>Docs: docs/22-sorting.md.
*/
@Entity
public class Song {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "song_title")
private String title;
private String artist;
/** Nullable on purpose -- an unrated song is NULL, not 0. */
private Integer rating;
@ManyToOne
@JoinColumn(name = "playlist_id")
private Playlist playlist;
protected Song() {
// for Hibernate
}
public Song(String title, String artist, Integer rating) {
this.title = title;
this.artist = artist;
this.rating = rating;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public String getArtist() {
return artist;
}
public Integer getRating() {
return rating;
}
public Playlist getPlaylist() {
return playlist;
}
public void setPlaylist(Playlist playlist) {
this.playlist = playlist;
}
}
@@ -0,0 +1,33 @@
package com.ankurm.hibernatedemo.sorting;
import java.util.Set;
/**
* The whitelist chapter 22's "dynamic sorting" section builds against. Concatenating an
* unvalidated caller-supplied string directly into an HQL {@code order by} clause hands that
* caller a way to inject arbitrary HQL (a path onto an unrelated entity, a nested {@code case}
* expression, or simply a string that breaks the query outright as a denial-of-service). The
* fix is not to sanitize the string -- it's to never let it reach the query at all except
* through a fixed, known-safe set of property names.
*
* <p>Docs: docs/22-sorting.md.
*/
public final class SongSortField {
private static final Set<String> ALLOWED = Set.of("title", "artist", "rating");
private SongSortField() {
}
/**
* @return the exact HQL property name to sort by
* @throws IllegalArgumentException if {@code requested} is not one of the known-safe fields
*/
public static String toHqlPropertyOrThrow(String requested) {
if (!ALLOWED.contains(requested)) {
throw new IllegalArgumentException(
"'" + requested + "' is not a sortable field; allowed values are " + ALLOWED);
}
return requested;
}
}
@@ -0,0 +1,76 @@
package com.ankurm.hibernatedemo.testdb;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
/**
* Backs docs/09-testing-in-memory-databases.md (post 4868 rewrite). One entity, run through the SAME mapping
* against H2 2.4.240, HSQLDB 2.7.3 and Apache Derby 10.16.1.1 via {@link TestDbBootstrap} --
* the DDL differences, dialect selection, and cross-database failure case are all read off
* this exact class.
*
* <ul>
* <li>{@code id} -- {@code GenerationType.AUTO}, deliberately not IDENTITY or SEQUENCE, to
* see what each dialect resolves AUTO to.</li>
* <li>{@code sku} -- {@code varchar(5)}, short enough that an over-length value is a real
* constraint violation on at least one of the three databases.</li>
* <li>{@code order} -- a column named after a SQL reserved word on purpose (see
* {@code @Column(name = "\"order\"")}), the classic "works on some databases, breaks on
* others" trap.</li>
* <li>{@code active} -- a plain {@code boolean}, to see how each dialect maps and prints it.</li>
* <li>{@code description} -- {@code @Lob}, to see clob/text mapping differences.</li>
* </ul>
*/
@Entity
public class TestDbWidget {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(length = 5)
private String sku;
@Column(name = "\"order\"")
private Integer order;
private boolean active;
@Lob
private String description;
protected TestDbWidget() {
// JPA
}
public TestDbWidget(String sku, Integer order, boolean active, String description) {
this.sku = sku;
this.order = order;
this.active = active;
this.description = description;
}
public Long getId() {
return id;
}
public String getSku() {
return sku;
}
public Integer getOrder() {
return order;
}
public boolean isActive() {
return active;
}
public String getDescription() {
return description;
}
}
@@ -0,0 +1,18 @@
package com.ankurm.hibernatedemo.validation;
import jakarta.enterprise.context.ApplicationScoped;
/**
* A CDI-managed policy bean, deliberately not a constant. {@link PositiveInventoryValidator}
* depends on it via {@code @Inject} rather than hardcoding a threshold, so injection either
* genuinely happens or the validator has nothing usable to call.
*
* <p>Docs: docs/20-hibernate-validator-cdi.md
*/
@ApplicationScoped
public class InventoryPolicy {
public int minimumThreshold() {
return 5;
}
}
@@ -0,0 +1,27 @@
package com.ankurm.hibernatedemo.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* A stock quantity must be at or above {@link InventoryPolicy#minimumThreshold()} -- a policy
* value, not a hardcoded number, which is the whole point: {@link PositiveInventoryValidator}
* needs that bean injected to do its job at all.
*
* <p>Docs: docs/20-hibernate-validator-cdi.md
*/
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PositiveInventoryValidator.class)
public @interface PositiveInventory {
String message() default "quantity is below the minimum inventory threshold";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@@ -0,0 +1,28 @@
package com.ankurm.hibernatedemo.validation;
import jakarta.inject.Inject;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
/**
* The article's central claim, measured directly: does {@code @Inject} actually work inside a
* {@code ConstraintValidator}? The field below is never null-checked defensively on purpose --
* the {@code NullPointerException} it throws when {@code policy} was never injected is itself
* the evidence chapter 20 measures.
*
* <p>Docs: docs/20-hibernate-validator-cdi.md
*/
public class PositiveInventoryValidator implements ConstraintValidator<PositiveInventory, Integer> {
@Inject
private InventoryPolicy policy;
@Override
public boolean isValid(Integer quantity, ConstraintValidatorContext context) {
if (quantity == null) {
return true; // let @NotNull handle nullness; this constraint is about the value
}
// No null-guard on `policy` here, deliberately -- see the class Javadoc.
return quantity >= policy.minimumThreshold();
}
}
@@ -0,0 +1,21 @@
package com.ankurm.hibernatedemo.validation;
/**
* A plain POJO, deliberately not a JPA entity -- this chapter is about Bean Validation and CDI,
* not persistence.
*
* <p>Docs: docs/20-hibernate-validator-cdi.md
*/
public class StockLevel {
@PositiveInventory
private final Integer quantity;
public StockLevel(Integer quantity) {
this.quantity = quantity;
}
public Integer getQuantity() {
return quantity;
}
}