diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCommonAbstractCriteria.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCommonAbstractCriteria.java index a6803ba0d0..830884c0f2 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCommonAbstractCriteria.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/PersistentEntityCommonAbstractCriteria.java @@ -33,7 +33,7 @@ public interface PersistentEntityCommonAbstractCriteria extends CommonAbstractCr * @param type The type * @param The subquery type * @return A new subquery - * @see 4.10 + * @since 4.10 */ PersistentEntitySubquery subquery(ExpressionType type); diff --git a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityJoinSupport.java b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityJoinSupport.java index 59ef6fc3d3..11016911cf 100644 --- a/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityJoinSupport.java +++ b/data-model/src/main/java/io/micronaut/data/model/jpa/criteria/impl/AbstractPersistentEntityJoinSupport.java @@ -84,6 +84,21 @@ private PersistentAssociationPath getJoin(String attributeName, io. private PersistentAssociationPath getJoin(String attributeName, io.micronaut.data.annotation.Join.Type type, String alias) { PersistentProperty persistentProperty = getPersistentEntity().getPropertyByName(attributeName); + + if (persistentProperty == null && attributeName.contains(".")) { + int periodIndex = attributeName.indexOf("."); + String owner = attributeName.substring(0, periodIndex); + PersistentAssociationPath persistentAssociationPath; + if (joins.containsKey(owner)) { + persistentAssociationPath = joins.get(owner); + } else { + persistentAssociationPath = (PersistentAssociationPath) join(owner, type); + } + String remainingJoinPath = attributeName.substring(periodIndex + 1); + return alias == null ? (PersistentAssociationPath) persistentAssociationPath.join(remainingJoinPath, type) + : (PersistentAssociationPath) persistentAssociationPath.join(remainingJoinPath, type, alias); + } + if (!(persistentProperty instanceof Association association)) { throw new IllegalStateException("Expected an association for attribute name: " + attributeName); } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java index 41cf207d42..82b2b52108 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java @@ -62,6 +62,8 @@ import jakarta.persistence.criteria.Selection; import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; @@ -343,7 +345,7 @@ protected final CriteriaQueryBuilder getCriteriaQueryBuilder(MethodInvoca } } if (CollectionUtils.isNotEmpty(joinPaths)) { - for (JoinPath joinPath : joinPaths) { + for (JoinPath joinPath : sortJoinPaths(joinPaths)) { join(root, joinPath); } } @@ -384,7 +386,7 @@ private CriteriaQuery createSelectIdsCriteriaQuery(MethodInvocationCo } } if (CollectionUtils.isNotEmpty(joinPaths)) { - for (JoinPath joinPath : joinPaths) { + for (JoinPath joinPath : sortJoinPaths(joinPaths)) { join(root, joinPath); } } @@ -588,4 +590,9 @@ protected enum Type { COUNT, FIND_ONE, FIND_PAGE, FIND_ALL, DELETE_ALL, UPDATE_ALL, EXISTS } + private List sortJoinPaths(Collection joinPaths) { + List sortedJoinPaths = new ArrayList<>(joinPaths); + sortedJoinPaths.sort((o1, o2) -> Comparator.comparingInt(String::length).thenComparing(String::compareTo).compare(o1.getPath(), o2.getPath())); + return sortedJoinPaths; + } } diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy index 36f73c507f..312e78b1c2 100644 --- a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy @@ -82,6 +82,8 @@ import java.time.ZoneId import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import static io.micronaut.data.tck.repositories.AuthorRepository.Specifications.authorIdEquals +import static io.micronaut.data.tck.repositories.AuthorRepository.Specifications.authorNameEquals import static io.micronaut.data.tck.repositories.BookSpecifications.hasChapter import static io.micronaut.data.tck.repositories.BookSpecifications.titleAndTotalPagesEquals import static io.micronaut.data.tck.repositories.BookSpecifications.titleAndTotalPagesEqualsUsingConjunction @@ -1946,20 +1948,27 @@ abstract class AbstractRepositorySpec extends Specification { author.getBooks()[0].preRemove == 0 author.getBooks()[0].postRemove == 0 - def result1 = author.getBooks().find {book -> book.title == "Book1" } - result1.pages.size() == 1 - result1.pages.find {page -> page.num = 1} + verifyAuthorBooksAndPages(author) - def result2 = author.getBooks().find {book -> book.title == "Book2" } - result2.pages.size() == 2 - result2.pages.find {page -> page.num = 21} - result2.pages.find {page -> page.num = 22} + when:"Retrieve author using findOne predicate specification" + def foundAuthor = authorRepository.findOne(authorNameEquals(author.name)).orElse(null) + then:"All joined relations are loaded" + foundAuthor + foundAuthor.name == author.name + verifyAuthorBooksAndPages(foundAuthor) - def result3 = author.getBooks().find {book -> book.title == "Book3" } - result3.pages.size() == 3 - result3.pages.find {page -> page.num = 31} - result3.pages.find {page -> page.num = 32} - result3.pages.find {page -> page.num = 33} + when:"Retrieve author using findOne query specification" + def otherFoundAuthor = authorRepository.findOne(authorIdEquals(author.id)).orElse(null) + then:"All joined relations are loaded" + otherFoundAuthor + otherFoundAuthor.name == author.name + verifyAuthorBooksAndPages(otherFoundAuthor) + + when:"Retrieve author using findAll predicate specification" + def foundAuthors = authorRepository.findAll(authorNameEquals(author.name)) + then:"All joined relations are loaded using findAll" + foundAuthors.size() == 1 + verifyAuthorBooksAndPages(foundAuthors[0]) when: def newBook = new Book() @@ -1991,6 +2000,25 @@ abstract class AbstractRepositorySpec extends Specification { // author.getBooks()[0].postRemove == 1 } + def verifyAuthorBooksAndPages(Author author) { + def book1 = author.getBooks().find { book -> book.title == "Book1" } + def book2 = author.getBooks().find { book -> book.title == "Book2" } + def book3 = author.getBooks().find { book -> book.title == "Book3" } + def book1pages = book1.pages.sort {it -> it.num} + def book2pages = book2.pages.sort {it -> it.num} + def book3pages = book3.pages.sort {it -> it.num} + author.books.size() == 3 && + book1.pages.size() == 1 && + book1pages[0].num == 1 && + book2pages.size() == 2 && + book2pages[0].num == 21 && + book2pages[1].num == 22 && + book3pages.size() == 3 && + book3pages[0].num == 31 && + book3pages[1].num == 32 && + book3pages[2].num == 33 + } + void "test one-to-one mappedBy"() { when:"when a one-to-one mapped by is saved" def face = faceRepository.save(new Face("Bob")) diff --git a/data-tck/src/main/java/io/micronaut/data/tck/repositories/AuthorRepository.java b/data-tck/src/main/java/io/micronaut/data/tck/repositories/AuthorRepository.java index 35fb375489..4d9b5160bf 100644 --- a/data-tck/src/main/java/io/micronaut/data/tck/repositories/AuthorRepository.java +++ b/data-tck/src/main/java/io/micronaut/data/tck/repositories/AuthorRepository.java @@ -26,6 +26,9 @@ import io.micronaut.data.model.Page; import io.micronaut.data.model.Pageable; import io.micronaut.data.repository.CrudRepository; +import io.micronaut.data.repository.jpa.JpaSpecificationExecutor; +import io.micronaut.data.repository.jpa.criteria.PredicateSpecification; +import io.micronaut.data.repository.jpa.criteria.QuerySpecification; import io.micronaut.data.tck.entities.Author; import io.micronaut.core.annotation.Nullable; @@ -37,7 +40,7 @@ import java.util.Optional; import java.util.stream.Stream; -public interface AuthorRepository extends CrudRepository { +public interface AuthorRepository extends CrudRepository, JpaSpecificationExecutor { @Join(value = "books", type = Join.Type.LEFT_FETCH) Author queryByName(String name); @@ -48,6 +51,19 @@ public interface AuthorRepository extends CrudRepository { @Join(value = "books.pages", alias = "bp", type = Join.Type.LEFT_FETCH) Optional findById(@NonNull @NotNull Long aLong); + @Override + @Join(value = "books.pages", alias = "bp", type = Join.Type.LEFT_FETCH) + @Join(value = "books", alias = "b", type = Join.Type.LEFT_FETCH) + Optional findOne(PredicateSpecification specification); + + @Override + @Join(value = "books.pages", alias = "bp", type = Join.Type.LEFT_FETCH) + List findAll(PredicateSpecification specification); + + @Override + @Join(value = "books.pages", type = Join.Type.LEFT_FETCH) + Optional findOne(QuerySpecification specification); + Author findByName(String name); Author findByBooksTitle(String title); @@ -138,4 +154,18 @@ public interface AuthorRepository extends CrudRepository { WHERE author_.name = :name """) List findAllByNameCustom(String name); + + final class Specifications { + + private Specifications() { + } + + static PredicateSpecification authorNameEquals(String name) { + return (root, criteriaBuilder) -> criteriaBuilder.equal(root.get("name"), name); + } + + static QuerySpecification authorIdEquals(Long id) { + return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("id"), id); + } + } }