A comprehensive Spring Boot application demonstrating the N+1 query problem in JPA/Hibernate and how to solve it using EntityGraph optimization. This project provides a practical, hands-on comparison between lazy loading and EntityGraph approaches with real-time query counting and detailed logging.
This project serves as a proof of concept to demonstrate:
- The N+1 query problem that occurs with lazy loading
- How EntityGraph can solve this performance issue
- Real-time SQL query counting and monitoring
- Best practices for handling many-to-many relationships in JPA
- Features
- Architecture
- Quick Start
- API Endpoints
- Database Schema
- Query Comparison
- Technologies Used
- Project Structure
- Configuration
- Testing
- Performance Analysis
- Contributing
- N+1 Query Problem Demonstration: Shows how lazy loading can cause performance issues
- EntityGraph Optimization: Demonstrates the solution using JPA EntityGraph
- Real-time Query Counting: Custom StatementInspector tracks every SQL query
- Automated Testing: Comprehensive test suite validating query performance
- REST API: Endpoints to test both approaches via HTTP requests
- Many-to-Many Relationships: Book β Author entities with proper JPA mapping
- DTO Pattern: Clean data transfer objects for API responses
- SQL Logging: Detailed SQL query logging with formatting
- H2 Console: In-memory database with web console for debugging
- YAML Configuration: Clean, hierarchical configuration structure
βββββββββββββββ βββββββββββββββββββ βββββββββββββββ
β Author β β book_authors β β Book β
βββββββββββββββ€ β (join table) β βββββββββββββββ€
β id (PK) βββββββββββ€ author_id (FK) β β id (PK) β
β name β β book_id (FK) ββββββββββΊβ title β
β email β βββββββββββββββββββ β isbn β
βββββββββββββββ βββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β REST Layer β
β BookController - Exposes /lazy and /entity-graph APIs β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Service Layer β
β BookService - Business logic and query strategies β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Repository Layer β
β BookRepository - Data access with EntityGraph support β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Persistence Layer β
β JPA/Hibernate with H2 In-Memory Database β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Java 21 or higher
- Maven 3.6+
- IDE (IntelliJ IDEA, Eclipse, or VS Code)
-
Clone the repository
git clone <repository-url> cd entity-graph
-
Run the application
mvn spring-boot:run
-
Access the application
- Application:
http://localhost:8080
- H2 Console:
http://localhost:8080/h2-console
- JDBC URL:
jdbc:h2:mem:testdb
- Username:
sa
- Password: (empty)
- JDBC URL:
- Application:
- Database Creation: H2 creates tables automatically
- Data Population: SQL script inserts 20 authors and 10 books
- Demo Execution: N+1 query demonstration runs automatically
- Query Logging: All SQL queries are logged with counts
Method | Endpoint | Description |
---|---|---|
GET |
/api/books/lazy |
Fetch books using lazy loading (N+1 problem) |
GET |
/api/books/entity-graph |
Fetch books using EntityGraph (optimized) |
Test Lazy Loading (N+1 Problem):
curl -X GET http://localhost:8080/api/books/lazy
Test EntityGraph (Optimized):
curl -X GET http://localhost:8080/api/books/entity-graph
[
{
"id": 1,
"title": "The Enchanted Forest",
"isbn": "ISBN-978-0-100000-X",
"authors": [
{
"id": 1,
"name": "J.K. Rowling",
"email": "jkrowling@literature.com"
}
]
}
]
The application automatically populates the database with:
- J.K. Rowling, Stephen King, George R.R. Martin
- Agatha Christie, J.R.R. Tolkien, Isaac Asimov
- And 14 more famous authors...
Book Title | Authors Count | Sample Authors |
---|---|---|
The Enchanted Forest | 1 | J.K. Rowling |
Mystery of the Lost Key | 2 | Stephen King, George R.R. Martin |
Dragons and Legends | 3 | Agatha Christie, J.R.R. Tolkien, Isaac Asimov |
The Secret Garden Chronicles | 4 | Arthur Conan Doyle, Jane Austen, Mark Twain, Charles Dickens |
-- Initial query to fetch all books
SELECT b.id, b.title, b.isbn FROM books b
-- Then N additional queries (one per book to fetch authors)
SELECT a.id, a.name, a.email FROM authors a
JOIN book_authors ba ON a.id = ba.author_id
WHERE ba.book_id = 1
SELECT a.id, a.name, a.email FROM authors a
JOIN book_authors ba ON a.id = ba.author_id
WHERE ba.book_id = 2
-- ... 8 more similar queries
Total Queries: 11 (1 + 10)
-- Single query with JOIN to fetch everything
SELECT b.id, ba.book_id, a.id, a.email, a.name, b.isbn, b.title
FROM books b
LEFT JOIN book_authors ba ON b.id = ba.book_id
LEFT JOIN authors a ON a.id = ba.author_id
Total Queries: 1
- Query Reduction: 90% fewer queries (11 β 1)
- Network Round-trips: Significant reduction
- Database Load: Dramatically improved
- Response Time: Faster for larger datasets
- Spring Boot 3.5.6: Main application framework
- Spring Data JPA: Data persistence and repository layer
- Hibernate 6.6+: ORM implementation
- H2 Database: In-memory database for development and testing
- Flyway/SQL Scripts: Database initialization
- Lombok: Reduces boilerplate code
- Maven: Build and dependency management
- SLF4J + Logback: Logging framework
- JUnit 5: Unit and integration testing
- Spring Boot Test: Testing Spring components
src/
βββ main/
β βββ java/com/example/entitygraph/
β β βββ controller/
β β β βββ BookController.java # REST endpoints
β β βββ dto/
β β β βββ AuthorDto.java # Author data transfer object
β β β βββ BookDto.java # Book data transfer object
β β βββ entity/
β β β βββ Author.java # Author JPA entity
β β β βββ Book.java # Book JPA entity with EntityGraph
β β βββ repository/
β β β βββ AuthorRepository.java # Author data access
β β β βββ BookRepository.java # Book data access with EntityGraph
β β βββ service/
β β β βββ BookService.java # Business logic interface
β β βββ util/
β β β βββ QueryCounter.java # SQL query counter
β β β βββ CountingStatementInspector.java # Hibernate query interceptor
β β βββ demo/
β β β βββ N1QueryDemoRunner.java # Automated demonstration
β β βββ EntityGraphApplication.java # Main application class
β βββ resources/
β βββ application.yaml # Application configuration
β βββ data.sql # Database initialization script
βββ test/
βββ java/com/example/entitygraph/
βββ service/
βββ BookServiceTest.java # Service layer tests
spring:
application:
name: entity-graph
# Database Configuration
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
# JPA Configuration
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true
use_sql_comments: true
session_factory:
statement_inspector: com.example.entitygraph.util.CountingStatementInspector
# SQL Initialization
sql:
init:
mode: always
# Logging Configuration
logging:
level:
org.hibernate.SQL: DEBUG
com.example.entitygraph: DEBUG
- Query Logging: All SQL queries are logged with formatting
- Statement Inspector: Custom interceptor counts every query
- Auto-initialization: Database populated on startup
- H2 Console: Enabled for development debugging
# Run all tests
mvn test
# Run specific test class
mvn test -Dtest=BookServiceTest
# Run with detailed logging
mvn test -Dspring.profiles.active=test
- N+1 Problem Validation: Verifies lazy loading causes multiple queries
- EntityGraph Optimization: Confirms single query execution
- Query Count Assertions: Validates exact number of SQL queries
- Data Integrity: Ensures correct data mapping and relationships
========================================
TESTING LAZY LOADING (N+1 PROBLEM)
========================================
Books fetched: 10
Total queries executed: 11
Expected: 11 queries (1 for books + 10 for authors)
========================================
TESTING ENTITY GRAPH (OPTIMIZED)
========================================
Books fetched: 10
Total queries executed: 1
Expected: 1 query (books with authors in single join)
Scenario | Books Count | Queries (Lazy) | Queries (EntityGraph) | Improvement |
---|---|---|---|---|
10 books | 10 | 11 | 1 | 90.9% |
50 books | 50 | 51 | 1 | 98.0% |
100 books | 100 | 101 | 1 | 99.0% |
Use Lazy Loading When:
- You rarely need related entities
- Working with single entities
- Memory constraints are critical
Use EntityGraph When:
- You always need related entities
- Working with collections
- Performance is critical
- Reducing database round-trips
The application includes a custom CountingStatementInspector
that:
- Intercepts every SQL query
- Assigns sequential numbers to queries
- Logs queries with detailed formatting
- Provides real-time query counting
Access the H2 database console at http://localhost:8080/h2-console
:
- Browse tables and data
- Execute custom SQL queries
- Verify data relationships
- Debug query execution
Monitor application logs to see:
SQL Query #1: SELECT b.id, b.title, b.isbn FROM books b
SQL Query #2: SELECT a.id, a.name FROM authors a WHERE ...
SQL Query #3: SELECT a.id, a.name FROM authors a WHERE ...