← Back to Java series
🌱
Spring Boot
Spring Boot Β· Prerequisite: previous lecture

26. Spring Data JPA and H2

Until now we used in-memory `Map`s. Now we add a **persistence layer** with **Spring Data JPA** and the **H2 in-memory database** so data survives between calls. JPA is the ORM standard that automates the mapping between Java objects and SQL.

Spring BootJavaJPAH2@Entity
Duration
⏱ ~1.5-2 hours
Level
πŸ“Š Intermediate-Advanced
Prerequisite
🎯 Previous lecture or equivalent knowledge
OUTCOME
Until now we used in-memory `Map`s. Now we add a **persistence layer** with **Spring Data JPA** and the **H2 in-memory database** so data survives between calls. JPA is the ORM standard that automates the mapping between Java objects and SQL.

What you'll learn

  • 1Map a class to a table with `@Entity`
  • 2Get CRUD for free by defining a `JpaRepository`
  • 3Inspect the actual SQL via the H2 console
  • 4Wire up a small CRUD notepad API end-to-end

Overview

Until now we used in-memory `Map`s. Now we add a **persistence layer** with **Spring Data JPA** and the **H2 in-memory database** so data survives between calls. JPA is the ORM standard that automates the mapping between Java objects and SQL.

Core Concepts

1) Dependencies

xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>runtime</scope>
</dependency>

2) `application.properties`

properties
spring.datasource.url=jdbc:h2:mem:demo
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.h2.console.enabled=true

Visit `http://localhost:8080/h2-console` and connect with `jdbc:h2:mem:demo` to see the data.

3) `@Entity`

java
import jakarta.persistence.*;

@Entity
@Table(name = "notes")
public class Note {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String title;

    @Column(columnDefinition = "TEXT")
    private String body;

    protected Note() {}     // JPA needs a no-arg ctor
    public Note(String title, String body) { this.title = title; this.body = body; }

    public Long getId() { return id; }
    public String getTitle() { return title; }
    public String getBody() { return body; }
}

4) `JpaRepository`

java
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface NoteRepository extends JpaRepository<Note, Long> {
    List<Note> findByTitleContaining(String keyword);    // query method
}

Spring generates the implementation at startup β€” no SQL needed for basic CRUD.

5) Standard CRUD methods

MethodEffect
`save(entity)`INSERT or UPDATE
`findById(id)`SELECT one (returns `Optional`)
`findAll()`SELECT all
`deleteById(id)`DELETE
`count()`COUNT

Examples

Example 1 β€” `Note.java` (entity)

java
package com.example.demo;

import jakarta.persistence.*;

@Entity
@Table(name = "notes")
public class Note {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false, length = 200) private String title;
    @Column(columnDefinition = "TEXT") private String body;

    protected Note() {}
    public Note(String title, String body) { this.title = title; this.body = body; }

    public Long getId() { return id; }
    public String getTitle() { return title; }
    public String getBody() { return body; }
}

Example 2 β€” `NoteRepository.java`

java
package com.example.demo;

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface NoteRepository extends JpaRepository<Note, Long> {
    List<Note> findByTitleContaining(String keyword);
}

Example 3 β€” `NoteController.java`

java
package com.example.demo;

import java.util.List;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/notes")
public class NoteController {
    private final NoteRepository repo;
    public NoteController(NoteRepository repo) { this.repo = repo; }

    record CreateNoteRequest(String title, String body) {}
    record NoteResponse(Long id, String title, String body) {}

    @GetMapping
    public List<NoteResponse> all() {
        return repo.findAll().stream()
            .map(n -> new NoteResponse(n.getId(), n.getTitle(), n.getBody()))
            .toList();
    }

