From 321d6e33a908d20494454761fb78dce4bd709b15 Mon Sep 17 00:00:00 2001 From: Shubham Date: Sat, 25 Apr 2026 18:52:26 +0530 Subject: [PATCH 1/2] feat(hibernate-cache-tour): add Postgres v3 codec drift reproducer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spring Boot 3 + Hibernate 6 + Postgres 16 sample that exercises two specific drift signatures in keploy's Postgres v3 logical codec: * Issue #1 — pgjdbc prepareThreshold flip. Each WHERE-id JpaRepository finder is hit with the same id-space repeating every 4 calls so the 5th execution on a given handle straddles pgjdbc's prepareThreshold (default 5) and flips the bind format text -> binary mid-recording. * Issue #3 — Hibernate StatementCache classification drift. Query + second-level cache are enabled (EHCache 3.x via JCache); a request interceptor evicts the L2 cache on every keploy.io/test-name change so per-test boundaries observably re-run the pgjdbc bind path. Schema: customer(id, name, email) + customer_tag(id, customer_id, tag, priority). init.sql seeds 4 customers (ids 1..4) and 8 tags so the CI exerciser can hit deterministic ids without first POSTing fixtures. Endpoints: POST /customer, GET /customer/{id}, GET /customer/{id}/tags, GET /tags?priority=N, POST /tag. The CI harness driving this sample lives in keploy/integrations at .ci/scripts/java/hibernate-cache-tour-postgres/. Signed-off-by: Shubham --- hibernate-cache-tour-postgres/.gitignore | 5 + hibernate-cache-tour-postgres/README.md | 52 ++++++++++ hibernate-cache-tour-postgres/pom.xml | 96 +++++++++++++++++++ .../HibernateCacheTourApplication.java | 18 ++++ .../config/TestBoundaryInterceptor.java | 62 ++++++++++++ .../controller/CustomerController.java | 91 ++++++++++++++++++ .../hibernatecachetour/model/Customer.java | 46 +++++++++ .../hibernatecachetour/model/CustomerTag.java | 52 ++++++++++ .../repository/CustomerRepository.java | 23 +++++ .../repository/CustomerTagRepository.java | 22 +++++ .../src/main/resources/application.properties | 34 +++++++ .../src/main/resources/init.sql | 45 +++++++++ 12 files changed, 546 insertions(+) create mode 100644 hibernate-cache-tour-postgres/.gitignore create mode 100644 hibernate-cache-tour-postgres/README.md create mode 100644 hibernate-cache-tour-postgres/pom.xml create mode 100644 hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/HibernateCacheTourApplication.java create mode 100644 hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/config/TestBoundaryInterceptor.java create mode 100644 hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/controller/CustomerController.java create mode 100644 hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/model/Customer.java create mode 100644 hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/model/CustomerTag.java create mode 100644 hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/repository/CustomerRepository.java create mode 100644 hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/repository/CustomerTagRepository.java create mode 100644 hibernate-cache-tour-postgres/src/main/resources/application.properties create mode 100644 hibernate-cache-tour-postgres/src/main/resources/init.sql diff --git a/hibernate-cache-tour-postgres/.gitignore b/hibernate-cache-tour-postgres/.gitignore new file mode 100644 index 00000000..a2b7bde0 --- /dev/null +++ b/hibernate-cache-tour-postgres/.gitignore @@ -0,0 +1,5 @@ +target/ +.idea/ +*.iml +.vscode/ +.DS_Store diff --git a/hibernate-cache-tour-postgres/README.md b/hibernate-cache-tour-postgres/README.md new file mode 100644 index 00000000..f4ade0c6 --- /dev/null +++ b/hibernate-cache-tour-postgres/README.md @@ -0,0 +1,52 @@ +# hibernate-cache-tour-postgres + +A Spring Boot 3 + Hibernate 6 + Postgres 16 reproducer that exercises two +specific drift signatures in keploy's Postgres v3 logical codec: + +1. **Issue #1 — pgjdbc `prepareThreshold` flip.** + pgjdbc defaults to `prepareThreshold=5`: the same SQL handle is sent in + text format for the first 4 calls and switches to binary on the 5th + execution. Each `WHERE id = ?` JpaRepository finder hit `>= 8` times will + straddle that boundary — the recorded mocks for one SQL hash carry mixed + format codes; replay only matches when the v3 codec reconciles them. + +2. **Issue #3 — Hibernate StatementCache classification drift.** + With `hibernate.cache.use_query_cache` and `use_second_level_cache` + enabled (EHCache 3.x via JCache), per-test cache eviction triggered by + the `keploy.io/test-name` header changes the cache hit/miss pattern + between record and replay, which classifies the same SQL differently + in keploy's StatementCache. + +## Endpoints + +| Method | Path | Notes | +|--------|----------------------------|------------------------------------| +| POST | /customer | Create + return id | +| GET | /customer/{id} | `WHERE id = ?` (single int4 bind) | +| GET | /customer/{id}/tags | `WHERE customer_id = ?` | +| GET | /tags?priority={p} | `WHERE priority = ?` | +| POST | /tag | Create tag + return id | + +## Schema + +`customer(id SERIAL, name, email)` and `customer_tag(id SERIAL, +customer_id REFERENCES customer, tag VARCHAR(64), priority INT)`. + +`init.sql` seeds 4 customers (ids 1..4) and 8 tags. The CI exerciser hits +each id-keyed endpoint 8 times against `1 + (i-1) % 4`, so the same id +recurs every 4 calls — the 5th call on a given handle is the +prepareThreshold flip. + +## Build / run + +``` +mvn -DskipTests package +SPRING_DATASOURCE_URL=jdbc:postgresql://127.0.0.1:5432/hibcache \ +SPRING_DATASOURCE_USERNAME=hibcache \ +SPRING_DATASOURCE_PASSWORD=hibcache \ +java -jar target/hibernate-cache-tour.jar +``` + +The CI harness lives in `keploy/integrations` at +`.ci/scripts/java/hibernate-cache-tour-postgres/` and drives the full +record → replay regression. diff --git a/hibernate-cache-tour-postgres/pom.xml b/hibernate-cache-tour-postgres/pom.xml new file mode 100644 index 00000000..845e5dc8 --- /dev/null +++ b/hibernate-cache-tour-postgres/pom.xml @@ -0,0 +1,96 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.keploy + hibernate-cache-tour-postgres + 0.0.1-SNAPSHOT + hibernate-cache-tour-postgres + Repro for pgjdbc prepareThreshold + Hibernate StatementCache drift + + + 17 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.hibernate.orm + hibernate-jcache + + + org.ehcache + ehcache + jakarta + + + + org.postgresql + postgresql + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + hibernate-cache-tour + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/HibernateCacheTourApplication.java b/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/HibernateCacheTourApplication.java new file mode 100644 index 00000000..bb5a2cae --- /dev/null +++ b/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/HibernateCacheTourApplication.java @@ -0,0 +1,18 @@ +package com.keploy.hibernatecachetour; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; + +/** + * Entry point. EnableCaching switches on Spring's cache abstraction so the + * Hibernate L2/query cache wired in application.properties is actually used + * end-to-end. + */ +@SpringBootApplication +@EnableCaching +public class HibernateCacheTourApplication { + public static void main(String[] args) { + SpringApplication.run(HibernateCacheTourApplication.class, args); + } +} diff --git a/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/config/TestBoundaryInterceptor.java b/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/config/TestBoundaryInterceptor.java new file mode 100644 index 00000000..1349172f --- /dev/null +++ b/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/config/TestBoundaryInterceptor.java @@ -0,0 +1,62 @@ +package com.keploy.hibernatecachetour.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.persistence.EntityManagerFactory; +import org.hibernate.Cache; +import org.hibernate.SessionFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Reads the keploy.io/test-name header (and its alias X-Keploy-Test-Name) + * and evicts the entire Hibernate L2 cache when the test name changes. + * This is what creates the per-test classification boundary that drives + * issue #3 — StatementCache drift between record and replay caused by + * differing cache hit/miss patterns. + * + * The eviction is intentionally aggressive (cache.evictAllRegions): it + * forces Hibernate to issue WHERE id = ? and WHERE customer_id = ? + * against Postgres on the first request after each boundary, so the + * recorded mocks contain the full pgjdbc bind dance for that path. + * Without eviction every request after the first would hit the L2 + * cache and never reach pgjdbc, defeating the prepareThreshold flip + * we want to surface for issue #1. + */ +@Component +public class TestBoundaryInterceptor implements HandlerInterceptor, WebMvcConfigurer { + + private final EntityManagerFactory emf; + private volatile String currentTestName = ""; + + public TestBoundaryInterceptor(EntityManagerFactory emf) { + this.emf = emf; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String name = request.getHeader("keploy.io/test-name"); + if (name == null || name.isEmpty()) { + name = request.getHeader("X-Keploy-Test-Name"); + } + if (name == null) { + return true; + } + if (!name.equals(currentTestName)) { + currentTestName = name; + SessionFactory sf = emf.unwrap(SessionFactory.class); + Cache cache = sf.getCache(); + if (cache != null) { + cache.evictAllRegions(); + } + } + return true; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(this); + } +} diff --git a/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/controller/CustomerController.java b/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/controller/CustomerController.java new file mode 100644 index 00000000..74c35233 --- /dev/null +++ b/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/controller/CustomerController.java @@ -0,0 +1,91 @@ +package com.keploy.hibernatecachetour.controller; + +import com.keploy.hibernatecachetour.model.Customer; +import com.keploy.hibernatecachetour.model.CustomerTag; +import com.keploy.hibernatecachetour.repository.CustomerRepository; +import com.keploy.hibernatecachetour.repository.CustomerTagRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * REST surface that exercises the prepared-statement bind paths the v3 + * codec must reconcile. + * + * Endpoints: + * POST /customer create + return id + * GET /customer/{id} single-bind WHERE id = ? (the canonical + * format-flip trigger) + * GET /customer/{id}/tags WHERE customer_id = ? + * GET /tags?priority=N WHERE priority = ? + * POST /tag insert + return id + * + * Each GET is intentionally a JpaRepository finder that compiles to a + * single-int-bind prepared statement. The exerciser hits each one >=8 + * times (CI script) so pgjdbc's prepareThreshold (default 5) trips and + * the bind-format byte flips text -> binary mid-recording. + * + * The X-Keploy-Test-Name header (forwarded by the keploy recorder when + * configured with keploy.io/test-name) is read by an interceptor that + * evicts the L2 cache between logical tests — that's how issue #3 + * (StatementCache classification drift) gets exercised deterministically. + */ +@RestController +public class CustomerController { + + private final CustomerRepository customers; + private final CustomerTagRepository tags; + + @PersistenceContext + private EntityManager em; + + public CustomerController(CustomerRepository customers, CustomerTagRepository tags) { + this.customers = customers; + this.tags = tags; + } + + @PostMapping("/customer") + @Transactional + public ResponseEntity> create(@RequestBody Map body) { + Customer c = new Customer(body.getOrDefault("name", "anon"), + body.getOrDefault("email", "anon@example.com")); + Customer saved = customers.save(c); + return ResponseEntity.ok(Map.of("id", saved.getId(), "name", saved.getName())); + } + + @GetMapping("/customer/{id}") + @Transactional(readOnly = true) + public ResponseEntity get(@PathVariable Integer id) { + return customers.findById(id) + .>map(ResponseEntity::ok) + .orElseGet(() -> ResponseEntity.status(404).body(Map.of("error", "not found"))); + } + + @GetMapping("/customer/{id}/tags") + @Transactional(readOnly = true) + public ResponseEntity> tagsFor(@PathVariable Integer id) { + return ResponseEntity.ok(tags.findByCustomerId(id)); + } + + @GetMapping("/tags") + @Transactional(readOnly = true) + public ResponseEntity> tagsByPriority(@RequestParam("priority") Integer priority) { + return ResponseEntity.ok(tags.findByPriority(priority)); + } + + @PostMapping("/tag") + @Transactional + public ResponseEntity> createTag(@RequestBody Map body) { + Integer customerId = (Integer) body.get("customerId"); + String tag = (String) body.getOrDefault("tag", "untagged"); + Integer priority = (Integer) body.getOrDefault("priority", 1); + CustomerTag t = new CustomerTag(customerId, tag, priority); + CustomerTag saved = tags.save(t); + return ResponseEntity.ok(Map.of("id", saved.getId(), "tag", saved.getTag())); + } +} diff --git a/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/model/Customer.java b/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/model/Customer.java new file mode 100644 index 00000000..2e53ddc2 --- /dev/null +++ b/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/model/Customer.java @@ -0,0 +1,46 @@ +package com.keploy.hibernatecachetour.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +/** + * Customer is annotated with @Cache so a fetch by id population the L2 + * second-level cache. On a per-test boundary (clear/evict on + * keploy.io/test-name change) the L2 entry classification differs between + * record and replay — the cache miss path drives an extra `WHERE id = ?` + * query that doesn't exist on a cache hit. That divergence is the + * Hibernate StatementCache classification drift in issue #3. + */ +@Entity +@Table(name = "customer") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +public class Customer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + private String name; + private String email; + + public Customer() {} + + public Customer(String name, String email) { + this.name = name; + this.email = email; + } + + public Integer getId() { return id; } + public void setId(Integer id) { this.id = id; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } +} diff --git a/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/model/CustomerTag.java b/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/model/CustomerTag.java new file mode 100644 index 00000000..5261a3d8 --- /dev/null +++ b/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/model/CustomerTag.java @@ -0,0 +1,52 @@ +package com.keploy.hibernatecachetour.model; + +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 org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +/** + * Tag entity. customer_id and priority are both indexed columns the + * exerciser hits on every iteration, which keeps the prepareThreshold + * counter climbing for those two SQL handles independently of the + * /customer/{id} endpoint. + */ +@Entity +@Table(name = "customer_tag") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +public class CustomerTag { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(name = "customer_id") + private Integer customerId; + + private String tag; + private Integer priority; + + public CustomerTag() {} + + public CustomerTag(Integer customerId, String tag, Integer priority) { + this.customerId = customerId; + this.tag = tag; + this.priority = priority; + } + + public Integer getId() { return id; } + public void setId(Integer id) { this.id = id; } + + public Integer getCustomerId() { return customerId; } + public void setCustomerId(Integer customerId) { this.customerId = customerId; } + + public String getTag() { return tag; } + public void setTag(String tag) { this.tag = tag; } + + public Integer getPriority() { return priority; } + public void setPriority(Integer priority) { this.priority = priority; } +} diff --git a/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/repository/CustomerRepository.java b/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/repository/CustomerRepository.java new file mode 100644 index 00000000..81bb01a8 --- /dev/null +++ b/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/repository/CustomerRepository.java @@ -0,0 +1,23 @@ +package com.keploy.hibernatecachetour.repository; + +import com.keploy.hibernatecachetour.model.Customer; +import jakarta.persistence.QueryHint; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.QueryHints; + +/** + * JpaRepository.findById issues a prepared statement with a single int4 + * bind in the WHERE clause — this is exactly the pattern that flips + * pgjdbc format text->binary at prepareThreshold (default 5). + * + * QueryHints HIBERNATE_CACHEABLE keeps the query result in the query + * cache so per-test eviction matters. Without this hint, only entity + * loads (findById) populate the L2 entity cache; with it, the WHERE + * clause query result itself is cached too — enlarging the surface + * for issue #3 to manifest. + */ +public interface CustomerRepository extends JpaRepository { + @Override + @QueryHints({@QueryHint(name = "org.hibernate.cacheable", value = "true")}) + java.util.Optional findById(Integer id); +} diff --git a/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/repository/CustomerTagRepository.java b/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/repository/CustomerTagRepository.java new file mode 100644 index 00000000..1051a992 --- /dev/null +++ b/hibernate-cache-tour-postgres/src/main/java/com/keploy/hibernatecachetour/repository/CustomerTagRepository.java @@ -0,0 +1,22 @@ +package com.keploy.hibernatecachetour.repository; + +import com.keploy.hibernatecachetour.model.CustomerTag; +import jakarta.persistence.QueryHint; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.QueryHints; + +import java.util.List; + +/** + * Two derived finders — each compiled to a separate prepared statement + * by Hibernate, each with a single int4 bind. Calling them >=5 times + * with the same id-space drives BOTH SQL handles past prepareThreshold. + */ +public interface CustomerTagRepository extends JpaRepository { + + @QueryHints({@QueryHint(name = "org.hibernate.cacheable", value = "true")}) + List findByCustomerId(Integer customerId); + + @QueryHints({@QueryHint(name = "org.hibernate.cacheable", value = "true")}) + List findByPriority(Integer priority); +} diff --git a/hibernate-cache-tour-postgres/src/main/resources/application.properties b/hibernate-cache-tour-postgres/src/main/resources/application.properties new file mode 100644 index 00000000..1361b170 --- /dev/null +++ b/hibernate-cache-tour-postgres/src/main/resources/application.properties @@ -0,0 +1,34 @@ +# Hibernate cache tour — Spring Boot configuration. +# +# Datasource is overridable via SPRING_DATASOURCE_* env vars; defaults match +# the docker-compose.yml in the CI harness. The pgjdbc URL deliberately omits +# `prepareThreshold=` so the driver default of 5 applies — that's the value +# the exerciser counts on for the format-flip trigger. +spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://127.0.0.1:5432/hibcache} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME:hibcache} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:hibcache} +spring.datasource.driver-class-name=org.postgresql.Driver + +# We let Hibernate validate the schema only — the docker container init.sql +# creates the tables and the seed data the exerciser depends on (ids 1..4). +spring.jpa.hibernate.ddl-auto=validate +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.show-sql=false + +# --- Hibernate L2 + query cache ------------------------------------------- +# These four flags together flip on the cache surface that issue #3 needs. +# JCacheRegionFactory delegates to javax.cache.spi.CachingProvider, which +# the EHCache 3.x Jakarta classifier registers automatically when on the +# classpath. No explicit ehcache.xml is required for the default config. +spring.jpa.properties.hibernate.cache.use_second_level_cache=true +spring.jpa.properties.hibernate.cache.use_query_cache=true +spring.jpa.properties.hibernate.cache.region.factory_class=jcache +spring.jpa.properties.hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider + +# Default Spring Boot cache type (kept consistent with the JPA L2 provider). +spring.cache.type=jcache + +# Server / actuator +server.port=${SERVER_PORT:8080} +management.endpoint.health.probes.enabled=true +management.endpoints.web.exposure.include=health,info diff --git a/hibernate-cache-tour-postgres/src/main/resources/init.sql b/hibernate-cache-tour-postgres/src/main/resources/init.sql new file mode 100644 index 00000000..cfeeef3c --- /dev/null +++ b/hibernate-cache-tour-postgres/src/main/resources/init.sql @@ -0,0 +1,45 @@ +-- init.sql — schema + seed data for the hibernate-cache-tour repro. +-- +-- Postgres mounts files in /docker-entrypoint-initdb.d/ at first start. +-- Both the schema and the seed are committed here so the docker-compose +-- run-dir contains exactly one file the DB needs to bootstrap. +-- +-- The seed creates customers with ids 1..4 and 8 tags spread across them. +-- The exerciser hits each WHERE-id endpoint with `id = 1 + (i-1) % 4` +-- so the same id-space repeats every 4 calls — that's how a single SQL +-- handle gets its 5th execution on iteration 5 and trips the pgjdbc +-- prepareThreshold (default 5) into binary format. + +CREATE TABLE IF NOT EXISTS customer ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + email TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS customer_tag ( + id SERIAL PRIMARY KEY, + customer_id INT REFERENCES customer(id), + tag VARCHAR(64) NOT NULL, + priority INT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_customer_tag_customer_id ON customer_tag(customer_id); +CREATE INDEX IF NOT EXISTS idx_customer_tag_priority ON customer_tag(priority); + +INSERT INTO customer (name, email) VALUES + ('alice', 'alice@example.com'), + ('bob', 'bob@example.com'), + ('carol', 'carol@example.com'), + ('dave', 'dave@example.com') +ON CONFLICT DO NOTHING; + +INSERT INTO customer_tag (customer_id, tag, priority) VALUES + (1, 'gold', 10), + (1, 'beta', 20), + (2, 'silver', 10), + (2, 'beta', 30), + (3, 'bronze', 20), + (3, 'preview', 30), + (4, 'gold', 10), + (4, 'preview', 20) +ON CONFLICT DO NOTHING; From b8f63cb171639e88ab0290130c6ea7d83b38d236 Mon Sep 17 00:00:00 2001 From: Shubham Date: Sat, 25 Apr 2026 19:25:16 +0530 Subject: [PATCH 2/2] fix(hibernate-cache-tour): add spring-boot-starter-cache for L2 wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Hibernate L2 / query cache is enabled in application.properties via hibernate-jcache + EHCache 3.x, but the Spring context still needs a CacheManager bean to satisfy the JPA layer's cache-related autowires. Without spring-boot-starter-cache, Spring never auto-configures one and entityManagerFactory creation fails on boot — Tomcat binds 8080 but the JPA layer is dead, so every request crashes after the dispatcher. Dropping the starter in next to hibernate-jcache fixes it; the rest of the cache config (region factory, EHCache config) is unchanged. Signed-off-by: Shubham --- hibernate-cache-tour-postgres/pom.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hibernate-cache-tour-postgres/pom.xml b/hibernate-cache-tour-postgres/pom.xml index 845e5dc8..2317f12a 100644 --- a/hibernate-cache-tour-postgres/pom.xml +++ b/hibernate-cache-tour-postgres/pom.xml @@ -60,7 +60,13 @@ + ehcache-2 provider was dropped). spring-boot-starter-cache + is required so Spring auto-configures a CacheManager bean + that the JPA EntityManagerFactory can wire on boot. --> + + org.springframework.boot + spring-boot-starter-cache + org.hibernate.orm hibernate-jcache