Hibernate - @ElementCollection - 删除/插入行为异常

50
@Entity
public class Person {

    @ElementCollection
    @CollectionTable(name = "PERSON_LOCATIONS", joinColumns = @JoinColumn(name = "PERSON_ID"))
    private List<Location> locations;

    [...]

}

@Embeddable
public class Location {

    [...]

}

给定以下类结构,当我尝试将一个新位置添加到Person的位置列表中时,它总是导致以下SQL查询:

DELETE FROM PERSON_LOCATIONS WHERE PERSON_ID = :idOfPerson

并且

A lotsa' inserts into the PERSON_LOCATIONS table

Hibernate(3.5.x / JPA 2)会删除给定 Person 的所有关联记录,并重新插入所有先前的记录以及新记录。

我认为 Location 上的 equals/hashcode 方法可以解决这个问题,但它并没有改变任何东西。

欢迎提供任何提示!


1
我遇到了相反的问题 - 当我更新父对象时,Hibernate没有删除任何内容。这是我在网络上找到的唯一相关帖子,包括JBoss的Hibernate文档。事实证明,由于某种原因,我将父对象上的集合变量声明为final,而Hibernate默默地什么都没做。我去掉了final关键字,它开始正常更新和删除了。 - Adam
我发现在简单的列表操作中(不是更新或删除),使用fetch = FetchType.EAGER也会出现这种情况。@OrderColumn似乎没有任何效果。 - lightswitch05
4个回答

73

这个问题在JPA wikibook的ElementCollection页面中有所解释:

CollectionTable中的主键

JPA 2.0规范没有提供一种定义EmbeddableId的方法。但是,要删除或更新ElementCollection映射中的元素,通常需要某个唯一键。否则,在每次更新时,JPA提供程序都需要删除EntityCollectionTable中的所有内容,然后重新插入值。因此,JPA提供程序很可能会假定Embeddable中所有字段的组合与外键(JoinColunm(s))结合使用是唯一的。然而,如果Embeddable很大或复杂,则可能效率低下或根本不可行。

这正是发生在这里的情况(加粗部分):Hibernate不为集合表生成主键,并且无法检测到集合中的哪个元素已更改,将删除旧内容并插入新内容。

然而,如果你定义一个@OrderColumn(用于指定用于维护列表持久顺序的列 - 这是有道理的,因为你正在使用List),Hibernate将创建一个主键(由顺序列和连接列组成),并且能够更新集合表而无需删除整个内容。
类似于这样(如果要使用默认列名):
@Entity
public class Person {
    ...
    @ElementCollection
    @CollectionTable(name = "PERSON_LOCATIONS", joinColumns = @JoinColumn(name = "PERSON_ID"))
    @OrderColumn
    private List<Location> locations;
    ...
}

参考文献


有什么想法可以使用Hibernate XML映射创建这样的主键吗? - Wojciech Owczarczyk
2
我发现了相同的“删除全部”,“插入行1”,“插入行2”等SQL行为。我正在使用OpenJPA2.2.2,并且在可嵌入的子实体中插入@Id注解并没有起到帮助作用。OrderColumn需要一个新的整数排序列,而我的数据库架构不是由这个JPA应用程序维护的,所以可能不是一个选项。 - Whome
这个方法也适用于Map吗?使用MapKey(Join)Column等。JPA总是为我执行删除操作。 - phil294
1
有人知道这个解决方案是否适用于基本集合吗?(例如,如果字段是List <String>而不是List <Location>,它是否有效)还是只适用于可嵌入的对象? - molinab297
@molinab297 是的,它适用于基本集合。 上述解决方案的问题在于,如果您删除列表的第一个元素,则所有元素的索引都将更新(向左移动一个位置)。 - Uri Loya
大家好,我知道这个问题很旧了,但@molinab297的评论对我有帮助。在我的情况下,List<String> 对于“findAll”请求是正确的,但对于“findOne”来说,项目被复制了。(数据库没有重复行)通过将List<String>更改为List<CustomObject>(其中CustomObject是可嵌入的),问题得到了解决。 - Kinga l