    @GetMapping("/{id}")
    public ResponseEntity<NoteResponse> get(@PathVariable Long id) {
        return repo.findById(id)
            .map(n -> ResponseEntity.ok(new NoteResponse(n.getId(), n.getTitle(), n.getBody())))
            .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<NoteResponse> create(@RequestBody CreateNoteRequest req) {
        Note saved = repo.save(new Note(req.title(), req.body()));
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(new NoteResponse(saved.getId(), saved.getTitle(), saved.getBody()));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        if (!repo.existsById(id)) return ResponseEntity.notFound().build();
        repo.deleteById(id);
        return ResponseEntity.noContent().build();
    }

    @GetMapping("/search")
    public List<NoteResponse> search(@RequestParam String q) {
        return repo.findByTitleContaining(q).stream()
            .map(n -> new NoteResponse(n.getId(), n.getTitle(), n.getBody()))
            .toList();
    }
}

Example 4 β€” curl walkthrough

bash
$ curl -X POST localhost:8080/notes -H 'Content-Type: application/json' \
       -d '{"title":"shopping","body":"milk, bread"}'
{"id":1,"title":"shopping","body":"milk, bread"}

$ curl localhost:8080/notes
[{"id":1,"title":"shopping","body":"milk, bread"}]

$ curl 'localhost:8080/notes/search?q=shop'
[{"id":1,"title":"shopping","body":"milk, bread"}]

$ curl -X DELETE localhost:8080/notes/1

Common Mistakes

  1. Forgetting a protected/public no-arg constructor on the entity β†’ JPA refuses to map
  2. Returning entities directly to the client (lazy collections, security leaks) β€” map to DTO
  3. Using `spring.jpa.hibernate.ddl-auto=create-drop` in production (data wiped on restart)
  4. Holding the entity outside the transactional boundary and getting `LazyInitializationException`
  5. Writing a custom `save` that overrides `JpaRepository.save` and breaks Hibernate state management

Summary

  • `@Entity` maps a class to a table; JPA handles the SQL
  • `JpaRepository` gives you CRUD for free
  • H2 + the H2 console are perfect for learning

Practice

# Practice - 26. Spring Data JPA

## Exercise 1 β€” `Book` entity + repository

  • Map a `Book(id, title, author)` entity.
  • Add `findByAuthor(String author)` to the repository.

## Exercise 2 β€” REST endpoints for `/books`

  • Implement Create / List / Get / Delete using the repository.

Expected

text
$ curl localhost:8080/books
[{"id":1,"title":"Effective Java","author":"Joshua Bloch"}]

## Solutions After trying it yourself, compare with [`answer/`](./answer/).

Solution code (homework/answer/)

answer/Book.java

java
package com.example.demo;

import jakarta.persistence.*;

@Entity
@Table(name = "books")
public class Book {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false, length = 200) private String title;
    @Column(nullable = false, length = 100) private String author;

    protected Book() {}
    public Book(String title, String author) { this.title = title; this.author = author; }
    public Long getId() { return id; }
    public String getTitle() { return title; }
    public String getAuthor() { return author; }
}

answer/BookRepository.java

java
package com.example.demo;

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, Long> {
    List<Book> findByAuthor(String author);
}

answer/BookController.java

java
package com.example.demo;

import java.util.List;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/books")
public class BookController {
    private final BookRepository repo;
    public BookController(BookRepository repo) { this.repo = repo; }

    record CreateBookRequest(String title, String author) {}
    record BookResponse(Long id, String title, String author) {}

    @GetMapping
    public List<BookResponse> all() {
        return repo.findAll().stream()
            .map(b -> new BookResponse(b.getId(), b.getTitle(), b.getAuthor()))
            .toList();
    }

    @PostMapping
    public ResponseEntity<BookResponse> create(@RequestBody CreateBookRequest req) {
        Book saved = repo.save(new Book(req.title(), req.author()));
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(new BookResponse(saved.getId(), saved.getTitle(), saved.getAuthor()));
    }

    @GetMapping("/{id}")
    public ResponseEntity<BookResponse> get(@PathVariable Long id) {
        return repo.findById(id)
            .map(b -> ResponseEntity.ok(new BookResponse(b.getId(), b.getTitle(), b.getAuthor())))
            .orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        if (!repo.existsById(id)) return ResponseEntity.notFound().build();
        repo.deleteById(id);
        return ResponseEntity.noContent().build();
    }
}

Try It Yourself

bash
mvn spring-boot:run
# Then open http://localhost:8080/h2-console
# JDBC URL: jdbc:h2:mem:demo

Next Lecture

Nice work β€” you've finished the course!

You completed all 26 lectures. Where to next?

  • Authentication / authorization (Spring Security)
  • Real databases (PostgreSQL / MySQL)
  • Calling external APIs (RestTemplate / WebClient)
  • Testing (Mockito + MockMvc)
  • Deployment (Docker / Cloud)

Keep building. The mini-project workbook in the root [README](../../) is a good next step.

Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub β†—