映射自关联双向实体问题

Mapping self associated bidirectional entity issue

我有以下具有自关联的参数实体,我正在尝试进行单元测试以检查我的实现,但感觉我在映射中犯了一些错误,我无法检测到哪里我错了。

我的实体如下

import java.io.Serializable;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

import com.tuto.common.enums.ParameterCategory;
import com.tuto.common.enums.ParameterType;
import com.tuto.common.enums.converter.ParameterTypePersistenceConverter;

/**
 * This class represents the PARAMETERS SQL table as a java entity.
 *
*/

@Entity
@Table(name = "PARAMETERS")
public class Parameter implements Serializable {

    /**
     * serialVersionUID.
     */
    private static final long serialVersionUID = -732987999122243011L;

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SQ_PARAMETER_ID")
    @SequenceGenerator(name = "SQ_PARAMETER_ID", sequenceName = "SQ_PARAMETER_ID", allocationSize = 1)
    @Column(name = "PARAMETER_ID", unique = true)
    private long id;
    
    @Column(name = "PARAMETER_NAME")
    private String name;
    
    @Column(name = "PARAMETER_LABEL")
    private String label;
    
    @Column(name = "PARAMETER_COMMENT")
    private String comment;

    @JoinColumn(name = "ES_ID")
    @OneToOne(fetch = FetchType.LAZY)
    private ExpertSystem expertSystem;

    @Column(name = "PARAMETER_CATEGORY")
    @Enumerated(EnumType.ORDINAL)
    private ParameterCategory category;
    
    @Column(name = "PARAMETER_TYPE")
    @Convert(converter = ParameterTypePersistenceConverter.class)
    private ParameterType type;


    @Column(name = "PARAMETER_CREATION_DATE",columnDefinition = "TIMESTAMP")
    private OffsetDateTime creationDate;

    @JoinColumn(name = "PARAMETER_CREATION_USER",referencedColumnName = "USER_FIRSTNAME")
    @OneToOne(fetch = FetchType.LAZY)
    private User creationUser;

    @Column(name = "PARAMETER_UPDATE_DATE",columnDefinition = "TIMESTAMP")
    private OffsetDateTime updateDate;

    @JoinColumn(name = "PARAMETER_UPDATE_USER",referencedColumnName = "USER_FIRSTNAME")
    @OneToOne(fetch = FetchType.LAZY)
    private User updateUser;

    @Column(name = "MULTIVALUE_SIZE")
    private int multivalueSize;
 
