← Back to Advanced Java
Practical Work 3

JPA & Hibernate

Build a library management system with entity relationships and Spring Data JPA

Duration4-5 hours
DifficultyIntermediate
Session3 - JPA & Hibernate

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

Resources