mardi 29 septembre 2020

Why specification-arg-resolver Join aliases don't work when created manually?

My application and end-to-end tests works just fine, they find proper items by tag in a db. I use in controller layer annotation-based specification-arg-resolver to easily handle requests with many parameters and create for me specifications. The problem lies in a @DataJpaTest class where I manually create Specification. Tests that uses joins fail on this:

Caused by: java.lang.IllegalArgumentException: Unable to locate Attribute  with the the given name [pt] on this ManagedType [com.tbar.makeupstoreapplication.model.Product]
at org.hibernate.metamodel.model.domain.internal.AbstractManagedType.checkNotNull(AbstractManagedType.java:147)
at org.hibernate.metamodel.model.domain.internal.AbstractManagedType.getAttribute(AbstractManagedType.java:118)
at org.hibernate.metamodel.model.domain.internal.AbstractManagedType.getAttribute(AbstractManagedType.java:43)
at org.hibernate.query.criteria.internal.path.AbstractFromImpl.locateAttributeInternal(AbstractFromImpl.java:111)
at org.hibernate.query.criteria.internal.path.AbstractPathImpl.locateAttribute(AbstractPathImpl.java:204)
at org.hibernate.query.criteria.internal.path.AbstractPathImpl.get(AbstractPathImpl.java:177)
at net.kaczmarzyk.spring.data.jpa.domain.PathSpecification.path(PathSpecification.java:50)
at net.kaczmarzyk.spring.data.jpa.domain.In.toPredicate(In.java:59)
at net.kaczmarzyk.spring.data.jpa.domain.EmptyResultOnTypeMismatch.toPredicate(EmptyResultOnTypeMismatch.java:55)
at net.kaczmarzyk.spring.data.jpa.domain.Conjunction.toPredicate(Conjunction.java:80)
at net.kaczmarzyk.spring.data.jpa.domain.Conjunction.toPredicate(Conjunction.java:80)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.applySpecificationToCriteria(SimpleJpaRepository.java:762)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.getQuery(SimpleJpaRepository.java:693)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.getQuery(SimpleJpaRepository.java:651)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll(SimpleJpaRepository.java:443)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:371)
at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:204)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:657)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:621)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:605)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:366)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)

The failing test look like this:

@Test
void shouldFindProductWithTag() {
    Product expectedProduct = createProductWithTags(1L, "ProperTag");
    productRepository.save(expectedProduct);

    Specification<Product> specification = createSpecWithTag("ProperTag");
    Pageable pageable = PageRequest.of(0, 12);

    Page<Product> actualPageOfProducts = productRepository.findAll(specification, pageable);

    assertEquals(expectedProduct, actualPageOfProducts.getContent().get(0));
}
private Conjunction<Product> createSpecWithTag(String... tagName) {
    return new Conjunction<>(createJoin("productTags", "pt"),
            new Conjunction<>(createIn("pt.name", tagName)));
}

private Join<Product> createJoin(String pathToJoinOn, String alias) {
    return new Join<>(
            new WebRequestQueryContext(nativeWebRequestMock), pathToJoinOn, alias, JoinType.INNER, true);
}

private EmptyResultOnTypeMismatch<Product> createIn(String path, String[] expectedValue) {
    return new EmptyResultOnTypeMismatch<>(
            new In<>(
                    new WebRequestQueryContext(nativeWebRequestMock), path, expectedValue,
                    Converter.withTypeMismatchBehaviour(OnTypeMismatch.EMPTY_RESULT)));
}

I copied the structure of the createSpecWithTags private method from the debugging process of real, working application where Specification's toString method looks like this:

Conjunction [innerSpecs=[Join [pathToJoinOn=productTags, alias=pt, joinType=INNER, queryContext=WebRequestQueryContext [contextMap={}], distinctQuery=true], Conjunction [innerSpecs=[EmptyResultOnTypeMismatch [wrappedSpec=net.kaczmarzyk.spring.data.jpa.domain.In@90beeb55]]]]]

Model

@Entity
@Table
@Data
@NoArgsConstructor
public class Product {

    @Id
    private Long id;
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinColumn(name = "PRODUCT_ID")
    private Set<ProductTag> productTags;
}
@Entity
@Table
@Data
@NoArgsConstructor
@RequiredArgsConstructor
public class ProductTag {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @EqualsAndHashCode.Exclude
    private int id;
    @NonNull
    private String name;
}

Repository

public interface ProductRepository extends JpaRepository<Product, Long> {

    Page<Product> findAll(Specification<Product> specification, Pageable pageable);
}

Controller method

@GetMapping
    public String shopPage(Model model,
        @Join(path = "productTags", alias = "pt")
        @And({
            @Spec(path = "pt.name", params = "product_tags", spec = In.class)
    }) Specification<Product> productSpecification, Pageable pageable) throws ProductsNotFoundException {
        Page<Product> pageOfProducts = makeupService.findProducts(productSpecification, pageable);
        addAttributesToShopModel(model, pageOfProducts);
        return ViewNames.SHOP;
    }

Aucun commentaire:

Enregistrer un commentaire