【问题标题】:Strange one-to-many behaviour with Spring Data JDBC使用 Spring Data JDBC 的奇怪的一对多行为
【发布时间】:2018-11-29 16:33:57
【问题描述】:

我碰巧在 Spring Data JDBC(使用带有必要启动器的 Spring Boot 2.1)聚合处理中遇到了一些非常奇怪的东西。让我解释一下这个案例(我使用的是 Lombok,不过这个问题可能是相关的)......

这是我的实体的摘录:

import java.util.Set;
@Data
public class Person {
    @Id
    private Long id;
    ...
    private Set<Address> address;
}

这是一个关联的 Spring Data 存储库:

public interface PersonsRepository extends CrudRepository<Person, Long> {
}

这是一个失败的测试:

@Autowired
private PersonsRepository personDao;
...
Person person = personDao.findById(1L).get();
Assert.assertTrue(person.getAddress().isEmpty());
person.getAddress().add(myAddress); // builder made, whatever
person = personDao.save(person);
Assert.assertEquals(1, person.getAddress().size()); // count is... 2!

事实是,通过调试,我发现地址集合(它是一个集合)包含两个附加地址的同一实例的引用。 我没有看到两个引用是如何结束的,最重要的是,一个 SET(实际上是一个 LinkedHashSet,为了记录)如何处理同一个实例 TWICE!

person  Person  (id=218)    
    address LinkedHashSet<E>  (id=228)  
        [0] Address  (id=206)   
        [1] Address  (id=206)   

有人知道这种情况吗?谢谢

【问题讨论】:

    标签: java spring-boot hsqldb lombok spring-data-jdbc


    【解决方案1】:

    当此实例同时发生变异时,(Linked)HashSet 可以(作为副作用)存储同一实例两次(引用自 Set):

    注意:如果将可变对象用作集合元素,则必须非常小心。如果对象的值以影响equals 比较的方式更改,而对象是集合中的一个元素,则不指定集合的​​行为。

    所以这就是可能发生的事情:

    1. 您创建了一个 Address 的新实例,但未设置其 ID (id=null)。
    2. 您将其添加到Set,其哈希码计算为某个值A
    3. 您调用 PersonsRepository.save 很可能会保留 Address 并在其上设置一些非空 ID。
    4. PersonsRepository.save 可能还调用HashSet.add 以确保地址 在集合中。但由于 ID 已更改,哈希码现在计算为某个值 B
    5. 哈希码AB 映射到HashSet 中的不同桶,因此Address.equals 方法甚至不会在HashSet.add 期间被调用。因此,您最终会在两个不同的存储桶中获得相同的实例。

    最后,我认为您的实体应该仅基于 ID 具有 equals/hashCode 语义。要使用 Lombok 实现它,您可以使用 @EqualsAndHashCode,如下所示:

    @Data
    @EqualsAndHashCode(of = "id")
    public class Person {
        @Id
        private Long id;
        ...
    }
    
    @Data
    @EqualsAndHashCode(of = "id")
    public class Address {
        @Id
        private Long id;
        ...
    }
    

    不过,这并不能解决您遇到的问题,因为更改的是 ID,因此哈希码仍然会有所不同。

    处理此问题的一种方法是在将Address 添加到Set 之前将其持久化。

    【讨论】:

    • 感谢您的见解。因为我只是在做实验,所以我不会选择“生产解决方案”:-) 目前,我将尝试通过调用 #findById 来覆盖 #save 输出以查看其行为方式。我会在线程中添加信息!
    • 实际上,我选择了与您相反的解决方案:@EqualsAndHashCode(exclude = "id") 成功了;但我不确定从长远来看这是一个好主意
    • @ThomasEscolan 当然@EqualsAndHashCode(exclude = "id") 可以解决问题。但这会产生深远的影响,因为你的类不再是一个实体——它变成了一个价值对象。您可以阅读更多相关信息,例如here.
    • 是的,Tomasz,这绝对是真的。此外,这是一个简单的案例,只有 ID 是数据库生成的;使用触发器和其他默认值会变得更糟!
    • 事情是,与 Spring Data JPA 不同,Spring Data JDBC 的保存操作返回传入的相同实例(内存指针)。因此,您必须预先保存每个实体(您的建议)或在保存后系统地重新加载根实体。
    【解决方案2】:

    Tomasz Linkowski 的解释非常准确。但我会主张以不同的方式解决问题。

    内部发生的情况如下:Person 实体被保存。如果Person 是不可变的,这可能会或可能不会创建一个新的Person 实例。

    然后Address 被保存,从而得到一个新的id,它改变了它的哈希码。然后Address 被添加到Person,因为它可能又是一个新的Address 实例。

    但它现在是同一个实例,但哈希码已更改,这导致单个集合包含相同的 Address 两次。

    你需要做的是解决这个问题:

    定义equalshashCode,以便在保存实例时两者都稳定

    即当实例被保存时,hashCode 不能改变,或者你的应用程序中的任何其他操作都不能改变。

    有多种可能的方法。

    1. equalshashCode 建立在不包括Id 的字段子集上。确保在将Address 添加到Set 后不要编辑这些字段。即使不是,您基本上也必须将其视为不可变的类。从 DDD 的角度来看,这将实体视为一个值类。
    2. equalshashCode 为基础,并在构造函数中设置 Id。从领域的角度来看,这将类视为由其 ID 标识的适当实体。

    【讨论】:

    • 好的,谢谢。你知道我们是否必须使用 org.springframework.data.annotation.Immutable 注解,以便 Spring Data 提供新的实例?那么如何区分不可变值对象和可变实体呢?
    • 只要让它不可变,即没有设置器而不是“withers”或带所有参数的构造器。但在这种情况下,重要的部分是哈希码不会改变。因此,如果您将其设置为不可变但哈希码发生更改,您将再次在集合中拥有两个条目。
    • 嗨 Jens,当我尝试声明我的实体不可变时(使用 Lombok @Value 而不是 @Data),我的不同测试(保存-插入或更新-、查找全部、按 ID 查找)失败了有各种异常(不支持的操作,违反约束)。
    • 注意:@Immutable 让事情变得(有点)好。仍在调查:-)
    • 我创建了一个问题来调查我们是否可以改善行为jira.spring.io/browse/DATAJDBC-300
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-05-07
    • 1970-01-01
    • 2019-10-04
    • 2016-04-14
    • 2014-08-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多