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/lazyTest 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 queriesTotal 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_idTotal 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 ...