Search code examples
javahibernatejpaone-to-many

JPA Two-step OneToMany Hibernate 6


In our team we decided to upgrade from Hibernate 5 to Hibernate 6. We have hundreds of JPA entities in our project. Some of them aren't working any more due to a new validation in Hibernate. I will explain it in detail with just a minimum of attributes:

Table Adapter:

  • column Integer ADID (PK) -> adapterId
  • column String name

Table AdapterParam:

  • column Integer ADID (PK, FK to Adapter.ADID)
  • column String PAN (PK) -> parameterName
  • ...

Table AdapterParamSelectionList:

  • column Integer ADID (PK, FK to AdapterParam.ADID)
  • column String PAN (PK, FK to AdapterParam.PAN)
  • column String code (PK)

JPA entity of Adapter:

@Entity
public class Adapter implements Serializable {

    @Id
    @Column(name = "ADID")
    private Integer adapterId;

    @OneToMany(mappedBy = "adapter", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<AdapterParam> adapterParams = new HashSet<>();


        // Getter and Setter...
}

JPA entity of AdapterParam:

@Entity
@IdClass(AdapterParamId.class)
public class AdapterParam implements Serializable {

    public static class AdapterParamId implements Serializable {
        
        private static final long serialVersionUID = 1L;
        
        private Adapter adapter;
        private String parameterName;
        @Override
        public int hashCode() {
            return Objects.hash(adapter, parameterName);
        }
        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            AdapterParamId other = (AdapterParamId) obj;
            return Objects.equals(adapter, other.adapter) && Objects.equals(parameterName, other.parameterName);
        }
    }

    @Id
    @Column(name = "PAN")
    private String parameterName;

    @Id
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ADID", referencedColumnName = "ADID")
    private Adapter adapter;

    @OneToMany(mappedBy = "adapterParam", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<AdapterParamSelectionList> adapterParamSelectList = new HashSet<>();


        // Getter and Setter...
}

JPA entity of AdapterParamSelectionList:

@Entity
@IdClass(AdapterParamSelectionListId.class)
public class AdapterParamSelectionList implements Serializable {

    public static class AdapterParamSelectionListId implements Serializable {
        
        private static final long serialVersionUID = 1L;
        
        private AdapterParam adapterParam;
        private String code;
        @Override
        public int hashCode() {
            return Objects.hash(adapterParam, code);
        }
        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            AdapterParamSelectionList other = (AdapterParamSelectionList) obj;
            return Objects.equals(adapterParam, other.adapterParam)
                    && Objects.equals(code, other.code);
        }
    }

    @Id
    @Column(name = "VAL")
    private String code;

    @Id
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ADID", referencedColumnName = "ADID") // this turns into error
    @JoinColumn(name = "PAN", referencedColumnName = "PAN")
    private AdapterParam adapterParam;

        // Getter and Setter...
}

The exception of Hibernate is the following. It occurs while starting our application:

org.hibernate.AnnotationException: Referenced column 'ADID' in '@JoinColumn' for 'de.example.AdapterParamSelectionList.id.adapterParam' is not mapped by any property of the target entity
    at org.hibernate.boot.model.internal.BinderHelper.findPropertiesByColumns(BinderHelper.java:501)
    at org.hibernate.boot.model.internal.BinderHelper.createSyntheticPropertyReference(BinderHelper.java:160)
    at org.hibernate.boot.model.internal.ToOneFkSecondPass.doSecondPass(ToOneFkSecondPass.java:114)
    at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processFkSecondPassesInOrder(InFlightMetadataCollectorImpl.java:1850)
    at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processSecondPasses(InFlightMetadataCollectorImpl.java:1764)
    at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:334)
    at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.build(MetadataBuildingProcess.java:129)
    at org.hibernate.boot.internal.MetadataBuilderImpl.build(MetadataBuilderImpl.java:449)
    at org.hibernate.boot.internal.MetadataBuilderImpl.build(MetadataBuilderImpl.java:101)
    at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:966)
    at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1016)

In Hibernate 5.6 the relationship from AdapterParamSelectionList to AdapterParamBO worked as expected. I don't know what changed. What is the best way to define this?

Just to clarify: we are using IdClass instead of EmbeddedId because we are mapping the entites to DTOs.


Solution

  • Starting from Hibernate 5.3, composite identifiers using @IdClass must directly map the columns. In your case, this does not happen because you use a ManyToOne relationship.

    One solution would be to switch from @IdClass to @EmbeddedId and @Embeddable. This is the recommended option and looks like it fits your needs perfectly.

    The AdapterParamId will change from @IdClass to @Embeddable:

    @Embeddable
    public class AdapterParamId implements Serializable {
    
        @Column(name = "ADID")
        private Integer adapterId;
        
        @Column(name = "PAN")
        private String parameterName;
    
        // equals() and hashCode()
    
    }