JPA & Hibernate
Build a library management system with entity relationships and Spring Data JPA
Objectives
- Create JPA entities with proper annotations
- Map @OneToMany and @ManyToMany relationships
- Use Spring Data JPA repositories
- Write custom queries with @Query and JPQL
- Implement pagination and sorting
- Identify and fix N+1 query problems
Prerequisites
- Java 17+ and Maven installed
- Understanding of SQL basics
- Completed Practical Work 2 (Spring basics)
Instructions
Step 1: Create Spring Boot Project
Create pom.xml with Spring Boot and JPA:
<?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
http://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.2.3</version>
</parent>
<groupId>com.example</groupId>
<artifactId>library-management</artifactId>
<version>1.0.0-SNAPSHOT</version>
<dependencies>
<dependency> <!-- (#1:Spring Data JPA) -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency> <!-- (#2:H2 in-memory database) -->
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Step 2: Configure H2 Database
Create src/main/resources/application.properties:
# H2 Database
spring.datasource.url=jdbc:h2:mem:librarydb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# JPA/Hibernate
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true # (#1:Show SQL in console)
# H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
Step 3: Create Author Entity
package com.example.library.entity;
import jakarta.persistence.*;
import java.util.*;
@Entity
@Table(name = "authors")
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // (#1:Auto-increment ID)
private Long id;
@Column(nullable = false)
private String name;
private String nationality;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL) // (#2:One author has many books)
private List<Book> books = new ArrayList<>();
// Constructors
public Author() {}
public Author(String name, String nationality) {
this.name = name;
this.nationality = nationality;
}
// Helper method for bidirectional relationship
public void addBook(Book book) {
books.add(book);
book.setAuthor(this);
}
// Getters and setters
public Long getId() { return id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getNationality() { return nationality; }
public void setNationality(String nationality) { this.nationality = nationality; }
public List<Book> getBooks() { return books; }
}
Step 4: Create Book Entity with Relationships
package com.example.library.entity;
import jakarta.persistence.*;
import java.util.*;
@Entity
@Table(name = "books")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
private String isbn;
private Integer publicationYear;
@ManyToOne(fetch = FetchType.LAZY) // (#1:Many books belong to one author)
@JoinColumn(name = "author_id")
private Author author;
@ManyToMany // (#2:Many-to-Many with categories)
@JoinTable(
name = "book_categories",
joinColumns = @JoinColumn(name = "book_id"),
inverseJoinColumns = @JoinColumn(name = "category_id")
)
private Set<Category> categories = new HashSet<>();
// Constructors
public Book() {}
public Book(String title, String isbn, Integer publicationYear) {
this.title = title;
this.isbn = isbn;
this.publicationYear = publicationYear;
}
// Helper methods
public void addCategory(Category category) {
categories.add(category);
category.getBooks().add(this);
}
// Getters and setters
public Long getId() { return id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getIsbn() { return isbn; }
public void setIsbn(String isbn) { this.isbn = isbn; }
public Integer getPublicationYear() { return publicationYear; }
public void setPublicationYear(Integer year) { this.publicationYear = year; }
public Author getAuthor() { return author; }
public void setAuthor(Author author) { this.author = author; }
public Set<Category> getCategories() { return categories; }
}
Step 5: Create Category Entity
package com.example.library.entity;
import jakarta.persistence.*;
import java.util.*;
@Entity
@Table(name = "categories")
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name;
@ManyToMany(mappedBy = "categories") // (#1:Inverse side of relationship)
private Set<Book> books = new HashSet<>();
public Category() {}
public Category(String name) {
this.name = name;
}
// Getters and setters
public Long getId() { return id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Set<Book> getBooks() { return books; }
}
Step 6: Create Book Repository
package com.example.library.repository;
import com.example.library.entity.Book;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface BookRepository extends JpaRepository<Book, Long> {
// (#1:Query method by convention)
List<Book> findByTitleContainingIgnoreCase(String title);
// (#2:Custom JPQL query)
@Query("SELECT b FROM Book b WHERE b.author.name = :authorName")
List<Book> findByAuthorName(@Param("authorName") String authorName);
// (#3:Query with pagination)
Page<Book> findByPublicationYearGreaterThan(Integer year, Pageable pageable);
// (#4:Fix N+1 with JOIN FETCH)
@Query("SELECT b FROM Book b JOIN FETCH b.author")
List<Book> findAllWithAuthors();
// (#5:Using @EntityGraph alternative)
@EntityGraph(attributePaths = {"author", "categories"})
List<Book> findAll();
}
Step 7: Create Main Application with Data
package com.example.library;
import com.example.library.entity.*;
import com.example.library.repository.*;
import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class LibraryApplication {
public static void main(String[] args) {
SpringApplication.run(LibraryApplication.class, args);
}
@Bean
CommandLineRunner initData(BookRepository bookRepo,
AuthorRepository authorRepo,
CategoryRepository categoryRepo) {
return args -> {
// Create categories
Category fiction = categoryRepo.save(new Category("Fiction"));
Category sciFi = categoryRepo.save(new Category("Science Fiction"));
// Create author with books
Author author = new Author("Isaac Asimov", "American");
Book book1 = new Book("Foundation", "978-0553293357", 1951);
Book book2 = new Book("I, Robot", "978-0553382563", 1950);
author.addBook(book1);
author.addBook(book2);
book1.addCategory(sciFi);
book2.addCategory(sciFi);
authorRepo.save(author);
// Test queries
System.out.println("=== Books by Asimov ===");
bookRepo.findByAuthorName("Isaac Asimov")
.forEach(b -> System.out.println(b.getTitle()));
};
}
}
Step 8: Write Repository Tests
package com.example.library;
import com.example.library.entity.*;
import com.example.library.repository.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.data.domain.*;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest // (#1:Configures in-memory DB for testing)
class BookRepositoryTest {
@Autowired
private BookRepository bookRepository;
@Autowired
private AuthorRepository authorRepository;
@Test
void shouldFindBooksByAuthorName() {
// Arrange
Author author = new Author("Test Author", "UK");
Book book = new Book("Test Book", "123", 2024);
author.addBook(book);
authorRepository.save(author);
// Act
var books = bookRepository.findByAuthorName("Test Author");
// Assert
assertEquals(1, books.size());
assertEquals("Test Book", books.get(0).getTitle());
}
@Test
void shouldReturnPagedResults() {
// Arrange - create multiple books
Author author = authorRepository.save(new Author("Author", "US"));
for (int i = 1; i <= 15; i++) {
Book book = new Book("Book " + i, "ISBN" + i, 2000 + i);
author.addBook(book);
}
authorRepository.save(author);
// Act - get first page of 5
Page<Book> page = bookRepository.findByPublicationYearGreaterThan(
2000, PageRequest.of(0, 5, Sort.by("title")));
// Assert
assertEquals(5, page.getContent().size());
assertEquals(15, page.getTotalElements());
assertEquals(3, page.getTotalPages());
}
}
Run tests:
mvn test
Deliverables
- Source Code: Complete library-management project
- Screenshot: H2 Console showing tables
- Screenshot: SQL logs showing JOIN FETCH vs N+1
Bonus Challenges
- Challenge 1: Add a Loan entity with Member relationship
- Challenge 2: Implement findOverdueLoans() custom query
- Challenge 3: Add Flyway migration for schema versioning
- Challenge 4: Enable second-level caching with Ehcache