如何持久化具有两级 oneToMany 关系的 JPA 实体?

Posted

技术标签:

【中文标题】如何持久化具有两级 oneToMany 关系的 JPA 实体?【英文标题】:How to persist a JPA entity which has two level of oneToMany relationship? 【发布时间】:2016-11-27 10:48:27 【问题描述】:

我在 Spring JPA/hibernate 环境中存在持久实体问题。我也在使用 Lombok Plugin 来减少样板代码。 基本上我有一个名为 Product 的实体,它与 SubProduct 有 oneToMany 关系:

    @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;
   

这是我的子产品,它与名为 ProductVariantMap 的 joinTable 有另一个 oneToMany 关系:

@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;


这是我的 ProductVariantMap,其嵌入的 id 包含 productId、subId 和 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;



最后但并非最不重要的是我的变体:

@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;

我的目标是在我执行 productRepository.saveAndFlush(product) 时能够一次性维持整个关系。请注意,所有主键都是由数据库序列生成器生成的,因此它在 saveAndFlush 之前为空。当我尝试执行上述操作时出现以下异常:

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)

我跟踪了执行情况,似乎异常是由具有 null HashCode 的 Variant 类引起的,这不应该是这种情况,因为我为每个变量填充了不同的名称以供休眠区分(尽管 Id 尚不清楚)子产品(我试图在一个产品下保存 2 个子产品)。然后每个子产品将有两个 productVariantMap。

我希望剩下 1 个产品行、2 个子产品、4 个产品变体映射行和 2 个变体行。 是否有可能在 JPA 中一次性维持整个关系? 非常感谢

【问题讨论】:

【参考方案1】:

在 JPA 中,hash 和 equal 方法必须基于主键。而不是使用框架来生成 hash() equals() 并实现它。

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();
    

其他说明: @Embedable 类是同一个数据库表中实体的一部分。 embedable 与其父级之间不需要关联。删除@ManyToOne

【讨论】:

之所以我的 hash 和 equal 方法是基于 ID 之外的其他字段,是因为我依赖于数据库 ID 生成器。如果我使用 ID 作为我的唯一标识符,Java 会将一组中的新实体视为一个实体,因为它们都有空 ID,请参阅此 onjava.com/pub/a/onjava/2006/09/13/… 在equals方法中看到这一行: if (id == null) return false;没有 ID 的新对象将永远不会相同。 如果我在 Embeddable 类中删除 manyToOne,hibernate 怎么知道一个特定的 productVariantMap 有哪个 sub_id 或 product_id?请记住,在 saveAndFlush 之前所有实体 Id 都是 null 这是 hibernate 文档所说的:“永远不要使用数据库标识符来实现相等性;使用业务键,一个唯一的,通常是不可变的属性的组合。如果一个瞬态对象是数据库标识符将改变持久化。如果瞬态实例(通常与分离的实例一起)保存在一个集合中,更改哈希码会破坏集合的合同。业务键的属性不必像数据库主键一样稳定,你只有只要对象在同一个 Set 中,就可以保证稳定性。” 是的。如果 Id 为 null,则唯一标识符为 super.hashCode()。两个没有 Id 的对象有不同的 hashCodes,然后把它们放入 Set 没有问题。

以上是关于如何持久化具有两级 oneToMany 关系的 JPA 实体?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Hibernate Criteria 连接两个具有 OneToMany 关系的表

如何验证两个实体之间的 OneToMany 关系

如何在 @OneToMany 关系映射的列上使用 JPA findBy 查询?

具有 OneToMany 关系的休眠重复条目

Doctrine2 - 无法删除具有单向 oneToMany 关系的实体

Hibernate:映射@OneToMany 关系的最后一行