Java + Spring Boot track

Lesson 26 · Part 6: Spring Boot

Data Persistence with JPA

So far, data has only been handled with the in-memory Map. Now we use Spring Data JPA and H2 In-Memory DB to create a persistence layer that behaves the same across…

Part 6: Spring Boot3 min readFree lesson

So far, data has only been handled with the in-memory Map. Now we use Spring Data JPA and H2 In-Memory DB to create a persistence layer that behaves the same across restarts. JPA is an ORM standard that automates the mapping between Java objects and SQL.

Learning Objectives

  • Map the class to the table with @Entity
  • Only define the JpaRepository interface and use it without implementation.
  • View actual SQL in H2 console
  • Complete a small notepad API that operates CRUD

Key concepts

1) @Entity

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

    private String title;

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

    protected Memo() {}    //JPA uses
    public Memo(String title, String body) { this.title = title; this.body = body; }
    // getters/setters ...
}
  • Default constructor (protected/public) required
  • @Id is PK
  • For @GeneratedValue(IDENTITY), DB automatically creates PK

2) JpaRepository

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

save, findAll, findById, delete, count, etc. are automatically implemented. With a naming convention (findByfield name), a simple lookup method only needs to declare a signature.

3) H2 in-memory DB

spring.datasource.url=jdbc:h2:mem:lecture26
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.jpa.show-sql=true

You can view the table by accessing the console using the browser http://localhost:8087/h2-console and entering the above URL as is in the JDBC URL.

4) Transaction

@Service
public class MemoService {
    @Transactional
    public Memo create(...) { ... }
}

All DB operations that occur within the @Transactional method must succeed before being committed, and if even one fails, they are automatically rolled back.

See as example

Example 1 — Memo.java: Entity

@Entity
@Table(name = "memos")
public class Memo {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    @Column(columnDefinition = "TEXT")
    private String body;
    // ...
}

Example 2 — MemoRepository.java: Spring Data JPA

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

Just define this interface and Spring will create and inject the implementation at runtime.

Example 3 — MemoService.java: Business Logic

@Service
@Transactional
public class MemoService {
    private final MemoRepository repository;
    public MemoService(MemoRepository repository) { this.repository = repository; }

    public Memo create(String title, String body) { ... }
    public Memo update(Long id, String title, String body) { ... }
    public void delete(Long id) { ... }
    public Memo get(Long id) { ... }
    public List<Memo> search(String q) { ... }
}

Example 4 — MemoController.java: REST API

@RestController
@RequestMapping("/api/memos")
public class MemoController {
    private final MemoService service;
    // ...
    @GetMapping public List<MemoResponse> list(...) { ... }
    @GetMapping("/{id}") public MemoResponse one(@PathVariable Long id) { ... }
    @PostMapping public MemoResponse create(@RequestBody MemoRequest req) { ... }
    @PutMapping("/{id}") public MemoResponse update(...) { ... }
    @DeleteMapping("/{id}") public ResponseEntity<Void> delete(...) { ... }
}

Run + curl scenario

cd 06_Spring_Boot/26_Data_Save
mvn spring-boot:run
#create
curl -X POST localhost:8087/api/memos -H "Content-Type: application/json" \
     -d '{"title":"Spring","body":"Good"}'
#{"id":1,"title":"Spring","body":"Good"}

#View
curl localhost:8087/api/memos
# [{"id":1, ...}]

curl localhost:8087/api/memos/1

#search
curl "localhost:8087/api/memos?q=Spr"

#Edit
curl -X PUT localhost:8087/api/memos/1 \
     -H "Content-Type: application/json" \
     -d '{"title":"Spring Boot","body":"Very good"}'

#Delete
curl -X DELETE localhost:8087/api/memos/1

Common Mistakes

  1. Startup fails because the entity does not have a default constructor
  2. Missing @Transactional → Exception in lazy loading
  3. Confusion about whether save is an update or an insert (if there is an id, it is an update)
  4. Apply DDL automatically (ddl-auto=update) to the operating environment as is
  5. Use findById(id).get() → Immediate exception if not present, orElseThrow is safe

Cleanup

  • 80% of CRUD is completed with only @Entity + JpaRepository
  • Business logic is service layer + @Transactional
  • Actual SQL and data can be checked with H2 console
  • When replacing with an external DB, only application.properties needs to be replaced.

Try it yourself

cd 06_Spring_Boot/26_Data_Save
mvn spring-boot:run
#Running the above curl scenario in another terminal

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/main/java/com/codingnow/lecture/spring26/Application.javaJava
package com.codingnow.lecture.spring26;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
src/main/java/com/codingnow/lecture/spring26/controller/MemoController.javaJava
package com.codingnow.lecture.spring26.controller;

import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.codingnow.lecture.spring26.dto.MemoRequest;
import com.codingnow.lecture.spring26.dto.MemoResponse;
import com.codingnow.lecture.spring26.service.MemoService;

