Introduction Primary key choice affects performance, scalability, and application design. This article covers every strategy JPA supports — from the simple AUTO_INCREMENT to UUID and composite keys — with the trade-offs of each. IDENTITY Strategy (MySQL AUTO_INCREMENT) GenerationType.IDENTITY delegates key generation to the database column’s auto-increment feature. This is the standard for MySQL. @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; Generated DDL (when ddl-auto=create): id BIGINT NOT NULL AUTO_INCREMENT How IDENTITY works with Hibernate IDENTITY has one important behaviour: Hibernate cannot batch INSERT statements when using IDENTITY.
Continue reading »Hibernate
31 posts in this section
Projections and DTOs: Fetching Only What You Need
The Over-Fetching Problem By default, findAll() loads every column for every entity. A Product entity with 20 fields loads all 20 columns — even when a dropdown menu only needs id and name. This wastes bandwidth, memory, and query time. Projections let you define what shape the result should take, and Spring Data JPA generates a query that fetches only those columns. Interface Projections The simplest projection: declare an interface with getter methods matching entity field names.
Continue reading »Repository Interfaces: CrudRepository, JpaRepository, and How They Work
Introduction Spring Data JPA’s repository abstraction eliminates the DAO boilerplate that every Java application used to require. You declare an interface, extend one of the repository base interfaces, and Spring generates a complete implementation at startup — find, save, delete, count, and more — with zero code written. The Repository Hierarchy Repository<T, ID> ← Marker interface — no methods │ └── CrudRepository<T, ID> ← Basic CRUD: save, find, delete, count │ └── PagingAndSortingRepository<T, ID> ← + findAll(Pageable), findAll(Sort) │ └── JpaRepository<T, ID> ← + flush, saveAndFlush, deleteInBatch Each interface adds more operations.
Continue reading »Second-Level Cache and Query Cache with Hibernate
Cache Layers in Hibernate Hibernate has two cache levels: Level Scope Lifetime Shared? First-level cache One Session (one transaction) Transaction No — per session Second-level cache SessionFactory Application lifetime Yes — across sessions The first-level cache (Article 24) prevents repeated reads within one transaction. The second-level cache prevents repeated reads across transactions — once an entity is loaded, it stays in the shared cache until evicted. When to Use the Second-Level Cache Good candidates:
Continue reading »Setting Up Spring Boot with Spring Data JPA and MySQL
Introduction This article builds the project foundation used throughout the entire series. By the end, you will have a running Spring Boot 3.3 application connected to MySQL 8.x with Hibernate 6, a proper connection pool, schema management via Flyway, and SQL logging configured so you can see exactly what Hibernate sends to the database. Project Setup Maven pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.
Continue reading »Specifications and the Criteria API: Dynamic Queries
Why Specifications Derived query methods and @Query annotations work for fixed queries. But what happens when the user can filter by any combination of 10 fields — some optional, some not? // Wrong approach — combinatorial explosion List<Product> findByName(String name); List<Product> findByNameAndPrice(String name, BigDecimal price); List<Product> findByNameAndPriceAndCategory(...); // ... you'd need 2^10 = 1024 methods The Specification pattern solves this. Each filter condition is a reusable Specification<T> object. You compose them with and(), or(), and not() at runtime, and Spring Data JPA generates the correct query.
Continue reading »Testing Spring Data JPA: @DataJpaTest, Testcontainers, and Best Practices
Why Test JPA Code? Unit tests with mocked repositories verify your service logic but nothing about the database. They won’t catch: Wrong column mapping Constraint violations N+1 queries introduced by a new lazy association Transactions that silently don’t roll back Queries that fail on the real database but pass on H2 Testing against a real database — or at minimum a database-compatible in-memory store — is necessary. Spring Boot’s @DataJpaTest and Testcontainers make this practical.
Continue reading »The N+1 Problem: Detection, Root Cause, and All Solutions
What Is the N+1 Problem? The N+1 problem occurs when loading a list of N entities triggers N additional queries to load their associations — one query per entity. Example: load 50 orders, then access each order’s customer: SELECT * FROM orders; -- 1 query SELECT * FROM customers WHERE id = 1; -- query for order 1's customer SELECT * FROM customers WHERE id = 2; -- query for order 2's customer SELECT * FROM customers WHERE id = 3; -- query for order 3's customer .
Continue reading »The Persistence Context: How JPA Tracks Your Entities
Introduction The persistence context is the single most important concept in JPA. Everything else — lazy loading, dirty checking, transaction scope, detached entity exceptions — only makes sense once you understand what the persistence context is and how it works. Many JPA bugs come from developers not understanding this concept. Understand it well and the rest of JPA becomes predictable. What Is the Persistence Context? The persistence context is an in-memory map of entities managed by the current EntityManager.
Continue reading »Transaction Boundaries and Common Pitfalls
Transaction Boundaries A transaction boundary is the point where a transaction starts and where it ends. In Spring, boundaries are defined by @Transactional on service methods. HTTP Request └── Controller (no transaction) └── @Transactional Service method ← transaction OPENS here ├── Repository call 1 ├── Repository call 2 └── method returns ← transaction COMMITS here Everything inside the @Transactional method runs in the same database transaction. The persistence context (first-level cache) lives for the duration of that transaction.
Continue reading »