Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for runtime criteria containing multi level joins #3305

Merged
merged 3 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public interface PersistentEntityCommonAbstractCriteria extends CommonAbstractCr
* @param type The type
* @param <U> The subquery type
* @return A new subquery
* @see 4.10
* @since 4.10
*/
<U> PersistentEntitySubquery<U> subquery(ExpressionType<U> type);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,21 @@ private <X, Y> PersistentAssociationPath<X, Y> getJoin(String attributeName, io.

private <X, Y> PersistentAssociationPath<X, Y> 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<E, ?> persistentAssociationPath;
if (joins.containsKey(owner)) {
persistentAssociationPath = joins.get(owner);
} else {
persistentAssociationPath = (PersistentAssociationPath<E, ?>) join(owner, type);
}
String remainingJoinPath = attributeName.substring(periodIndex + 1);
return alias == null ? (PersistentAssociationPath<X, Y>) persistentAssociationPath.join(remainingJoinPath, type)
: (PersistentAssociationPath<X, Y>) persistentAssociationPath.join(remainingJoinPath, type, alias);
}

if (!(persistentProperty instanceof Association association)) {
throw new IllegalStateException("Expected an association for attribute name: " + attributeName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -343,7 +345,7 @@ protected final <K> CriteriaQueryBuilder<K> getCriteriaQueryBuilder(MethodInvoca
}
}
if (CollectionUtils.isNotEmpty(joinPaths)) {
for (JoinPath joinPath : joinPaths) {
for (JoinPath joinPath : sortJoinPaths(joinPaths)) {
join(root, joinPath);
}
}
Expand Down Expand Up @@ -384,7 +386,7 @@ private <K> CriteriaQuery<Tuple> createSelectIdsCriteriaQuery(MethodInvocationCo
}
}
if (CollectionUtils.isNotEmpty(joinPaths)) {
for (JoinPath joinPath : joinPaths) {
for (JoinPath joinPath : sortJoinPaths(joinPaths)) {
join(root, joinPath);
}
}
Expand Down Expand Up @@ -588,4 +590,9 @@ protected enum Type {
COUNT, FIND_ONE, FIND_PAGE, FIND_ALL, DELETE_ALL, UPDATE_ALL, EXISTS
}

private List<JoinPath> sortJoinPaths(Collection<JoinPath> joinPaths) {
List<JoinPath> sortedJoinPaths = new ArrayList<>(joinPaths);
sortedJoinPaths.sort((o1, o2) -> Comparator.comparingInt(String::length).thenComparing(String::compareTo).compare(o1.getPath(), o2.getPath()));
return sortedJoinPaths;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -37,7 +40,7 @@
import java.util.Optional;
import java.util.stream.Stream;

public interface AuthorRepository extends CrudRepository<Author, Long> {
public interface AuthorRepository extends CrudRepository<Author, Long>, JpaSpecificationExecutor<Author> {

@Join(value = "books", type = Join.Type.LEFT_FETCH)
Author queryByName(String name);
Expand All @@ -48,6 +51,19 @@ public interface AuthorRepository extends CrudRepository<Author, Long> {
@Join(value = "books.pages", alias = "bp", type = Join.Type.LEFT_FETCH)
Optional<Author> 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<Author> findOne(PredicateSpecification<Author> specification);

@Override
@Join(value = "books.pages", alias = "bp", type = Join.Type.LEFT_FETCH)
List<Author> findAll(PredicateSpecification<Author> specification);

@Override
@Join(value = "books.pages", type = Join.Type.LEFT_FETCH)
Optional<Author> findOne(QuerySpecification<Author> specification);

Author findByName(String name);

Author findByBooksTitle(String title);
Expand Down Expand Up @@ -138,4 +154,18 @@ public interface AuthorRepository extends CrudRepository<Author, Long> {
WHERE author_.name = :name
""")
List<Author> findAllByNameCustom(String name);

final class Specifications {

private Specifications() {
}

static PredicateSpecification<Author> authorNameEquals(String name) {
return (root, criteriaBuilder) -> criteriaBuilder.equal(root.get("name"), name);
}

static QuerySpecification<Author> authorIdEquals(Long id) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("id"), id);
}
}
}
Loading