    @OneToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "dependences",
               joinColumns = {@JoinColumn(name ="PARAMETER_ID")},
               inverseJoinColumns = {@JoinColumn(name ="DEPENDANCE_ID")})
    private List<Parameter> dependences = new ArrayList<>();
    
    @OneToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "dependences",
               joinColumns = {@JoinColumn(name ="DEPENDANCE_ID")},
               inverseJoinColumns = {@JoinColumn(name ="PARAMETER_ID")})
    private List<Parameter> consequences = new ArrayList<>();
    
    
    public Parameter() {}

    public Parameter(long id, String name, String label, String comment, ExpertSystem expertSystem,
            ParameterCategory category, ParameterType parameterType, OffsetDateTime creationDate, User creationUser,
            OffsetDateTime updateDate, User updateUser, int multivalueSize,List<Parameter> dependences) {
        super();
        this.id = id;
        this.name = name;
        this.label = label;
        this.comment = comment;
        this.expertSystem = expertSystem;
        this.category = category;
        this.type = parameterType;
        this.creationDate = creationDate;
        this.creationUser = creationUser;
        this.updateDate = updateDate;
        this.updateUser = updateUser;
        this.multivalueSize = multivalueSize;
        this.dependences = dependences;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getLabel() {
        return label;
    }

    public void setLabel(String label) {
        this.label = label;
    }

    public String getComment() {
        return comment;
    }

    public void setComment(String comment) {
        this.comment = comment;
    }

    public ExpertSystem getExpertSystem() {
        return expertSystem;
    }

    public void setExpertSystem(ExpertSystem expertSystem) {
        this.expertSystem = expertSystem;
    }

    public ParameterCategory getCategory() {
        return category;
    }

    public void setCategory(ParameterCategory category) {
        this.category = category;
    }

    public ParameterType getType() {
        return type;
    }

    public void setType(ParameterType parameterType) {
        this.type = parameterType;
    }

    public OffsetDateTime getCreationDate() {
        return creationDate;
    }
    
    public List<Parameter> getDependences() {
        return dependences;
    }

    public List<Parameter> getConsequences() {
        return consequences;
    }
    
    public void setCreationDate(OffsetDateTime creationDate) {
        this.creationDate = creationDate;
    }

    public User getCreationUser() {
        return creationUser;
    }

    public void setCreationUser(User creationUser) {
        this.creationUser = creationUser;
    }

    public OffsetDateTime getUpdateDate() {
        return updateDate;
    }

    public void setUpdateDate(OffsetDateTime updateDate) {
        this.updateDate = updateDate;
    }

    public User getUpdateUser() {
        return updateUser;
    }

    public void setUpdateUser(User updateUser) {
        this.updateUser = updateUser;
    }

    public int getMultivalueSize() {
        return multivalueSize;
    }

    public void setMultivalueSize(int multivalueSize) {
        this.multivalueSize = multivalueSize;
    }
    
    public void setDependences(List<Parameter> dependences) {
        this.dependences = dependences;
    }

    public void setConsequences(List<Parameter> consequences) {
        this.consequences = consequences;
    }
    
    public void addDependence(Parameter dependence) {
        if(dependence != null ) {
             removeConsequence(dependence);
             this.dependences.add(dependence);
             dependence.getConsequences().add(this);
        }
    }

    public void removeDependence(Parameter dependence) {
        if(dependence != null && this.dependences.contains(dependence) ) {
            final  int indexOfConsequence = this.dependences.indexOf(dependence);
            Parameter parameter = this.dependences.get(indexOfConsequence) ;
            parameter.getDependences().remove(this);   
         }
    }
    
    public void addConsequence(Parameter consequence) {
        if(consequence != null ) {
             removeConsequence(consequence);
             this.consequences.add(consequence);
             consequence.getDependences().add(this);
        }
    }

    public void removeConsequence(Parameter consequence) {
        if(consequence != null && this.consequences.contains(consequence) ) {
            final  int indexOfConsequence = this.consequences.indexOf(consequence);
            Parameter parameter = this.consequences.get(indexOfConsequence) ;
            parameter.getDependences().remove(this);   
         }
    }
    
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + (int) (id ^ (id >>> 32));
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Parameter other = (Parameter) obj;
        if (id != other.id)
            return false;
        return true;
    }

    @Override
    public String toString() {
        return "Parameter [id=" + id + ", name=" + name + ", label=" + label + ", comment=" + comment
                + ", expertSystem=" + expertSystem + ", category=" + category + ", type=" + type
                + ", creationDate=" + creationDate + ", creationUser=" + creationUser + ", updateDate=" + updateDate
                + ", updateUser=" + updateUser + ", multivalueSize=" + multivalueSize +  ", dependences=" + dependences + "]";
    }
}

我的仓库如下


import java.util.List;

import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import com.tuto.entity.Parameter;


/**
 * This interface defines the contract of the possible operations to manage the parameter entity in the database.
 *
 */
@Repository
public interface IParameterRepository extends JpaRepository<Parameter, Long> {

    /**
     * This method fetches the list of all available parameters. It could return an empty list.
     *
     * @return List<Parameter> The list of all Parameters found
     */
    @Transactional
    @Modifying(clearAutomatically = false)
    @Query(value = "SELECT param FROM Parameter param")
    List<Parameter> getAllParameters();

    /**
     * This method fetches the dependencies list (as Parameter list) for the given Param id.
     * It could return an empty list.
     *
     * @param idParam The id of Parameter.
     * @return List<Parameter> The dependencies list (as Parameter list) found for the given Param id.
     */
    @Transactional
    @Modifying(clearAutomatically = false)
    @EntityGraph(attributePaths = {"dependences","expertSystem"})
    @Query(value = "SELECT param.dependences FROM Parameter param join fetch param.dependences where param.id = :#{#idParam}  ", nativeQuery = false)
    List<Parameter> getDependancesByParamId(final long idParam);

