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:
@@ -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} — docs/01-get-vs-load.md, {@link com.ankurm.hibernatedemo.scenario.GetVsLoadRunner}</li>
|
||||
* <li>{@code merge-vs-refresh} — docs/02-merge-vs-refresh.md, {@link com.ankurm.hibernatedemo.scenario.MergeVsRefreshRunner}</li>
|
||||
* <li>{@code insert-identity} / {@code insert-sequence} — 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 — there is no web server to keep alive,
|
||||
* so {@code scripts/run.sh <profile>} 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 — {@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 — 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)} — 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;
|
||||
}
|
||||
}
|
||||
+42
@@ -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;
|
||||
}
|
||||
}
|
||||
+106
@@ -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);
|
||||
}
|
||||
}
|
||||
+56
@@ -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;
|
||||
}
|
||||
}
|
||||
+39
@@ -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
|
||||
* <natural-id> 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
@@ -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 — 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
|
||||
* "USER_EDIT" vs "ADMIN_EDIT" scenario in docs/02-merge-vs-refresh.md. The
|
||||
* {@code notes} collection is LAZY and cascades MERGE only — 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
@@ -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} — 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} — 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} — 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 — 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) {
|
||||
}
|
||||
+38
@@ -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;
|
||||
}
|
||||
}
|
||||
+54
@@ -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;
|
||||
}
|
||||
}
|
||||
+44
@@ -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;
|
||||
}
|
||||
}
|
||||
+42
@@ -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;
|
||||
}
|
||||
}
|
||||
+62
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+49
@@ -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
|
||||
}
|
||||
}
|
||||
+40
@@ -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
|
||||
}
|
||||
}
|
||||
+61
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -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);
|
||||
}
|
||||
}
|
||||
+41
@@ -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.
|
||||
}
|
||||
+40
@@ -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;
|
||||
}
|
||||
}
|
||||
+76
@@ -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;
|
||||
}
|
||||
}
|
||||
+11
@@ -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) {
|
||||
}
|
||||
Executable
+42
@@ -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) — 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 — 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 — 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user