10

除了Pascal的回答外,您还必须将至少一列设置为NOT NULL:

@Embeddable
public class Location {

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

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

    public Location() {
    }

    public Location(String path, String parent) {
        this.path = path;
        this.parent= parent;
    }

    public String getPath() {
        return path;
    }

    public String getParent() {
        return parent;
    }
}

这个要求在AbstractPersistentCollection文档中有详细说明:

针对类似HHH-7072的情况的解决方法。如果集合元素是完全由可空属性组成的组件,则我们目前必须强制重新创建整个集合。请参见AbstractCollectionPersister构造函数中hasNotNullableColumns的使用以了解更多信息。为了逐行删除,需要的SQL语句为“WHERE(COL =?或(COL为空且?为空))”,而不是当前的“WHERE COL =?”(大多数数据库对空值失败)。请注意,该参数必须绑定两次。在ORM 5+中,直到我们最终添加“参数绑定点”概念处理此类型的条件要么极其困难要么根本不可能。强制重新创建并不理想,但在ORM 4中没有其他选择。


1
天啊,谢谢你!我花了好几个小时在使用@ElementCollection@OrderColumn@Embeddable@UniqueConstraints时遇到了困难。当我删除或更改java.util.List的顺序时,由于Hibernate试图进行更新而不是删除整个集合,导致唯一约束条件被违反。将标记为@Embeddable的类的字段更改为可为空就解决了问题! - Jagger
1
在最新的Hibernate(5.2.3)中,似乎列是否可为空已经不再相关。更新似乎每次都会执行,这实际上对我的应用程序来说是不幸的,因为我需要删除所有集合元素,然后将它们替换为一个@UniqueConstraints,当执行更新时违反了此约束。考虑使用带有@OrderColumn@OneToMany并将该唯一约束定义为主键... 嗯..., 如果可能的话, 我们来看看。 - Jagger
如果您认为这是一个回归问题,您可以添加一个 Jira 问题。 - Vlad Mihalcea

3
我们发现我们定义为ElementCollection类型的实体没有定义equals或hashcode方法,并且具有可空字段。我们提供了这些方法(通过@lombok)在实体类型上,它允许Hibernate(v 5.2.14)识别集合是否已更改。
另外,这个错误在我们的服务方法中出现,该方法标记有注释@Transaction(readonly=true)。由于Hibernate会尝试清除相关的元素集合并重新插入,所以当刷新时事务会失败,并且会出现这个非常难以跟踪的消息:
HHH000346:托管刷新期间出错[批量更新返回意外行计数 [0]; 实际行计数:0; 预期:1]
下面是我们出现错误的实体模型示例。
@Entity
public class Entity1 {
@ElementCollection @Default private Set<Entity2> relatedEntity2s = Sets.newHashSet();
}

public class Entity2 {
  private UUID someUUID;
}

将其更改为:

将它更改为这样

@Entity
public class Entity1 {
@ElementCollection @Default private Set<Entity2> relatedEntity2s = Sets.newHashSet();
}

@EqualsAndHashCode
public class Entity2 {
  @Column(nullable = false)
  private UUID someUUID;
}

我们解决了问题。祝你好运。


2
我也遇到了同样的问题,但是想要映射一个枚举列表:List<EnumType>
我通过以下方式解决了这个问题:
@ElementCollection
@CollectionTable(
        name = "enum_table",
        joinColumns = @JoinColumn(name = "some_id")
)
@OrderColumn
@Enumerated(EnumType.STRING)
private List<EnumType> enumTypeList = new ArrayList<>();

public void setEnumList(List<EnumType> newEnumList) {
    this.enumTypeList.clear();
    this.enumTypeList.addAll(newEnumList);
}

我的问题在于使用默认的setter方法替换了List对象,因此Hibernate将其视为完全“新”的对象,尽管枚举值并未更改。"最初的回答"

我确认。这个有效。非常感谢。 - MicD

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接