Felix Stanley
Felix Stanley

Reputation: 73

How to persist a JPA entity which has two level of oneToMany relationship?

I am having problem with persisting entities in Spring JPA/hibernate environment. I am using Lombok Plugin too to reduce boilerplate codes. Basically I have an entity called Product which has oneToMany relationship with SubProduct :

    @Entity
    @Table(name="product")
    @EqualsAndHashCode(exclude = {"productId"}, callSuper=false)
    @ToString(includeFieldNames=true, callSuper=true)
    @Data
    public class Product implements Serializable {

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


       @OneToMany(fetch = FetchType.EAGER, mappedBy="product"
           , cascade ={CascadeType.ALL}, orphanRemoval=true)
       private Set<SubProduct> subProducts;
   }

and this is my SubProduct which has another oneToMany relationship with joinTable called ProductVariantMap:

@Entity
@Table(name="sub_product")
@EqualsAndHashCode(exclude = {"subId","product"})
@ToString(includeFieldNames=true, exclude = {"product"})
@Data
public class SubProduct implements Serializable {

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

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "product_id" )
  private Product product;

  @OneToMany(fetch = FetchType.EAGER, 
         mappedBy="productVariantMapId.subProduct"
         , cascade =   {CascadeType.ALL})
  private List<ProductVariantMap> productVariantMaps;

}

This is my ProductVariantMap with an embedded id that contains productId, subId, and variantId

@Entity
@Table(name="product_variant_map")
@EqualsAndHashCode
@ToString(includeFieldNames=true)
@Data
public class ProductVariantMap implements Serializable {

  @EmbeddedId
  private ProductVariantMapId productVariantMapId;

  @Column(name="variant_value", nullable=false)
  private String variantValue;
}

productVariantMapId:

@Embeddable
@EqualsAndHashCode(exclude = {"subProduct","product"})
@ToString(includeFieldNames=true,exclude = {"subProduct","product"})
@Data
public class ProductVariantMapId implements Serializable {

  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = "sub_id" )
  private SubProduct subProduct;

  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = "product_id" )
  private Product product;

  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = "variant_id" )
  private Variant variant;


}

and last but not least is my variant:

@Entity
@Table(name="variant")
@EqualsAndHashCode(exclude = {"variantId"})
@ToString(includeFieldNames=true)
@Data
public class Variant implements Serializable {

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

    @Column(name="name", nullable=false)
    private String name;
}

My aim is to be able to persist the whole relationship in one go when I do productRepository.saveAndFlush(product). Please note that all primary keys are generated by database sequence generator and therefore its null before the saveAndFlush. I got below exception when I attempted to do the above:

java.lang.NullPointerException
    at org.hibernate.type.descriptor.java.AbstractTypeDescriptor.extractHashCode(AbstractTypeDescriptor.java:65)
    at org.hibernate.type.AbstractStandardBasicType.getHashCode(AbstractStandardBasicType.java:201)
    at org.hibernate.type.AbstractStandardBasicType.getHashCode(AbstractStandardBasicType.java:206)
    at org.hibernate.type.EntityType.getHashCode(EntityType.java:355)
    at org.hibernate.type.ComponentType.getHashCode(ComponentType.java:241)
    at org.hibernate.engine.spi.EntityKey.generateHashCode(EntityKey.java:61)
    at org.hibernate.engine.spi.EntityKey.<init>(EntityKey.java:54)
    at org.hibernate.internal.AbstractSharedSessionContract.generateEntityKey(AbstractSharedSessionContract.java:459)
    at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:162)
    at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:125)
    at org.hibernate.jpa.event.internal.core.JpaPersistEventListener.saveWithGeneratedId(JpaPersistEventListener.java:67)
    at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:189)
    at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:132)
    at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:788)
    at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:753)
    at org.hibernate.jpa.event.internal.core.JpaPersistEventListener$1.cascade(JpaPersistEventListener.java:80)
    at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:391)
    at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:316)

I traced the execution and It seemed that the exception was caused by Variant class having null HashCode which should not have been the case since I populated variant with different name for hibernate to distinguish (although the Id is not known yet) for each SubProduct (I was trying to save 2 subProducts under one product). Each subProduct will then have two productVariantMap.

I would expect to be left with 1 product row, 2 subproducts, 4 product variant map rows , and 2 variant rows. Is it possible to persist the whole relationship in one go in JPA? Thanks so much

Upvotes: 1

Views: 2553

Answers (1)

Peter Š&#225;ly
Peter Š&#225;ly

Reputation: 2933

In JPA hash and equal method have to be based on primary key. Rather not use framework to generate hash() equals() and implement it.

public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof Person))
    return false;

Person other = (Person)o;

if (id == other.getId()) return true;
if (id == null) return false;

// equivalence by id
return id.equals(other.getId());
}

public int hashCode() {
    if (id != null) {
        return id.hashCode();
    } else {
        return super.hashCode();
    }
}

Other remark: @Embedable class is a part of entity in the same DB table. No association between embedable and its parent needed. Remove @ManyToOne

Upvotes: 0

Related Questions