有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

没有自然密钥时equals()和hashCode()的java实现?

这个问题基本上是问题的后续:

Should I write equals() methods in JPA entities?What is the best practice when implementing equals() for entities with generated ids

先了解一下背景

您经常会遇到以下主键星座:

  1. 自然键(业务键):通常是实体的一组真实的多列属性
  2. 人工键(代理键):无意义的,通常是自动递增的(标识、自动递增、自动递增、序列、序列等)身份证
  3. 混合密钥(半自然/半人工密钥):通常由一个人工ID和一些额外的自然列组成,例如引用另一个使用ID并扩展该密钥的表(实体ID、序号nbr)或类似的表

常见场景:对根、分支或叶继承表的多对一引用,它们都通过标识关系/依赖键共享一个公共的“愚蠢”ID。 当另一个表需要引用所有实体类型时,根(和分支)表通常是有意义的,例如PostAddress->;联系人,其中联系人有子表人员、俱乐部、, 还有一些设施,它们除了“可联系”之外没有任何共同之处

现在请看JPA:

在Java中,我们可以创建PK可能不完整(null或部分null)的新实体对象,DBMS最终会阻止我们插入数据库的实体(行)

然而,在使用应用程序代码时,即使新的实体对象还没有PK值,拥有可以与现有(托管)实体进行比较的新的(或分离的)实体通常也很方便。要为任何具有自然键列的实体实现这一点,请使用它们实现equals()和hashCode()(正如其他两个SO帖子所建议的)

问题:

但是,当无法确定自然/业务密钥时,如Contacts表,它基本上只是一个ID(加上一个鉴别器),您会怎么做?对于equals()和hashCode()实现,什么是好的列选择策略?(上面的人工键2和3)

显然没有太多选择

一个(天真的)目标是实现同样的“暂时可比性”。能做到吗?如果不是,那么人工ID equals()和hashCode()实现的一般方法是什么


注意:我已经在使用Apache EqualBuilder和HashCodeBuilder了。。。我故意把我的问题“美化”了


共 (3) 个答案

  1. # 1 楼答案

    如果你在这个物体上找不到一组能将它与其他同类物体区分开来的属性,那么你就不能比较这些物体,对吗?如果您提供了详细的用例,可能会有更多内容,但如果使用id和鉴别器,在没有id的情况下,您只能比较具有相同鉴别器的对象组。如果保证组只有一个元素,那么鉴别器就是关键

  2. # 2 楼答案

    通常建议的技术之一是将UUID用于标识符,这有两个缺点

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

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

    我决定在自己的项目中使用的解决方案是混合使用传统的指定标识符,并在hashCode()equals()方法内部使用UUID。它看起来像这样:

    @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字段在我们的应用程序代码中从未使用过,甚至从未考虑过,只是出于比较的目的。很有魅力

  3. # 3 楼答案

    我认为这个问题比讨论要简单得多

    获取数据库id(如果存在),否则使用对象#equals/Object identity

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

    讨论的重点似乎通常是假设两个具有相同属性的业务对象是相等的。但事实并非如此。 例如,只有当你不想有地址值的重复项时,同一街道和城市的两个地址才是相等的。但随后,您也将它们设置为数据库中的主键,这就导致了这样一个事实,即您总是为业务对象获取主键。 如果允许业务对象使用重复地址,则对象标识是主键,因为它是两个地址之间的唯一区别

    在分配一个实体后,数据库id会完全接管该任务,因为现在可以有同一实体的克隆,而该实体只共享相同的数据库id(但现在可以有多个内存位置/对象标识)