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
2.98k stars 1.41k forks source link

Can not use Spring's Sort to access generic fields, using both Sort and TypedSort #3382

Open dtthang912 opened 7 months ago

dtthang912 commented 7 months ago

I can not use Spring's Sort function to access generic fields in entities (I'm using Spring Boot 3.1.5, Hibernate 6.2.3). Here is my entity code:

@Data
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AbstractLog<I extends AbstractLogDetail<?, ?>> {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "action")
    @Enumerated(EnumType.STRING)
    private Action action;

    @OneToMany(mappedBy = "log", cascade = CascadeType.ALL)
    private List<I> changeLog;
}

@Data
@MappedSuperclass
public abstract class AbstractLogDetail<A extends AbstractLog<?>, E> {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @ManyToOne
    @JoinColumn(name = "log_id", nullable = false)
    private A log;

    @ManyToOne
    @JoinColumn(name = "entity_id", nullable = false)
    private E entity;
    //detail fields
}

@Data
@Entity
@Table(name = "supporter_log")
public class SupporterLog extends AbstractLog<SupporterLogDetail> {
}

@Data
@Entity
@Table(name = "supporter_log_detail")
public class SupporterLogDetail extends AbstractLogDetail<SupporterLog, Supporter> {

}

@Getter
@Setter
@Entity
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "supporter")
public class Supporter {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "level", nullable = false)
    private int level;

    @Column(name = "status", nullable = false)
    @Convert(converter = StatusConverter.class)
    private Status status;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    @ToString.Exclude
    private User user;
}

Here is how I use Spring's Sort to access the displayName field, in which entity is AbstractLogDetail's generic field:

List<Sort.Order> updatedOrders = pageable.getSort().stream()
        .map(order -> {
            String property = order.getProperty();
            return switch (property) {
                case "entityField" -> new Sort.Order(order.getDirection(), "entity.user.displayName");
                default -> order;
        };
        })
        .toList();
pageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by(updatedOrders));

Here is the error:

org.springframework.dao.InvalidDataAccessApiUsageException: Unable to locate Attribute with the given name [user] on this ManagedType [java.lang.Object]
    at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:371)
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:234)
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:550)
    at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
Caused by: java.lang.IllegalArgumentException: Unable to locate Attribute with the given name [user] on this ManagedType [java.lang.Object]
    at org.hibernate.metamodel.model.domain.AbstractManagedType.checkNotNull(AbstractManagedType.java:287)
    at org.hibernate.metamodel.model.domain.AbstractManagedType.getAttribute(AbstractManagedType.java:160)
    at org.hibernate.metamodel.model.domain.AbstractManagedType.getAttribute(AbstractManagedType.java:51)
    at org.springframework.data.jpa.repository.query.QueryUtils.requiresOuterJoin(QueryUtils.java:836)
    at org.springframework.data.jpa.repository.query.QueryUtils.toExpressionRecursively(QueryUtils.java:777)
    at org.springframework.data.jpa.repository.query.QueryUtils.toExpressionRecursively(QueryUtils.java:796)
    at org.springframework.data.jpa.repository.query.QueryUtils.toExpressionRecursively(QueryUtils.java:756)
    at org.springframework.data.jpa.repository.query.QueryUtils.toExpressionRecursively(QueryUtils.java:752)
    at org.springframework.data.jpa.repository.query.QueryUtils.toJpaOrder(QueryUtils.java:741)
    at org.springframework.data.jpa.repository.query.QueryUtils.toOrders(QueryUtils.java:693)
    at org.springframework.data.jpa.repository.support.SimpleJpaRepository.getQuery(SimpleJpaRepository.java:753)
    at org.springframework.data.jpa.repository.support.SimpleJpaRepository.getQuery(SimpleJpaRepository.java:710)
    at org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll(SimpleJpaRepository.java:447)

I think it's because the type "E entity" was transformed to Object after being compiled. I even tested TypedSort to see if it can access the generic field:

 Sort sort = Sort.sort(FundingApproverActivityChangeLog.class)
            .by((FundingApproverActivityChangeLog e) -> e.getEntity().getUser().getDisplayName()).ascending();
    pageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort);

But seems like Spring can not create a proxy, I got this error:

org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class java.lang.Object: Common causes of this problem include using a final class or a non-visible class
    at org.springframework.aop.framework.CglibAopProxy.buildProxy(CglibAopProxy.java:216)
    at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:158)
    at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110)
    at org.springframework.data.util.MethodInvocationRecorder.create(MethodInvocationRecorder.java:100)
    at org.springframework.data.util.MethodInvocationRecorder$RecordingMethodInterceptor.registerInvocation(MethodInvocationRecorder.java:159)
    at org.springframework.data.util.MethodInvocationRecorder$RecordingMethodInterceptor.invoke(MethodInvocationRecorder.java:150)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:702)
    at com.ntuc.lhub.custom.common.data.entity.funding.FundingApproverActivityChangeLog$$SpringCGLIB$$0.getEntity(<generated>)
Caused by: org.springframework.cglib.core.CodeGenerationException: java.lang.reflect.InaccessibleObjectException-->Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @6b419da
    at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:545)
    at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:367)
    at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:575)
    at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.lambda$new$1(AbstractClassGenerator.java:103)
    at org.springframework.cglib.core.internal.LoadingCache.lambda$createEntry$1(LoadingCache.java:52)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at org.springframework.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:57)
    at org.springframework.cglib.core.internal.LoadingCache.get(LoadingCache.java:34)
mp911de commented 7 months ago

This ticket seems related to #3307 where Hibernate loses type information on parametrized type paths.