2

我碰巧在 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)   

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

4

2 回答 2

2

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

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

所以这可能会发生:

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

最后,我认为您的实体应该具有equals/hashCode仅基于 ID 的语义。要使用 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.

于 2018-11-29T17:16:32.123 回答
1

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

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

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

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

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

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

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

有多种可能的方法。

  1. 基于不包括 Id 的字段子集equalshashCode确保在AddressSet. 即使不是,您基本上也必须将其视为不可变的类。从 DDD 的角度来看,这将实体视为一个值类。
  2. 基于IdequalshashCode在构造函数中设置 Id。从领域的角度来看,这将类视为由其 ID 标识的适当实体。
于 2018-11-30T13:07:55.423 回答