没有自然键时如何实现equals()和hashCode()方法?

3
这个问题基本上是对以下问题的跟进:Should I write equals() methods in JPA entities?What is the best practice when implementing equals() for entities with generated ids 首先一些背景信息...
您可以经常遇到以下主键组合:
1. 自然键(业务键):通常是实体的一组真实、多列属性。
2. 人工键(替代键):无意义,通常是自动增量(IDENTITY、AUTO_INCREMENT、AUTOINCREMENT、SEQUENCE、SERIAL等) ID。
3. 混合键(半自然/半人工键):通常由人工ID和一些额外的自然列组成,例如任何引用使用ID并扩展该键(entity_id、ordinal_nbr或类似)的另一个表的表。
频繁的场景:对根、分支或叶继承表的多对一引用,这些表通过识别关系/依赖键共享一个共同的“愚蠢”ID。 当另一个表需要引用所有实体类型时,根(和分支)表通常是有意义的,例如PostAddresses -> Contacts,其中Contacts具有子表Persons、Clubs和Facilities,它们除了可联系之外没有任何共同点。
现在来看JPA:
在Java中,我们可以创建其PK可能不完整(null或部分null)的新实体对象,这是一个DBMS最终会阻止我们将其插入到DB中的实体(行)。
但是,在与应用程序代码一起工作时,通常很方便拥有新的(或分离的)实体,即使新实体对象还没有PK值,也可以将其与现有(托管)实体进行比较。为了针对具有自然键列的任何实体实现这一点,请使用它们来实现equals()和hashCode()(如其他两个SO帖子所建议的)。
问题:
但是,当无法确定自然/业务键时,例如Contacts表的情况,该表基本上只是一个ID(加上鉴别器)时,您该怎么办?什么是基于哪些列进行equals()和hashCode()实现的良好列选择策略?(上述2和3的人工密钥)
显然选择不多...
一个(天真的)目标是实现相同的“瞬态可比性”。能做到吗?如果不能,人工ID equals()和hashCode()实现的一般方法是什么?
注意:我已经在使用Apache EqualsBuilder和HashCodeBuilder...我有意地“天真化”了我的问题。
3个回答

3

我认为这个主题比讨论的要简单。

如果存在数据库 id,则使用该 id,否则使用 Object#equals / 对象标识。

为什么?如果您将新实体放入数据库中,JPA 只会将新生成的 id 从数据库映射到实体对象的标识。另一方面,这意味着对象标识事先也是主键。

讨论的重点似乎经常是假设具有相同属性的两个业务对象相等。但它们并不相等。例如,具有相同街道和城市的两个地址仅在您不想拥有地址值的重复项时才相等。但是,然后您也将它们作为数据库中的主键,这导致您始终获得业务对象的主键。如果允许业务对象有重复地址,则对象标识是主键,因为它是区分两个地址之间的唯一标识。

持久化实体后,数据库 id 不再完全起作用,因为现在可以拥有相同实体的克隆版本,这些版本仅共享相同的数据库 id。(但现在可能有多个内存位置/对象标识)


1

其中一种常见的技术是使用UUID作为标识符,但这种方法有一些缺点。

它们会导致URL变得丑陋,并且据说基于如此长的标识符查询实体会影响性能。 长UUID还会导致数据库索引变得过大。

UUID的优点在于您不必为每个实体实现单独的hashCode() equals()方法。

我决定在自己的项目中使用传统的分配标识符并在内部使用UUID来进行hashCode() equals()方法。 它看起来像这样:

@Configurable
@MappedSuperclass
@EntityListeners({ModelListener.class})
@SuppressWarnings("serial")
public abstract class ModelBase implements Serializable {

     //~~ Instance Fields =====================================

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name = "id", nullable = false, updatable=false, unique=true)
     protected Long id;

    @Column(name="__UUID__", unique=true, nullable=false, updatable=false, length = 36)
    private String uuid = java.util.UUID.randomUUID().toString();

    //~ Business Methods =====================================

    @Override
    public String toString() {
        return new ToStringCreator(this)
            .append("id", getId())
            .append("uuid", uuid())
            .append("version", getVersion())
             .toString(); 
    }

    @Override
    public int hashCode() {
        return uuid().hashCode();
    }

    @Override
    public boolean equals(Object o) {
        return (o == this || (o instanceof ModelBase && uuid().equals(((ModelBase)o).uuid())));
     }

    /**
     * Returns this objects UUID.
     * 
     * @return - This object's UUID.
     */
    public String uuid() {
        return uuid;
    }

    //~ Accessor Methods ======================================

    public Long getId() {
        return id;
    }

    @SuppressWarnings("unused")
    private void setId(Long id) {
        this.id = id;
    }

     @SuppressWarnings("unused")
    private String getUuid() {
        return uuid;
    }

    @SuppressWarnings("unused")
    private void setUuid(String uuid) {
        this.uuid = uuid;
     }
}

只需为所有实体扩展ModelBase。这种技术的优点是uuid在对象创建时立即分配。但是我们仍然有一个分配的id可用于应用程序代码中查询特定对象。基本上,除了比较目的外,我们的应用程序代码从不使用或甚至考虑uuid字段。效果非常好。

1
我认为问题在于“通常有新的(或分离的)实体可以与现有的(托管的)实体进行比较”。那么,您将如何将这些新的UUID与现有对象进行比较? - Alex Gitelman
我明白你的观点,但是实际操作中我没有遇到任何此类情况。我基于原始问题提供了答案:“但是当无法确定自然/业务键时,比如联系人表的情况,它基本上只有一个ID(加上鉴别器),那么在基于equals()和hashCode()实现时,什么样的列选择策略才是好的呢?(上述第2和第3种人造键)。显然没有太多选择……”我的解决方案解决了这个问题。 - Nobody
我也没有看到好的使用案例,这就是为什么我希望它能够被澄清的原因。 - Alex Gitelman
我不想让所有实体都扩展另一个类,也因为并非所有实体都使用笨重的 (UU)IDs。大多数 PK 实际上是自然的。我只是不能帮助引入继承关系的 ID。这会导致混合类型键,就像 3. “混合键” 中描述的那样。 - Kawu
1
我认为你可以更好地将uuid标记为瞬态。如果你不想让实体继承一个基类,你可以使用AspectJ ITD来添加任何想要的行为。 - gpilotino
@gpilotino:这是个好主意,我之前没有想到过将UUID设置为短暂的。如果数据库生成的ID存在,则使用该ID。否则,使用短暂的UUID。这样就不必存储UUID了。我想我要修改我的模型对象了。=) - Nobody

1
如果您无法在对象上找到一组属性,以将其与同类其他对象区分开来,则无法比较这些对象,对吧?如果您提供了详细的用例,可能会有更多内容,但是在具有ID和鉴别器的联系情况下,在没有ID的情况下,您只能比较具有相同鉴别器的对象组。如果保证组仅具有一个元素,则鉴别器是您的关键。

请注意,鉴别器列无法使用,因为它不能进行标识。它被设置为引用超级实体的子实体类型。每个子类型Persons、Clubs和Facilities都可以有许多实体共享相同的值。对我来说,答案似乎只能使用联系人ID。也许子实体提供了一些东西,我还没有检查过。 - Kawu
还要注意,Contacts.id 可用的。除了将此ID传递给Apache构建程序之外,真的没有更多的了吗? - Kawu

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