@RestController
@RequestMapping("/api/memos")
public class MemoController {
    private final MemoService service;

    public MemoController(MemoService service) {
        this.service = service;
    }

    @GetMapping
    public List<MemoResponse> list(@RequestParam(required = false) String q) {
        return service.search(q).stream().map(MemoResponse::from).toList();
    }

    @GetMapping("/{id}")
    public MemoResponse one(@PathVariable Long id) {
        return MemoResponse.from(service.get(id));
    }

    @PostMapping
    public ResponseEntity<MemoResponse> create(@RequestBody MemoRequest req) {
        MemoResponse body = MemoResponse.from(service.create(req.title(), req.body()));
        return ResponseEntity.status(HttpStatus.CREATED).body(body);
    }

    @PutMapping("/{id}")
    public MemoResponse update(@PathVariable Long id, @RequestBody MemoRequest req) {
        return MemoResponse.from(service.update(id, req.title(), req.body()));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        service.delete(id);
        return ResponseEntity.noContent().build();
    }

    @ExceptionHandler(IllegalStateException.class)
    public ResponseEntity<String> handleMissing(IllegalStateException e) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleBadRequest(IllegalArgumentException e) {
        return ResponseEntity.badRequest().body(e.getMessage());
    }
}
src/main/java/com/codingnow/lecture/spring26/dto/MemoRequest.javaJava
package com.codingnow.lecture.spring26.dto;

public record MemoRequest(String title, String body) {}
src/main/java/com/codingnow/lecture/spring26/dto/MemoResponse.javaJava
package com.codingnow.lecture.spring26.dto;

import com.codingnow.lecture.spring26.entity.Memo;

public record MemoResponse(Long id, String title, String body) {
    public static MemoResponse from(Memo m) {
        return new MemoResponse(m.getId(), m.getTitle(), m.getBody());
    }
}
src/main/java/com/codingnow/lecture/spring26/entity/Memo.javaJava
package com.codingnow.lecture.spring26.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "memos")
public class Memo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

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

    protected Memo() {}

    public Memo(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; }

    public void update(String title, String body) {
        this.title = title;
        this.body = body;
    }
}
src/main/java/com/codingnow/lecture/spring26/repository/MemoRepository.javaJava
package com.codingnow.lecture.spring26.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import com.codingnow.lecture.spring26.entity.Memo;

public interface MemoRepository extends JpaRepository<Memo, Long> {
    List<Memo> findByTitleContaining(String keyword);
}
src/main/java/com/codingnow/lecture/spring26/service/MemoService.javaJava
package com.codingnow.lecture.spring26.service;

import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.codingnow.lecture.spring26.entity.Memo;
import com.codingnow.lecture.spring26.repository.MemoRepository;

@Service
@Transactional
public class MemoService {
    private final MemoRepository repository;

    public MemoService(MemoRepository repository) {
        this.repository = repository;
    }

    public Memo create(String title, String body) {
        if (title == null || title.isBlank()) {
            throw new IllegalArgumentException("title required");
        }
        return repository.save(new Memo(title, body));
    }

    @Transactional(readOnly = true)
    public List<Memo> findAll() {
        return repository.findAll();
    }

    @Transactional(readOnly = true)
    public List<Memo> search(String keyword) {
        if (keyword == null || keyword.isBlank()) return repository.findAll();
        return repository.findByTitleContaining(keyword);
    }

    @Transactional(readOnly = true)
    public Memo get(Long id) {
        return repository.findById(id)
            .orElseThrow(() -> new IllegalStateException("memo not found: " + id));
    }

    public Memo update(Long id, String title, String body) {
        Memo memo = get(id);
        memo.update(title, body);
        return memo;
    }

    public void delete(Long id) {
        if (!repository.existsById(id)) {
            throw new IllegalStateException("memo not found: " + id);
        }
        repository.deleteById(id);
    }
}
src/main/resources/application.propertiesText
server.port=8087
spring.application.name=lecture-spring26

# H2
spring.datasource.url=jdbc:h2:mem:lecture26;DB_CLOSE_DELAY=-1
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# H2 console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

Assignment

Problem — Book CRUD with JPA

  • Reference solution: shown below
  • Core concepts: @Entity, JpaRepository, @Transactional, H2

Requirements

  • Entity Book(id, title, author, pages)
  • BookRepository extends JpaRepository<Book, Long>
  • CRUD of BookService: create, list, get, update, delete (404 if not present)
  • Controller /api/books provides all 5 methods
  • DB is H2 in-memory + ddl-auto=update

Expected behavior

$ curl -X POST localhost:8088/api/books -H "Content-Type: application/json" \
       -d '{"title":"Effective Java","author":"Joshua Bloch","pages":384}'