    /**
     * This method fetches the consequences list (as Parameter list) for the given Param id.
     * It could return an empty list.
     *
     * @param idParam The id of Parameter.
     * @return List<Parameter> The dependencies list (as Parameter list) found for the given Param id.
     */
    @Transactional
    @Modifying(clearAutomatically = false)
    @EntityGraph(attributePaths = {"consequences","expertSystem"})
    @Query(value = "SELECT param.consequences FROM Parameter param join fetch param.consequences where param.id = :#{#idParam}  ", nativeQuery = false)
    List<Parameter> getConsequencesByParamId(final long idParam);

}

我的单元测试如下


import static org.junit.Assert.assertEquals;

import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.context.ActiveProfiles;

import com.tuto.common.enums.ParameterCategory;
import com.tuto.common.enums.ParameterType;
import com.tuto.entity.Parameter;
import com.tuto.core.profile.PfSpringProfiles;
import com.tuto.test.Tags;

@DataJpaTest(showSql = true)
@Tag(Tags.COMPONENT)
@ActiveProfiles(PfSpringProfiles.TEST)
public class ParameterRepositoryTest {

    @Autowired
    private TestEntityManager entityManager;
    
    @Autowired
    private IParameterRepository parameterRepository;
    
    @BeforeEach
    public void intTests() {
        
        final Parameter parameterInput = new Parameter();
        parameterInput.setCategory(ParameterCategory.INPUT);
        parameterInput.setType(ParameterType.DECIMAL);
        parameterInput.setId(1L);
        
        final Parameter parameterIntermediate = new Parameter();
        parameterIntermediate.setCategory(ParameterCategory.INTERMEDIATE);
        parameterIntermediate.setType(ParameterType.DECIMAL);
        parameterIntermediate.setId(2L);
        
        final Parameter parameterOutput = new Parameter();
        parameterOutput.setCategory(ParameterCategory.OUTPUT);
        parameterOutput.setType(ParameterType.DECIMAL);
        parameterOutput.setId(3L);

        entityManager.merge(parameterInput);
        entityManager.merge(parameterIntermediate);
        entityManager.merge(parameterOutput);

//        parameterInput.getConsequences().add(parameterIntermediate);
        parameterInput.addConsequence(parameterIntermediate);
        parameterIntermediate.addConsequence(parameterOutput); 
        
//        parameterIntermediate.getConsequences().add(parameterOutput);

//        parameterOutput.getDependences().add(parameterIntermediate);
//        parameterIntermediate.getDependences().add(parameterInput);
        
        entityManager.merge(parameterOutput);
        entityManager.merge(parameterIntermediate);
        entityManager.merge(parameterInput);
    }
    
    @Test
    public void checkConsequencesAndDependencesAreNotEmpty() {
        List<Parameter> parameter = parameterRepository.getDependancesByParamId(2L);
        assertEquals(parameter.size(),1);
        assertEquals(parameter.get(0).getId(),1L);
    }
}

我面临两个异常:第一个是著名的persist detached entity另一个是query specified join fetching,但是fetched association的owner是不在 select 列表中

全栈跟踪

Caused by: java.lang.IllegalArgumentException: org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list [FromElement{explicit,not a collection join,fetch join,fetch non-lazy properties,classAlias=null,role=com.tuto.entity.Parameter.consequences,tableName=parameters,tableAlias=parameter2_,origin=parameters parameter0_,columns={parameter0_.parameter_id,className=com.tuto.entity.Parameter}}] [SELECT param.consequences FROM com.tuto.entity.Parameter param join fetch param.consequences where param.id = :__$synthetic$__1  ]
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:138)
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:181)
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:188)
    at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:757)
    at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:114)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:362)
    at com.sun.proxy.$Proxy140.createQuery(Unknown Source)
    at org.springframework.data.jpa.repository.query.SimpleJpaQuery.validateQuery(SimpleJpaQuery.java:90)
    ... 88 common frames omitted
