diff --git a/src/main/java/com/ankurm/sdjpa4demo/DemoRunner.java b/src/main/java/com/ankurm/sdjpa4demo/DemoRunner.java index 62e76d9..84cbad7 100644 --- a/src/main/java/com/ankurm/sdjpa4demo/DemoRunner.java +++ b/src/main/java/com/ankurm/sdjpa4demo/DemoRunner.java @@ -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 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 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 pricey = books.findByPriceAmountGreaterThanEqual(new BigDecimal("45.00")); + System.out.println("findByPriceAmountGreaterThanEqual(45.00) -> " + + pricey.stream().map(Book::getTitle).toList()); + List summaries = books.findByPriceAmountLessThanEqual(new BigDecimal("42.00")); + summaries.forEach(s -> System.out.println("BookSummary projection -> " + s)); + section("DONE"); } }