DemoRunner: add sections H-L (Specification family, JpaSort.unsafe, Money value object)

This commit is contained in:
2026-07-25 08:20:14 +00:00
parent 0eaac42e01
commit 382e31cc37

View File

@@ -2,12 +2,15 @@ package com.ankurm.sdjpa4demo;
import com.ankurm.sdjpa4demo.domain.Author;
import com.ankurm.sdjpa4demo.domain.Book;
import com.ankurm.sdjpa4demo.domain.BookSummary;
import com.ankurm.sdjpa4demo.domain.Money;
import com.ankurm.sdjpa4demo.repo.AuthorRepository;
import com.ankurm.sdjpa4demo.repo.BookRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.JpaSort;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@@ -16,6 +19,10 @@ import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import static com.ankurm.sdjpa4demo.repo.AuthorSpecifications.byCountryDelete;
import static com.ankurm.sdjpa4demo.repo.AuthorSpecifications.hasCountry;
import static com.ankurm.sdjpa4demo.repo.AuthorSpecifications.relabelCountry;
@Component
@Profile("!test")
public class DemoRunner implements CommandLineRunner {
@@ -40,10 +47,12 @@ public class DemoRunner implements CommandLineRunner {
Author anon = authors.save(new Author("Anonymous", null)); // null country
authors.save(new Author("Martin Fowler", "UK"));
authors.save(new Author("Kent Beck", "DE")); // bookless: safe to delete in section G
Author wall1 = authors.save(new Author("Grady Booch", "US"));
Author wall2 = authors.save(new Author("Robert Martin", "US"));
books.save(new Book("Design Patterns", new BigDecimal("42.00"), gof));
books.save(new Book("Effective Java", new BigDecimal("45.00"), bloch));
books.save(new Book("Refactoring", new BigDecimal("45.00"), anon)); // duplicate price 45.00
books.save(new Book("Design Patterns", new Money(new BigDecimal("42.00"), "USD"), gof));
books.save(new Book("Effective Java", new Money(new BigDecimal("45.00"), "USD"), bloch));
books.save(new Book("Refactoring", new Money(new BigDecimal("45.00"), "USD"), anon)); // duplicate price 45.00
// (A) Optional vs non-null vs @Nullable single-result semantics
section("A. Null-handling of single-result query methods");
@@ -92,13 +101,13 @@ public class DemoRunner implements CommandLineRunner {
System.out.println("badgeFor(gof) -> " + authors.badgeFor(gof.getId()));
System.out.println("badgeFor(anon) -> " + authors.badgeFor(anon.getId()));
// (F) Single-result that matches multiple rows -> non-unique edge case
section("F. Non-unique single result edge case");
// (F) Single-result that matches multiple rows -> non-unique edge case (embedded-path version)
section("F. Non-unique single result edge case (embedded value object path)");
try {
Book b = books.findByPrice(new BigDecimal("45.00"));
System.out.println("findByPrice(45.00) -> " + b);
Book b = books.findByPriceAmount(new BigDecimal("45.00"));
System.out.println("findByPriceAmount(45.00) -> " + b);
} catch (IncorrectResultSizeDataAccessException ex) {
System.out.println("findByPrice(45.00) matches 2 rows -> throws "
System.out.println("findByPriceAmount(45.00) matches 2 rows -> throws "
+ ex.getClass().getSimpleName());
}
@@ -107,6 +116,44 @@ public class DemoRunner implements CommandLineRunner {
long deleted = authors.deleteByCountry("DE");
System.out.println("deleteByCountry('DE') removed -> " + deleted + " row(s)");
// (H) PredicateSpecification: the SAME instance reused for a read and a bulk delete
section("H. PredicateSpecification reused across read and delete");
List<Author> usAuthors = authors.findAll(hasCountry("US"));
System.out.println("findAll(hasCountry(\"US\")) -> "
+ usAuthors.stream().map(Author::getName).toList());
// (I) DeleteSpecification: explicit CriteriaDelete-typed bulk delete
section("I. DeleteSpecification bulk delete");
long removedGB = authors.delete(byCountryDelete("GB")); // no GB rows on purpose -> 0
System.out.println("delete(byCountryDelete(\"GB\")) removed -> " + removedGB + " row(s)");
long removedViaPredicate = authors.delete(hasCountry("US").and((root, cb) ->
cb.equal(root.get("name"), "Grady Booch")));
System.out.println("delete(hasCountry(\"US\").and(name=\"Grady Booch\")) removed -> "
+ removedViaPredicate + " row(s)");
// (J) UpdateSpecification: CriteriaUpdate-backed bulk update
section("J. UpdateSpecification bulk update");
long updated = authors.update(relabelCountry("UK", "GB"));
System.out.println("update(relabelCountry(\"UK\" -> \"GB\")) updated -> " + updated + " row(s)");
System.out.println("findAll(hasCountry(\"GB\")) -> "
+ authors.findAll(hasCountry("GB")).stream().map(Author::getName).toList());
// (K) JpaSort.unsafe(...) with a CASE expression, combined with a plain derived query
section("K. JpaSort.unsafe with a CASE expression");
Sort usFirst = JpaSort.unsafe(Sort.Direction.ASC,
"CASE WHEN country = 'US' THEN 0 ELSE 1 END");
List<Author> caseOrdered = authors.findByNameStartingWith("", usFirst);
System.out.println("JpaSort.unsafe(CASE WHEN country='US' ...) -> "
+ caseOrdered.stream().map(a -> a.getName() + "(" + a.getCountry() + ")").toList());
// (L) Value-object corner scenarios: embedded-path traversal + record projection
section("L. Money value object: embedded-path query and record projection");
List<Book> pricey = books.findByPriceAmountGreaterThanEqual(new BigDecimal("45.00"));
System.out.println("findByPriceAmountGreaterThanEqual(45.00) -> "
+ pricey.stream().map(Book::getTitle).toList());
List<BookSummary> summaries = books.findByPriceAmountLessThanEqual(new BigDecimal("42.00"));
summaries.forEach(s -> System.out.println("BookSummary projection -> " + s));
section("DONE");
}
}