Caused by: org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list [FromElement{explicit,not a collection join,fetch join,fetch non-lazy properties,classAlias=null,role=com.tuto.entity.Parameter.consequences,tableName=parameters,tableAlias=parameter2_,origin=parameters parameter0_,columns={parameter0_.parameter_id,className=com.tuto.entity.Parameter}}] [SELECT param.consequences FROM com.tuto.entity.Parameter param join fetch param.consequences where param.id = :__$synthetic$__1  ]
    at org.hibernate.QueryException.generateQueryException(QueryException.java:120)
    at org.hibernate.QueryException.wrapWithQueryString(QueryException.java:103)
    at org.hibernate.hql.internal.ast.QueryTranslatorImpl.doCompile(QueryTranslatorImpl.java:220)
    at org.hibernate.hql.internal.ast.QueryTranslatorImpl.compile(QueryTranslatorImpl.java:144)
    at org.hibernate.engine.query.spi.HQLQueryPlan.<init>(HQLQueryPlan.java:113)
    at org.hibernate.engine.query.spi.HQLQueryPlan.<init>(HQLQueryPlan.java:73)
    at org.hibernate.engine.query.spi.QueryPlanCache.getHQLQueryPlan(QueryPlanCache.java:162)
    at org.hibernate.internal.AbstractSharedSessionContract.getQueryPlan(AbstractSharedSessionContract.java:636)
    at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:748)
    ... 96 common frames omitted
Caused by: org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list [FromElement{explicit,not a collection join,fetch join,fetch non-lazy properties,classAlias=null,role=com.tuto.entity.Parameter.consequences,tableName=parameters,tableAlias=parameter2_,origin=parameters parameter0_,columns={parameter0_.parameter_id,className=com.tuto.entity.Parameter}}]
    at org.hibernate.hql.internal.ast.tree.SelectClause.initializeExplicitSelectClause(SelectClause.java:215)
    at org.hibernate.hql.internal.ast.HqlSqlWalker.useSelectClause(HqlSqlWalker.java:1028)
    at org.hibernate.hql.internal.ast.HqlSqlWalker.processQuery(HqlSqlWalker.java:796)
    at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.query(HqlSqlBaseWalker.java:694)
    at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.selectStatement(HqlSqlBaseWalker.java:330)
    at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.statement(HqlSqlBaseWalker.java:278)
    at org.hibernate.hql.internal.ast.QueryTranslatorImpl.analyze(QueryTranslatorImpl.java:276)
    at org.hibernate.hql.internal.ast.QueryTranslatorImpl.doCompile(QueryTranslatorImpl.java:192)
    ... 102 common frames omitted

我确实解决了这个问题,但我仍然面临如下一些审讯:

修复

 @Transactional
    @Modifying(clearAutomatically = false)
//    @EntityGraph(attributePaths = {"dependences","expertSystem"})
    @Query(value = "SELECT param.dependences FROM Parameter param  where param.id = :#{#idParam}  ", nativeQuery = false)
    List<Parameter> getDependancesByParamId(final long idParam);

    /**
     * This method fetches the consequences list (as Parameter list) for the given Param id.
     * It could return an empty list.
     *
     * @param idParam The id of Parameter.
     * @return List<Parameter> The dependances list (as Parameter list) found for the given Param id .
     */
    @Transactional
    @Modifying(clearAutomatically = false)
//    @EntityGraph(attributePaths = {"consequences","expertSystem"})
    @Query(value = "SELECT param.consequences FROM Parameter param where param.id = :#{#idParam}  ", nativeQuery = false)
    List<Parameter> getConsequencesByParamId(final long idParam);

我相信也许选择 param.consequences 的实体元素将帮助我避免获取此集合,但是每个 ExpertSystem 实体元素呢,这是使用延迟加载机制,我不应该能够从集合中获取加入它。 实体图不应该通过自动获取集合或子元素来帮助我避免这个问题!!!

你可以根据这个重现这个实验link