spring-projects / spring-data-jpa

Simplifies the development of creating a JPA-based data access layer.
https://spring.io/projects/spring-data-jpa/
Apache License 2.0
3.01k stars 1.42k forks source link

JpaQueryCreator fails using interfaces for projections in SpringDataJPA 2.6.1 #2427

Closed jcconca closed 2 years ago

jcconca commented 2 years ago

Hi guys:

In one of the projects I'm working now we decide to upgrade to SpringBoot 2.6.3

And as part of the upgrade one of the problems that we detect is:

Caused by: java.lang.IllegalArgumentException: Failed to create query for method public abstract java.util.List infrastructure.document.SpringDataDocumentRepository. findByMortgageRequestIdOrderByCreationDateDesc(java.lang.String)! null
    at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.<init>(PartTreeJpaQuery.java:96)
    at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$CreateQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:113)
    at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$CreateIfNotFoundQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:254)
    at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$AbstractQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:87)
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.lookupQuery(QueryExecutorMethodInterceptor.java:102)
    ... 80 common frames omitted
Caused by: java.lang.NullPointerException: null
    at org.springframework.data.jpa.repository.query.JpaQueryCreator.complete(JpaQueryCreator.java:181)
    at org.springframework.data.jpa.repository.query.JpaQueryCreator.complete(JpaQueryCreator.java:152)
    at org.springframework.data.jpa.repository.query.JpaQueryCreator.complete(JpaQueryCreator.java:59)
    at org.springframework.data.repository.query.parser.AbstractQueryCreator.createQuery(AbstractQueryCreator.java:95)
    at org.springframework.data.repository.query.parser.AbstractQueryCreator.createQuery(AbstractQueryCreator.java:81)
    at org.springframework.data.jpa.repository.query.PartTreeJpaQuery$QueryPreparer.<init>(PartTreeJpaQuery.java:217)
    at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.<init>(PartTreeJpaQuery.java:92)
    ... 84 common frames omitted

After having a look, seems that the method getTypeToRead() is returning a null under certain circumstances:

        @Nullable
        public Class<?> getTypeToRead() {
            return this.isProjecting() && this.information.isClosed() ? null : this.domainType;
        }

but is not controlled in the code that requested in JpaQueryCreator and fails running this command:

query = typeToRead.isInterface() ? query.multiselect(selections) : query.select((Selection) builder.construct(typeToRead, selections.toArray(new Selection[0])));

JpaQueryCreator:

      protected CriteriaQuery<? extends Object> complete(@Nullable Predicate predicate, Sort sort,
            CriteriaQuery<? extends Object> query, CriteriaBuilder builder, Root<?> root) {

        if (returnedType.needsCustomConstruction()) {

            List<Selection<?>> selections = new ArrayList<>();

            for (String property : returnedType.getInputProperties()) {

                PropertyPath path = PropertyPath.from(property, returnedType.getDomainType());
                selections.add(toExpressionRecursively(root, path, true).alias(property));
            }

            Class<?> typeToRead = returnedType.getTypeToRead();

            query = typeToRead.isInterface()
                    ? query.multiselect(selections)
                    : query.select((Selection) builder.construct(typeToRead,
                            selections.toArray(new Selection[0])));

        } else if (tree.isExistsProjection()) {

            if (root.getModel().hasSingleIdAttribute()) {

                SingularAttribute<?, ?> id = root.getModel().getId(root.getModel().getIdType().getJavaType());
                query = query.multiselect(root.get((SingularAttribute) id).alias(id.getName()));

            } else {

                query = query.multiselect(root.getModel().getIdClassAttributes().stream()//
                        .map(it -> (Selection<?>) root.get((SingularAttribute) it).alias(it.getName()))
                        .collect(Collectors.toList()));
            }

        } else {
            query = query.select((Root) root);
        }

        CriteriaQuery<? extends Object> select = query.orderBy(QueryUtils.toOrders(sort, root, builder));
        return predicate == null ? select : select.where(predicate);
    }

Why if the interface projection has the same properties as the input properties, is declared as closed and is required to return null?

public boolean isClosed() { return this.properties.equals(this.getInputProperties()); }

and why check if the TypeToRead is an interface over a possible null value?

If this helps, this are the classes that I have in my project and produce the error after the migration from SpringDataJpa 2.4.X to 2.6.X.

Entity embeddable id:

@Embeddable
public class DocumentId extends StringEntityId {

    public DocumentId() {
    }

    public DocumentId(String value) {
        super(value);
    }
}

Document entity:

@Entity
@EntityListeners(AuditingEntityListener.class)
public class Document implements Anonymizable, Serializable {

    @EmbeddedId
    private DocumentId id;

    @Column(nullable = false)
    private String mortgageRequestId;

    @Column(nullable = false)
    @Lob
    private byte[] document;

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDate creationDate;

    @Column(nullable = false)
    private String dataId;

    @Column(nullable = false)
    private String fileName;

    @Column(nullable = false)
    private String mimeType;

Projection interface

public interface DocumentName {

    DocumentId getId();

    String getFileName();

    String getMimeType();

    LocalDate getCreationDate();
}

Repository

public interface SpringDataDocumentRepository extends JpaRepository<Document, DocumentId> {

    Document findByDataId(final String dataId);

    List<DocumentName> findByMortgageRequestId(final String mortgageRequestId);

    List<DocumentName> findByMortgageRequestIdOrderByCreationDateDesc(final String mortgageRequestId);

    Optional<Document> findByIdAndMortgageRequestId(DocumentId id, String mortgageRequestId);
}

Any idea to solve this specific scenario?

schauder commented 2 years ago

Duplicate of #2408

jcconca commented 2 years ago

How is posible that is solved in 2.6.1 and fails in that exact version? @schauder

Which version has the bug fixed? I only see this comment about the version:

This should be fixed now in 2.6, 2.7 and 3.0 snapshots. by @odrotbohm

schauder commented 2 years ago

How is posible that is solved in 2.6.1 and fails in that exact version? I don't think anybody said this is fixed in 2.6.1

The fix will be included in the next releases of the 2.6, 2.7 and 3.0 branches: 2.6.2, 2.7.0-M3, and 3.0.0-M2

You can tell by the branches and tags the fixing commits are contained in. If it is in a tag for a version the fix is included in that version. if it only in main or a version.x branch it will be included in those branches next version.