Spring Data JPA CRUD Operations

2 minute read

This chapter implements complete CRUD with pagination, sorting, and DTOs for our Bookstore API.


DTOs — Request and Response

// Request DTO
public record CreateBookRequest(
    @NotBlank String title,
    @NotBlank String author,
    String isbn,
    @NotNull @Positive BigDecimal price
) {}

public record UpdateBookRequest(
    @NotBlank String title,
    @NotBlank String author,
    @Positive BigDecimal price
) {}

// Response DTO
public record BookResponse(
    Long id,
    String title,
    String author,
    String isbn,
    BigDecimal price,
    String status,
    LocalDateTime createdAt
) {
    public static BookResponse from(Book book) {
        return new BookResponse(
            book.getId(), book.getTitle(), book.getAuthor(),
            book.getIsbn(), book.getPrice(),
            book.getStatus().name(), book.getCreatedAt()
        );
    }
}

Service Layer — Full CRUD

@Service
@Transactional(readOnly = true)
public class BookService {

    private final BookRepository bookRepository;

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

    // READ - all with pagination
    public Page<BookResponse> findAll(Pageable pageable) {
        return bookRepository.findAll(pageable).map(BookResponse::from);
    }

    // READ - by ID
    public BookResponse findById(Long id) {
        Book book = bookRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Book", id));
        return BookResponse.from(book);
    }

    // CREATE
    @Transactional
    public BookResponse create(CreateBookRequest request) {
        if (request.isbn() != null && bookRepository.existsByIsbn(request.isbn())) {
            throw new DuplicateResourceException("ISBN already exists: " + request.isbn());
        }
        Book book = new Book(
            request.title(), request.author(),
            request.isbn(), request.price()
        );
        Book saved = bookRepository.save(book);
        return BookResponse.from(saved);
    }

    // UPDATE
    @Transactional
    public BookResponse update(Long id, UpdateBookRequest request) {
        Book book = bookRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Book", id));

        book.setTitle(request.title());
        book.setAuthor(request.author());
        book.setPrice(request.price());

        Book saved = bookRepository.save(book);
        return BookResponse.from(saved);
    }

    // DELETE
    @Transactional
    public void delete(Long id) {
        if (!bookRepository.existsById(id)) {
            throw new ResourceNotFoundException("Book", id);
        }
        bookRepository.deleteById(id);
    }
}

@Transactional(readOnly = true) on the class — read operations are optimized. Override with @Transactional on write methods.


Controller

@RestController
@RequestMapping("/api/books")
public class BookController {

    private final BookService bookService;

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

    @GetMapping
    public Page<BookResponse> getAll(Pageable pageable) {
        return bookService.findAll(pageable);
    }

    @GetMapping("/{id}")
    public BookResponse getById(@PathVariable Long id) {
        return bookService.findById(id);
    }

    @PostMapping
    public ResponseEntity<BookResponse> create(@Valid @RequestBody CreateBookRequest request) {
        BookResponse response = bookService.create(request);
        URI location = URI.create("/api/books/" + response.id());
        return ResponseEntity.created(location).body(response);
    }

    @PutMapping("/{id}")
    public BookResponse update(@PathVariable Long id,
                               @Valid @RequestBody UpdateBookRequest request) {
        return bookService.update(id, request);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        bookService.delete(id);
    }
}

Pagination and Sorting

# Page 0, 10 items per page
GET /api/books?page=0&size=10

# Sorted by price descending
GET /api/books?page=0&size=10&sort=price,desc

# Multiple sorts
GET /api/books?sort=author,asc&sort=title,asc

Paginated response:

{
  "content": [...],
  "pageable": { "pageNumber": 0, "pageSize": 10 },
  "totalElements": 42,
  "totalPages": 5,
  "first": true,
  "last": false,
  "numberOfElements": 10
}

Key Takeaways

  1. Use DTOs — separate request/response from entities.
  2. @Transactional(readOnly = true) on class, @Transactional on write methods.
  3. Pageable handles pagination and sorting automatically.
  4. Always validate existence before update/delete.
  5. Return 201 Created with Location header for POST.

What’s Next?

Next: Custom Queries — @Query, JPQL, native SQL, and Specifications.

Updated: