Java JPA hashCode()/equals()难题

Java JPA hashCode()/equals()难题,java,hibernate,jpa,identity,eclipselink,Java,Hibernate,Jpa,Identity,Eclipselink,这里已经讨论了JPA实体,JPA实体类应该使用哪些hashCode()/equals()实现。大多数(如果不是全部的话)都依赖于Hibernate,但我想中性地讨论它们JPA实现(顺便说一下,我使用的是EclipseLink) 在以下方面,所有可能的实施都有自己的优点和缺点: hashCode()/equals()针对列表/集合操作的一致性(不变性) 是否可以检测到相同的对象(例如来自不同会话、来自延迟加载数据结构的动态代理) 实体在分离(或非持久化)状态下的行为是否正确 据我所知,有三种

这里已经讨论了JPA实体,JPA实体类应该使用哪些
hashCode()
/
equals()
实现。大多数(如果不是全部的话)都依赖于Hibernate,但我想中性地讨论它们JPA实现(顺便说一下,我使用的是EclipseLink)

在以下方面,所有可能的实施都有自己的优点和缺点:

  • hashCode()
    /
    equals()
    针对
    列表
    /
    集合
    操作的一致性(不变性)
  • 是否可以检测到相同的对象(例如来自不同会话、来自延迟加载数据结构的动态代理)
  • 实体在分离(或非持久化)状态下的行为是否正确
据我所知,有三种选择:

  • 不要超越它们;依赖
    Object.equals()
    Object.hashCode()
    • hashCode()
      /
      equals()
      工作
    • 无法识别相同的对象,动态代理存在问题
    • 分离实体没有问题
  • 基于主键覆盖它们
    • hashCode()
      /
      equals()
      已损坏
    • 正确标识(适用于所有托管实体)
    • 分离实体的问题
  • 基于业务Id(非主键字段;外键如何?)
    • hashCode()
      /
      equals()
      已损坏
    • 正确标识(适用于所有托管实体)
    • 分离实体没有问题
  • 我的问题是:

  • 我是否错过了一个选项和/或赞成/反对的观点?
  • 你选择了什么选项?为什么


  • 更新1:


    通过“
    hashCode()
    /
    equals()
    被破坏”,我的意思是连续的
    hashCode()
    调用可能会返回不同的值,这些值(在正确实现时)在
    对象
    API文档的意义上不会被破坏,但是,当尝试从
    映射
    或其他基于哈希的
    集合
    检索更改的实体时,会出现问题。因此,JPA实现(至少EclipseLink)在某些情况下无法正常工作

    更新2:

    感谢您的回答,其中大多数都有卓越的品质。

    不幸的是,我仍然不确定哪种方法最适合实际应用程序,或者如何确定适合我的应用程序的最佳方法。因此,我将继续讨论这个问题,希望能有更多的讨论和/或意见。

    我总是覆盖equals/hashcode,并根据业务id实现它。这似乎是对我来说最合理的解决方案。见下文

    总而言之,下面列出了处理equals/hashCode的不同方法的适用范围和不适用范围:

    编辑

    要解释为什么这对我有效:

  • 在我的JPA应用程序中,我通常不使用基于哈希的集合(HashMap/HashSet)。如果必须,我更喜欢创建唯一列表解决方案
  • 我认为在运行时更改业务id并不是任何数据库应用程序的最佳实践。在极少数情况下,如果没有其他解决方案,我会进行特殊处理,例如删除元素并将其放回基于哈希的集合
  • 对于我的模型,我在构造函数上设置了业务id,但没有为其提供setter。我让JPA实现更改字段而不是属性
  • UUID解决方案似乎有些过分。如果你有自然的商业id,为什么要使用UUID?毕竟,我会在数据库中设置业务id的唯一性。为什么数据库中的每个表都有三个索引

  • 我个人已经在不同的项目中使用了这三种策略。我必须说,在我看来,选项1是现实生活中最实用的应用程序。根据我的经验,破坏hashCode()/equals()一致性会导致许多疯狂的错误,因为每次在将实体添加到集合后,相等结果都会发生变化

    但还有其他选择(也有其利弊):


    a) hashCode/equals基于一组不可变的非空构造函数分配的,字段

    (+)所有三个标准都有保证

    (-)字段值必须可用才能创建新实例

    (-)如果必须更改其中一个,则会使处理复杂化


    b) hashCode/equals基于应用程序(在构造函数中)而不是JPA分配的主键

    (+)所有三个标准都有保证

    (-)不能利用简单可靠的ID生成策略,如DB序列

    (-)如果在分布式环境(客户端/服务器)或应用程序服务器群集中创建新实体,则会很复杂


    c) hashCode/equals基于实体构造函数指定的

    (+)所有三个标准都有保证

    (-)UUID生成的开销


    (-)可能存在两次使用同一UUID的风险,这取决于所使用的算法(可能由DB上的唯一索引检测)

    如果要对集合使用
    equals()/hashCode()
    ,同一实体只能在其中一次,那么只有一个选项:选项2。这是因为根据定义,实体的主键永远不会更改(如果有人确实更新了它,它就不再是同一个实体)

    您应该逐字理解:因为您的
    equals()/hashCode()
    基于主键,所以在设置主键之前,您不能使用这些方法。所以哟
    @Entity
    public class User {
    
        @Id
        private int id;  // Persistence ID
        private UUID uuid; // Business ID
    
        // assuming all fields are subject to change
        // If we forbid users change their email or screenName we can use these
        // fields for business ID instead, but generally that's not the case
        private String screenName;
        private String email;
    
        // I don't put UUID generation in constructor for performance reasons. 
        // I call setUuid() when I create a new entity
        public User() {
        }
    
        // This method is only called when a brand new entity is added to 
        // persistence context - I add it as a safety net only but it might work 
        // for you. In some cases (say, when I add this entity to some set before 
        // calling em.persist()) setting a UUID might be too late. If I get a log 
        // output it means that I forgot to call setUuid() somewhere.
        @PrePersist
        public void ensureUuid() {
            if (getUuid() == null) {
                log.warn(format("User's UUID wasn't set on time. " 
                    + "uuid: %s, name: %s, email: %s",
                    getUuid(), getScreenName(), getEmail()));
                setUuid(UUID.randomUUID());
            }
        }
    
        // equals() and hashCode() rely on non-changing data only. Thus we 
        // guarantee that no matter how field values are changed we won't 
        // lose our entity in hash-based Sets.
        @Override
        public int hashCode() {
            return getUuid().hashCode();
        }
    
        // Note that I don't use direct field access inside my entity classes and
        // call getters instead. That's because Persistence provider (PP) might
        // want to load entity data lazily. And I don't use 
        //    this.getClass() == other.getClass() 
        // for the same reason. In order to support laziness PP might need to wrap
        // my entity object in some kind of proxy, i.e. subclassing it.
        @Override
        public boolean equals(final Object obj) {
            if (this == obj)
                return true;
            if (!(obj instanceof User))
                return false;
            return getUuid().equals(((User) obj).getUuid());
        }
    
        // Getters and setters follow
    }
    
    User user = new User();
    // user.setUuid(UUID.randomUUID()); // I should have called it here
    user.setName("Master Yoda");
    user.setEmail("yoda@jedicouncil.org");
    
    jediSet.add(user); // here's bug - we forgot to set UUID and 
                       //we won't find Yoda in Jedi set
    
    em.persist(user); // ensureUuid() was called and printed the log for me.
    
    jediCouncilSet.add(user); // Ok, we got a UUID now
    
    User user = new User();
    user.setUuid(UUID.randomUUID());
    
    @Entity
    public class User {
    
        @Id
        private int id;  // Persistence ID
        private UUID uuid; // Business ID
    
        ... // fields
    
        // Constructor for Persistence provider to use
        public User() {
        }
    
        // Constructor I use when creating new entities
        public User(UUID uuid) {
            setUuid(uuid);
        }
    
        ... // rest of the entity.
    }
    
    User user = new User(UUID.randomUUID());
    ...
    jediSet.add(user); // no bug this time
    
    em.persist(user); // and no log output
    
    @MappedSuperclass
    public abstract class AbstractJpaEntity extends AbstractMutable implements Identifiable, Modifiable {
    
        private static final long   serialVersionUID    = 1L;
    
        @Version
        @Column(name = "version", nullable = false)
        private int                 version             = 0;
    
        @Column(name = "uuid_least_sig_bits")
        private long                uuidLeastSigBits    = 0;
    
        @Column(name = "uuid_most_sig_bits")
        private long                uuidMostSigBits     = 0;
    
        private transient int       hashCode            = 0;
    
        public AbstractJpaEntity() {
            //
        }
    
        public abstract Integer getId();
    
        public abstract void setId(final Integer id);
    
        public boolean isPersisted() {
            return getId() != null;
        }
    
        public int getVersion() {
            return version;
        }
    
        //calling UUID.randomUUID() is pretty expensive, 
        //so this is to lazily initialize uuid bits.
        private void initUUID() {
            final UUID uuid = UUID.randomUUID();
            uuidLeastSigBits = uuid.getLeastSignificantBits();
            uuidMostSigBits = uuid.getMostSignificantBits();
        }
    
        public long getUuidLeastSigBits() {
            //its safe to assume uuidMostSigBits of a valid UUID is never zero
            if (uuidMostSigBits == 0) {
                initUUID();
            }
            return uuidLeastSigBits;
        }
    
        public long getUuidMostSigBits() {
            //its safe to assume uuidMostSigBits of a valid UUID is never zero
            if (uuidMostSigBits == 0) {
                initUUID();
            }
            return uuidMostSigBits;
        }
    
        public UUID getUuid() {
            return new UUID(getUuidMostSigBits(), getUuidLeastSigBits());
        }
    
        @Override
        public int hashCode() {
            if (hashCode == 0) {
                hashCode = (int) (getUuidMostSigBits() >> 32 ^ getUuidMostSigBits() ^ getUuidLeastSigBits() >> 32 ^ getUuidLeastSigBits());
            }
            return hashCode;
        }
    
        @Override
        public boolean equals(final Object obj) {
            if (obj == null) {
                return false;
            }
            if (!(obj instanceof AbstractJpaEntity)) {
                return false;
            }
            //UUID guarantees a pretty good uniqueness factor across distributed systems, so we can safely
            //dismiss getClass().equals(obj.getClass()) here since the chance of two different objects (even 
            //if they have different types) having the same UUID is astronomical
            final AbstractJpaEntity entity = (AbstractJpaEntity) obj;
            return getUuidMostSigBits() == entity.getUuidMostSigBits() && getUuidLeastSigBits() == entity.getUuidLeastSigBits();
        }
    
        @PrePersist
        public void prePersist() {
            // make sure the uuid is set before persisting
            getUuidLeastSigBits();
        }
    
    }
    
    @ManagedBean
    public class MyCarFacade {
      public Car createCar(){
        Car car = new Car();
        em.persist(car);
        return car;
      }
    }
    
    public class MyEntity { 
    
        @Id()
        @Column(name = "ID", length = 20, nullable = false, unique = true)
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id = null;
    
        @Transient private UUID uuid = null;
    
        @Column(name = "UUID_MOST", nullable = true, unique = false, updatable = false)
        private Long uuidMostSignificantBits = null;
        @Column(name = "UUID_LEAST", nullable = true, unique = false, updatable = false)
        private Long uuidLeastSignificantBits = null;
    
        @Override
        public final int hashCode() {
            return this.getUuid().hashCode();
        }
    
        @Override
        public final boolean equals(Object toBeCompared) {
            if(this == toBeCompared) {
                return true;
            }
            if(toBeCompared == null) {
                return false;
            }
            if(!this.getClass().isInstance(toBeCompared)) {
                return false;
            }
            return this.getUuid().equals(((MyEntity)toBeCompared).getUuid());
        }
    
        public final UUID getUuid() {
            // UUID already accessed on this physical object
            if(this.uuid != null) {
                return this.uuid;
            }
            // UUID one day generated on this entity before it was persisted
            if(this.uuidMostSignificantBits != null) {
                this.uuid = new UUID(this.uuidMostSignificantBits, this.uuidLeastSignificantBits);
            // UUID never generated on this entity before it was persisted
            } else if(this.getId() != null) {
                this.uuid = new UUID(this.getId(), this.getId());
            // UUID never accessed on this not yet persisted entity
            } else {
                this.setUuid(UUID.randomUUID());
            }
            return this.uuid; 
        }
    
        private void setUuid(UUID uuid) {
            if(uuid == null) {
                return;
            }
            // For the one hypothetical case where generated UUID could colude with UUID build from IDs
            if(uuid.getMostSignificantBits() == uuid.getLeastSignificantBits()) {
                throw new Exception("UUID: " + this.getUuid() + " format is only for internal use");
            }
            this.uuidMostSignificantBits = uuid.getMostSignificantBits();
            this.uuidLeastSignificantBits = uuid.getLeastSignificantBits();
            this.uuid = uuid;
        }
    
    import javax.persistence._
    import scala.util.Random
    
    @Entity
    @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
    abstract class UUIDEntity {
      @Id  @GeneratedValue(strategy = GenerationType.TABLE)
      var id:java.lang.Long=null
      var uuid:java.lang.Long=Random.nextLong()
      override def equals(o:Any):Boolean= 
        o match{
          case o : UUIDEntity => o.uuid==uuid
          case _ => false
        }
      override def hashCode() = uuid.hashCode()
    }
    
    @Entity
    public class Book implements Identifiable<Long> {
    
        @Id
        @GeneratedValue
        private Long id;
    
        private String title;
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Book)) return false;
            Book book = (Book) o;
            return getId() != null && Objects.equals(getId(), book.getId());
        }
    
        @Override
        public int hashCode() {
            return getClass().hashCode();
        }
    
        //Getters and setters omitted for brevity
    }
    
    @MappedSuperclass
    abstract public class Entity implements Serializable {
    
        @Id
        @GeneratedValue
        @Column(nullable = false, updatable = false)
        protected Long id;
    
        @Transient
        private Long tempId;
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public Long getId() {
            return id;
        }
    
        private void setTempId(Long tempId) {
            this.tempId = tempId;
        }
    
        // Fix Id on first call from equal() or hashCode()
        private Long getTempId() {
            if (tempId == null)
                // if we have id already, use it, else use 0
                setTempId(getId() == null ? 0 : getId());
            return tempId;
        }
    
        @Override
        public boolean equals(Object obj) {
            if (super.equals(obj))
                return true;
            // take proxied object into account
            if (obj == null || !Hibernate.getClass(obj).equals(this.getClass()))
                return false;
            Entity o = (Entity) obj;
            return getTempId() != 0 && o.getTempId() != 0 && getTempId().equals(o.getTempId());
        }
    
        // hash doesn't change in time
        @Override
        public int hashCode() {
            return getTempId() == 0 ? super.hashCode() : getTempId().hashCode();
        }
    }
    
    public boolean equals(Object obj) {
        if (null == obj) {
            return false;
        }
        if (this == obj) {
            return true;
        }
        if (!getClass().equals(ClassUtils.getUserClass(obj))) {
            return false;
        }
        AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
        return null == this.getId() ? false : this.getId().equals(that.getId());
    }
    
    @Override
    public int hashCode() {
        int hashCode = 17;
        hashCode += null == getId() ? 0 : getId().hashCode() * 31;
        return hashCode;
    }
    
    @MappedSuperclass
    public abstract class AbstractPersistable<K extends Serializable> {
    
      @Id @GeneratedValue
      private K id;
    
      @Transient
      private final String kind;
    
      public AbstractPersistable(final String kind) {
        this.kind = requireNonNull(kind, "Entity kind cannot be null");
      }
    
      @Override
      public final boolean equals(final Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof AbstractPersistable)) return false;
        final AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
        return null != this.id
            && Objects.equals(this.id, that.id)
            && Objects.equals(this.kind, that.kind);
      }
    
      @Override
      public final int hashCode() {
        return Objects.hash(kind, id);
      }
    
      public K getId() {
        return id;
      }
    
      protected void setId(final K id) {
        this.id = id;
      }
    }
    
    static class Foo extends AbstractPersistable<Long> {
      public Foo() {
        super("Foo");
      }
    }
    
    @Test
    public void test_EqualsAndHashcode_GivenSubclass() {
      // Check contract
      EqualsVerifier.forClass(Foo.class)
        .suppress(Warning.NONFINAL_FIELDS, Warning.TRANSIENT_FIELDS)
        .withOnlyTheseFields("id", "kind")
        .withNonnullFields("id", "kind")
        .verify();
      // Ensure new objects are not equal
      assertNotEquals(new Foo(), new Foo());
    }
    
    @Entity class Parent {
      @Id @GeneratedValue Long id;
      @NaturalId UUID uuid;
      @OneToMany(mappedBy = "parent") Set<Child> children;
      // equals/hashCode based on uuid
    }
    
    @Entity class Child {
      @EmbeddedId ChildId id;
      @ManyToOne Parent parent;
    
      @Embeddable class ChildId {
        UUID parentUuid;
        UUID childUuid;
        // equals/hashCode based on parentUuid and childUuid
      }
      // equals/hashCode based on id
    }
    
    @Entity class Parent {
      @Id @GeneratedValue Long id;
      @OneToMany(mappedBy = "parent") Set<Child> children;
      // equals/hashCode based on id
    }
    
    @Entity class Child {
      @EmbeddedId ChildId id;
      @ManyToOne Parent parent;
    
      @PrePersist void postPersist() {
        parent.children.remove(this);
      }
      @PostPersist void postPersist() {
        parent.children.add(this);
      }
    
      @Embeddable class ChildId {
        Long parentId;
        @GeneratedValue Long childId;
        // equals/hashCode based on parentId and childId
      }
      // equals/hashCode based on id
    }