Tag: Performance
All the articles with the tag "Performance".
-
[JPA + Spring Mastery 09] When to use what — a JPA / JPQL / Native SQL decision tree, cuts drawn by four measurements
Across eight prior posts, this series unpacked PersistenceContext / Dirty Checking / Optimistic Lock / N+1 / IDENTITY / self-invocation / Saga·Outbox with measurements. This finale lays that evidence on a single decision tree — starting with the first fork (domain unit vs data unit), then five places where JPA Dirty Checking shines and five where it bleeds, JPQL's middle ground, and the seat for Native / JdbcTemplate / MyBatis. Four measurement cuts (Dirty Checking 132× / saveAll IDENTITY ~1000× / OFFSET 1M 570× / bulk JPQL vs Dirty Checking 84×) decide the boundaries, anchored on Eric Evans' Aggregate (DDD 2003), Fowler's Identity Map / Unit of Work (PoEAA 2002), Greg Young's CQRS Documents (2010), and Vlad Mihalcea's **High-Performance Java Persistence** (2016). The entry point you can hand over in a single URL when an interviewer asks 'how have you used JPA?'
-
Why saveAll() Becomes 10K INSERTs — IDENTITY and Hibernate's Structural Batch Disablement
hibernate.jdbc.batch_size=50 is set, yet saveAll() of 10,000 rows fires 10,000 INSERTs. GenerationType.IDENTITY needs LAST_INSERT_ID() per INSERT, which Statement.RETURN_GENERATED_KEYS cannot deliver in batch — Hibernate disables batching structurally. Application-managed IDs (TABLE strategy simulation) restore batching at ~200 SQL. Raw JDBC batchUpdate with rewriteBatchedStatements=true rewrites them as multi-value INSERT (~10 SQL) — the fastest path. The DZone "IDENTITY → SEQUENCE 100x" post is PostgreSQL-specific. MySQL has no native SEQUENCE (it falls back to TABLE), so the real options on MySQL are UUID, TableGenerator pooled-lo, Snowflake/TSID, or raw JDBC batch.
-
JPA N+1 and the Four JOIN FETCH Traps — MultipleBagFetchException, Pagination OOM, OneToOne LAZY
In a 4-depth domain (owner→merchant→rule→history) findAll + child traversal yields 121 SQL. JOIN FETCH collapses it to 1 (12× faster). Fetching two collections at once raises MultipleBagFetchException — Hibernate refuses the cartesian of two Bags. JOIN FETCH + setMaxResults emits HHH000104 and applies pagination *in memory* — silent OOM at scale. Non-owning @OneToOne LAZY is *always fetched* because the proxy cannot tell whether the value is null. **`default_batch_fetch_size: 10` reduces N+1 from 121 → 13 prep (9.3× drop) — but does NOT fix @OneToOne non-owning LAZY (still 1201 prep)**, because batch fetch only batches collection LAZY triggers, not row-by-row ToOne SELECTs. The fetch traps come from Bag/List/Set semantics, proxy limitations, and Hibernate's cartesian handling — JOIN FETCH alone is half the answer.
-
The Real Cost of JPA Dirty Checking — readOnly, @DynamicUpdate, and Query Plan Cache Leaks
Hibernate's dirty checking copies an entity snapshot at load time and compares it against the current state at flush. With 10,000 rows, readOnly=true skips the snapshot copy (memory savings). @DynamicUpdate emits SQL with only the changed columns — but generates a fresh SQL string per update pattern, increasing Query Plan Cache usage (a permanent heap leak if hibernate.query.plan_cache_max_size is unset). @Modifying bulk JPQL is fastest but leaves the persistence context inconsistent — clearAutomatically=true is the standard. The clear() pattern (flush + clear every 50 inserts) keeps memory bounded for large insert batches. The trap in JPA is never one feature; it is the interaction of flush, cache, and snapshot lifecycle.