Spring Data JPA CRUD Operations
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@Transactionalon 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
- Use DTOs — separate request/response from entities.
@Transactional(readOnly = true)on class,@Transactionalon write methods.Pageablehandles pagination and sorting automatically.- Always validate existence before update/delete.
- Return
201 CreatedwithLocationheader for POST.
What’s Next?
Next: Custom Queries — @Query, JPQL, native SQL, and Specifications.