$ curl localhost:8088/api/books
$ curl localhost:8088/api/books/1
$ curl -X PUT localhost:8088/api/books/1 -H "Content-Type: application/json" \
       -d '{"title":"...","author":"...","pages":400}'
$ curl -X DELETE localhost:8088/api/books/1

Check the correct answer

Look at the Maven project in the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

pom.xmlXML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.6</version>
        <relativePath/>
    </parent>

    <groupId>com.codingnow</groupId>
    <artifactId>lecture-spring26-hw</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>21</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <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>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
src/main/java/com/codingnow/lecture/spring26hw/Application.javaJava
package com.codingnow.lecture.spring26hw;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
src/main/java/com/codingnow/lecture/spring26hw/controller/BookController.javaJava
package com.codingnow.lecture.spring26hw.controller;

import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.codingnow.lecture.spring26hw.dto.BookRequest;
import com.codingnow.lecture.spring26hw.dto.BookResponse;
import com.codingnow.lecture.spring26hw.service.BookService;

@RestController
@RequestMapping("/api/books")
public class BookController {
    private final BookService service;

    public BookController(BookService service) {
        this.service = service;
    }

    @GetMapping
    public List<BookResponse> list() {
        return service.findAll().stream().map(BookResponse::from).toList();
    }

    @GetMapping("/{id}")
    public BookResponse one(@PathVariable Long id) {
        return BookResponse.from(service.get(id));
    }

    @PostMapping
    public ResponseEntity<BookResponse> create(@RequestBody BookRequest req) {
        BookResponse body = BookResponse.from(service.create(req.title(), req.author(), req.pages()));
        return ResponseEntity.status(HttpStatus.CREATED).body(body);
    }

    @PutMapping("/{id}")
    public BookResponse update(@PathVariable Long id, @RequestBody BookRequest req) {
        return BookResponse.from(service.update(id, req.title(), req.author(), req.pages()));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        service.delete(id);
        return ResponseEntity.noContent().build();
    }

    @ExceptionHandler(IllegalStateException.class)
    public ResponseEntity<String> handleMissing(IllegalStateException e) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
    }
}
src/main/java/com/codingnow/lecture/spring26hw/dto/BookRequest.javaJava
package com.codingnow.lecture.spring26hw.dto;

public record BookRequest(String title, String author, int pages) {}
src/main/java/com/codingnow/lecture/spring26hw/dto/BookResponse.javaJava
package com.codingnow.lecture.spring26hw.dto;

import com.codingnow.lecture.spring26hw.entity.Book;

public record BookResponse(Long id, String title, String author, int pages) {
    public static BookResponse from(Book b) {
        return new BookResponse(b.getId(), b.getTitle(), b.getAuthor(), b.getPages());
    }
}
src/main/java/com/codingnow/lecture/spring26hw/entity/Book.javaJava
package com.codingnow.lecture.spring26hw.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "books")
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String author;
    private int pages;

    protected Book() {}

    public Book(String title, String author, int pages) {
        this.title = title;
        this.author = author;
        this.pages = pages;
    }

    public Long getId() { return id; }
    public String getTitle() { return title; }
    public String getAuthor() { return author; }
    public int getPages() { return pages; }

    public void update(String title, String author, int pages) {
        this.title = title;
        this.author = author;
        this.pages = pages;
    }
}
src/main/java/com/codingnow/lecture/spring26hw/repository/BookRepository.javaJava
package com.codingnow.lecture.spring26hw.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.codingnow.lecture.spring26hw.entity.Book;

public interface BookRepository extends JpaRepository<Book, Long> {
}
src/main/java/com/codingnow/lecture/spring26hw/service/BookService.javaJava
package com.codingnow.lecture.spring26hw.service;

import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.codingnow.lecture.spring26hw.entity.Book;
import com.codingnow.lecture.spring26hw.repository.BookRepository;

@Service
@Transactional
public class BookService {
    private final BookRepository repository;

    public BookService(BookRepository repository) {
        this.repository = repository;
    }

    public Book create(String title, String author, int pages) {
        return repository.save(new Book(title, author, pages));
    }

    @Transactional(readOnly = true)
    public List<Book> findAll() {
        return repository.findAll();
    }

    @Transactional(readOnly = true)
    public Book get(Long id) {
        return repository.findById(id)
            .orElseThrow(() -> new IllegalStateException("book not found: " + id));
    }

    public Book update(Long id, String title, String author, int pages) {
        Book b = get(id);
        b.update(title, author, pages);
        return b;
    }

    public void delete(Long id) {
        if (!repository.existsById(id)) {
            throw new IllegalStateException("book not found: " + id);
        }
        repository.deleteById(id);
    }
}
src/main/resources/application.propertiesText
server.port=8088
spring.application.name=lecture-spring26-hw

spring.datasource.url=jdbc:h2:mem:books;DB_CLOSE_DELAY=-1
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

Learn this lesson with a mentor

Work through the Java + Spring Boot track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now