development

JPA hashCode () / equals () 딜레마

big-blog 2020. 3. 17. 23:38
반응형

JPA hashCode () / equals () 딜레마


가 있었다 몇 가지 논의 JPA 엔티티와 여기에 대해 hashCode()/ equals()JPA의 엔티티 클래스에 사용되어야 구현입니다. 그들 중 대부분은 Hibernate에 의존하지만 JPA 구현 중립적으로 논의하고 싶습니다 (그런데 EclipseLink를 사용하고 있습니다).

가능한 모든 구현에는 다음과 같은 장점단점 이 있습니다.

  • hashCode()/equals() 계약 적합성 에 대한 (불변) List/ Set운영
  • 이든 동일한 개체 (예를 들면 서로 다른 세션에서, 느리게로드 된 데이터 구조로부터 동적 프록시)가 검출 될 수있다
  • 엔티티가 분리 (또는 비 지속) 상태 에서 올바르게 작동하는지 여부

내가 알 수 있듯이 세 가지 옵션이 있습니다 .

  1. 재정의하지 마십시오. 에 의존 Object.equals()하고Object.hashCode()
    • hashCode()/ equals()
    • 동일한 객체를 식별 할 수 없음, 동적 프록시 문제
    • 분리 된 엔티티에 문제가 없음
  2. 기본 키를 기준으로 재정의
    • hashCode()/ equals()깨졌다
    • 올바른 ID (모든 관리 대상 엔터티)
    • 분리 된 엔터티 문제
  3. Business-Id (기본이 아닌 키 필드, 외래 키는 어떻습니까)를 기준으로이를 재정의하십시오 .
    • hashCode()/ equals()깨졌다
    • 올바른 ID (모든 관리 대상 엔터티)
    • 분리 된 엔티티에 문제가 없음

내 질문은 :

  1. 옵션 및 / 또는 프로 / 콘택 포인트가 누락 되었습니까?
  2. 어떤 옵션을 선택했으며 그 이유는 무엇입니까?



업데이트 1 :

" hashCode()/ equals()깨진"이라는 말은 연속적인 hashCode()호출이 다른 값을 리턴 할 수 있음을 의미하며 , 이는 (정확하게 구현 된 경우) ObjectAPI 문서 의 의미에서 손상되지 않지만 Map, Set또는 해시 기반 Collection. 따라서 JPA 구현 (적어도 EclipseLink)은 경우에 따라 올바르게 작동하지 않습니다.

업데이트 2 :

답변 주셔서 감사합니다. 대부분 뛰어난 품질을 가지고 있습니다.
불행히도 실제 응용 프로그램에 가장 적합한 방법이나 응용 프로그램에 가장 적합한 방법을 결정하는 방법은 여전히 ​​확실하지 않습니다. 따라서 질문을 계속 열어두고 더 많은 토론이나 의견이 있기를 바랍니다.


이 주제에 관한이 훌륭한 기사를 읽으십시오 : 최대 절전 모드로 사용자의 신원을 도용하지 마십시오 .

기사의 결론은 다음과 같습니다.

객체가 데이터베이스에 유지 될 때 객체 아이덴티티는 기만적으로 올바르게 구현하기가 어렵습니다. 그러나 문제는 개체를 저장하기 전에 ID없이 개체를 존재하게하는 것에서 비롯됩니다. 우리는 Hibernate와 같은 객체 관계형 매핑 프레임 워크로부터 객체 ID를 할당하는 책임을지게함으로써 이러한 문제를 해결할 수있다. 대신, 객체가 인스턴스화되는 즉시 객체 ID를 할당 할 수 있습니다. 따라서 개체 ID를 간단하고 오류없이 만들 수 있으며 도메인 모델에 필요한 코드의 양이 줄어 듭니다.


항상 equals / hashcode를 재정의하고 비즈니스 ID를 기반으로 구현합니다. 나에게 가장 합리적인 해결책 인 것 같습니다. 다음 링크를 참조하십시오 .

이 모든 것을 요약하면 equals / hashCode를 처리하는 다른 방법으로 작동하거나 작동하지 않는 항목이 있습니다. 여기에 이미지 설명을 입력하십시오

편집 :

이것이 왜 효과가 있는지 설명하려면 :

  1. 일반적으로 JPA 응용 프로그램에서 해시 기반 컬렉션 (HashMap / HashSet)을 사용하지 않습니다. 필요한 경우 UniqueList 솔루션을 만드는 것을 선호합니다.
  2. 런타임에 비즈니스 ID를 변경하는 것이 모든 데이터베이스 응용 프로그램에 대한 모범 사례가 아니라고 생각합니다. 다른 솔루션이없는 드문 경우에는 요소를 제거하고 해시 기반 컬렉션에 다시 넣는 것과 같은 특별한 처리를 수행합니다.
  3. 내 모델의 경우 생성자에 비즈니스 ID를 설정하고 해당 설정자를 제공하지 않습니다. JPA 구현 이 속성 대신 필드 를 변경 하도록했습니다.
  4. UUID 솔루션은 과도하게 보입니다. 자연적인 사업체 ID가있는 경우 왜 UUID입니까? 결국 데이터베이스에서 비즈니스 ID의 고유성을 설정했습니다. 그러면 데이터베이스의 각 테이블에 대해 세 개의 인덱스가있는 이유는 무엇 입니까?

엔티티에는 일반적으로 두 개의 ID가 있습니다.

  1. 지속성 계층에만 해당되므로 지속성 공급자와 데이터베이스가 개체 간의 관계를 파악할 수 있습니다.
  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
}

편집 :setUuid() 메소드 호출과 관련된 요점을 명확히 합니다. 일반적인 시나리오는 다음과 같습니다.

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

기본 생성자와 세터를 사용하지만 두 생성자가 더 적합한 방법을 찾을 수 있습니다.


동일한 엔티티 가 한 번만 존재할 수 equals()/hashCode()있다는 의미에서 세트 에 사용하려는 경우 옵션 2 만 있습니다. 옵션 2 이는 정의에 따라 엔티티 기본 키 가 변경되지 않기 때문입니다 (누군가가 실제로 업데이트하는 경우) 그것은 더 이상 같은 실체가 아니다)

문자 그대로 사용해야 equals()/hashCode()합니다. 기본 키를 기반으로하므로 기본 키가 설정 될 때까지 이러한 방법을 사용해서는 안됩니다. 따라서 기본 키가 할당 될 때까지 집합에 엔터티를 넣지 않아야합니다. (예, UUID 및 이와 유사한 개념은 기본 키를 조기에 할당하는 데 도움이 될 수 있습니다.)

Now, it's theoretically also possible to achieve that with Option 3, even though so-called "business-keys" have the nasty drawback that they can change: "All you'll have to do is delete the already inserted entities from the set(s), and re-insert them." That is true - but it also means, that in a distributed system, you'll have to make sure, that this is done absolutely everywhere the data has been inserted to (and you'll have to make sure, that the update is performed, before other things occur). You'll need a sophisticated update mechanism, especially if some remote systems aren't currently reachable...

옵션 1은 세트의 모든 오브젝트가 동일한 최대 절전 모드 세션에있는 경우에만 사용할 수 있습니다. Hibernate 문서는 13.1.3 장에서 이것을 명확하게한다 . 객체 아이덴티티 고려 :

세션 내에서 응용 프로그램은 ==를 사용하여 객체를 안전하게 비교할 수 있습니다.

그러나 세션 외부에서 ==를 사용하는 응용 프로그램은 예기치 않은 결과를 생성 할 수 있습니다. 예기치 않은 장소에서도 발생할 수 있습니다. 예를 들어, 분리 된 두 인스턴스를 동일한 세트에 배치하면 둘 다 동일한 데이터베이스 ID를 가질 수 있습니다 (즉, 동일한 행을 나타냄). 그러나 분리 된 상태의 인스턴스에 대해서는 정의에 따라 JVM ID가 보장되지 않습니다. 개발자는 영속 클래스에서 equals () 및 hashCode () 메소드를 대체하고 고유 한 오브젝트 평등 개념을 구현해야합니다.

옵션 3에 찬성하여 계속 주장하고 있습니다.

한 가지주의 사항이 있습니다. 평등을 구현하기 위해 데이터베이스 식별자를 사용하지 마십시오. 고유하고 일반적으로 변경 불가능한 속성의 조합 인 비즈니스 키를 사용하십시오. 임시 오브젝트가 지속되면 데이터베이스 ID가 변경됩니다. 임시 인스턴스 (일반적으로 분리 된 인스턴스와 함께)가 세트에 유지되는 경우 해시 코드를 변경하면 세트의 계약이 중단됩니다.

이것은 사실입니다 만약 당신이

  • ID를 조기에 할당 할 수 없습니다 (예 : UUID 사용)
  • 그러나 객체는 일시적인 상태에서 객체를 세트에 넣기를 원합니다.

그렇지 않으면 옵션 2를 자유롭게 선택할 수 있습니다.

그런 다음 상대적인 안정성이 필요하다고 언급합니다.

비즈니스 키의 속성은 데이터베이스 기본 키만큼 안정적 일 필요는 없습니다. 객체가 동일한 세트에있는 한 안정성 만 보장하면됩니다.

맞습니다. 내가 실제로 볼 수있는 문제는 : 절대 안정성을 보장 할 수 없다면, "객체가 같은 세트에있는 한"안정성을 어떻게 보장 할 수 있을까요? 대화를 위해서만 세트를 사용하고 버리는 것과 같은 특별한 경우를 상상할 수는 있지만 이것의 일반적인 실행 가능성에 의문을 제기합니다.


짧은 버전 :

  • 옵션 1은 단일 세션 내의 개체에만 사용할 수 있습니다.
  • 가능하면 옵션 2를 사용하십시오. PK가 할당 될 때까지 개체를 세트로 사용할 수 없으므로 가능한 빨리 PK를 할당하십시오.
  • 상대적 안정성을 보장 할 수 있으면 옵션 3을 사용할 수 있습니다. 그러나이 점에주의하십시오.

저는 개인적으로이 세 가지 상태를 모두 다른 프로젝트에서 이미 사용했습니다. 그리고 옵션 1은 실제 응용 프로그램에서 가장 실용적이라고 생각해야합니다. 내 경험상 hashCode () / equals () 적합성을 깨는 것은 엔티티가 컬렉션에 추가 된 후 평등의 결과가 변경되는 상황에서 매번 끝날 것이므로 많은 미친 버그로 이어집니다.

그러나 추가 옵션이 있습니다 (장단점도 있습니다).


a) 해시 코드 / 세트에 기초하여 일치 한 불변 , null이 , 할당 생성자 필드

(+) 세 가지 기준 모두 보장

새 인스턴스를 만들려면 (-) 필드 값을 사용할 수 있어야합니다

(-) 다음 중 하나를 변경해야하는 경우 처리가 복잡해집니다.


b) hashCode / 같음 (JPA 대신 애플리케이션에서 생성자에 의해 지정된 기본 키를 기준으로 함)

(+) 세 가지 기준 모두 보장

(-) DB 시퀀스와 같은 단순하고 안정적인 ID 생성 상태를 활용할 수 없습니다.

(-) 분산 된 환경 (클라이언트 / 서버) 또는 앱 서버 클러스터에서 새로운 엔티티가 생성되면 복잡


c) 엔티티의 생성자에 의해 할당 된 UUID를 기반으로하는 hashCode / 같음

(+) 세 가지 기준 모두 보장

(-) UUID 생성 오버 헤드

(-)는 사용 된 알고리즘에 따라 동일한 UUID의 두 배가 사용될 위험이 적을 수 있습니다 (DB의 고유 색인으로 감지 될 수 있음)


  1. 당신이있는 경우 비즈니스 키를 , 당신은을 위해 그것을 사용한다 equals/ hashCode.
  2. 비즈니스 키가없는 경우 기본 키 Object와 hashCode 구현을 그대로두면 안됩니다.이 키 는 사용자 merge와 엔터티 이후에는 작동하지 않기 때문입니다 .
  3. 이 게시물에서 제안한 엔터티 식별자를 사용할 수 있습니다 . 유일한 문제점은 다음 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 31;
        }
    
        //Getters and setters omitted for brevity
    }
    

비즈니스 키 (옵션 3)를 사용하는 것이 가장 일반적으로 권장되는 접근 방법 이지만 ( Hibernate 커뮤니티 위키 , "Java Persistence with Hibernate"(398 페이지)) 가장 많이 사용하는 방법이지만, 최대 절전 모드에서이를 깨뜨리는 Hibernate 버그가 있습니다. 세트 : HHH-3799 . 이 경우, Hibernate는 필드가 초기화되기 전에 엔티티를 세트에 추가 할 수 있습니다. 권장되는 비즈니스 키 접근 방식이 실제로 문제가되기 때문에이 버그가 왜 더주의를 기울이지 않았는지 잘 모르겠습니다.

문제의 핵심은 equals와 hashCode가 불변 상태 ( Odersky et al. 참조)를 기반으로해야하며 Hibernate 관리 기본 키가있는 Hibernate 엔티티 에는 그러한 불변 상태 없다는 것 입니다. 기본 키는 임시 객체가 지속될 때 Hibernate에 의해 수정됩니다. 비즈니스 키는 초기화 과정에서 개체를 수화시킬 때 Hibernate에 의해 수정됩니다.

옵션 1 만 남겨두고, 객체 아이덴티티를 기반으로하는 java.lang.Object 구현을 상속하거나, "Do n't Let Hibernate Steal Your Identity" (Stijn Geukens의 답변에서 이미 참조) )과의 랜스 Arlaus에 의해 "개체 발생 : 최대 절전 모드 통합에 더 나은 접근" .

옵션 1의 가장 큰 문제는 분리 된 인스턴스를 .equals ()를 사용하는 영구 인스턴스와 비교할 수 없다는 것입니다. 그러나 괜찮습니다. equals와 hashCode의 계약은 개발자가 각 클래스에 대한 평등의 의미를 결정하도록합니다. 따라서 equals와 hashCode가 Object에서 상속되도록하십시오. 분리 된 인스턴스를 영구 인스턴스와 비교해야하는 경우 해당 목적에 따라 boolean sameEntity또는 boolean dbEquivalent또는 으로 명시 적으로 새 메소드를 작성할 수 있습니다 boolean businessEquals.


앤드류의 대답에 동의합니다. 우리는 응용 프로그램에서 동일한 작업을 수행하지만 UARD를 VARCHAR / CHAR로 저장하는 대신 두 개의 긴 값으로 나눕니다. UUID.getLeastSignificantBits () 및 UUID.getMostSignificantBits ()를 참조하십시오.

고려해야 할 또 다른 사항은 UUID.randomUUID () 호출이 매우 느리기 때문에 지속성 또는 equals () / hashCode () 호출과 같이 필요할 때만 느리게 UUID를 생성하는 것이 좋습니다.

@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();
    }

}

다른 사람들이 이미 지적한 것처럼 이미 지적했듯이 수많은 전략이 있습니다. 적용되는 디자인 패턴의 대부분은 성공을 향해 해킹을 시도하는 경우가있는 것 같습니다. 특수 생성자와 팩토리 메소드로 생성자 호출을 완전히 방해하지 않으면 생성자 액세스를 제한합니다. 실제로 명확한 컷 API로 항상 유쾌합니다. 그러나 유일한 이유는 equals 및 hashcode 재정의가 응용 프로그램과 호환되도록하는 것이라면 해당 전략이 KISS (Keep It Simple Stupid)를 준수하는지 궁금합니다.

나에게 id를 검사하여 equals와 hashcode를 재정의하고 싶습니다. 이 방법에서는 id가 null이 아니어야 하며이 동작을 잘 문서화해야합니다. 따라서 개발자를 다른 곳에 보관하기 전에 새로운 엔티티를 유지하는 계약이 될 것입니다. 이 계약을 준수하지 않는 응용 프로그램은 1 분 안에 실패 할 것입니다.

주의 사항 : 엔터티가 다른 테이블에 저장되어 있고 공급자가 기본 키에 대해 자동 생성 전략을 사용하는 경우 엔터티 유형간에 중복 된 기본 키를 얻게됩니다. 이 경우 런타임 유형을 Object # getClass () 호출과 비교 하면 물론 두 가지 유형이 동일하다고 간주 할 수 없습니다. 그것은 대부분 나에게 적합합니다.


여기에 이미 매우 유익한 답변이 있지만 우리가하는 일을 알려 드리겠습니다.

우리는 아무것도하지 않는다 (즉, 무시하지 않는다).

컬렉션에서 작동하기 위해 equals / hashcode가 필요한 경우 UUID를 사용합니다. 생성자에서 UUID를 작성하기 만하면됩니다. UUID 에는 http://wiki.fasterxml.com/JugHome사용 합니다. UUID는 약간 더 비싼 CPU이지만 직렬화 및 db 액세스에 비해 저렴합니다.


비즈니스 키 접근 방식은 우리에게 적합하지 않습니다. 딜레마를 해결하기 위해 DB 생성 ID , 임시 임시 tempId를 사용 하고 equal () / hashcode ()를 재정의 합니다. 모든 엔터티는 엔터티의 자손입니다. 장점 :

  1. DB에 추가 필드가 없습니다
  2. 자손 엔터티에 추가 코딩이 없으며 모든 사람을위한 하나의 접근법
  3. UUID와 같은 성능 문제 없음, DB Id 생성
  4. 해시 맵에 문제가 없습니다 (등등의 사용을 명심할 필요는 없습니다)
  5. 새로운 엔티티의 해시 코드는 지속 후에도 시간이 바뀌지 않습니다.

단점 :

  1. 지속되지 않는 엔티티를 직렬화 및 직렬화 해제하는 데 문제가있을 수 있습니다.
  2. 저장된 엔터티의 해시 코드는 DB에서 다시로드 한 후 변경 될 수 있습니다
  3. 지속되는 객체가 항상 다른 것으로 간주되지는 않습니다 (아마도 맞습니까?)
  4. 또 뭐요?

우리의 코드를보십시오 :

@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();
    }
}

사전 정의 된 유형 식별자 및 ID를 기반으로 다음 접근 방식을 고려하십시오.

JPA에 대한 특정 가정 :

  • 동일한 "유형"및 동일한 널이 아닌 ID의 엔티티는 동일한 것으로 간주됩니다.
  • 비 지속 엔티티 (ID가 없다고 가정)는 다른 엔티티와 동일하지 않습니다.

추상 실체 :

@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());
}

주요 장점 :

  • 간단
  • 서브 클래스가 타입 아이덴티티를 제공하도록 보장
  • 프록시 클래스의 예상 동작

단점 :

  • 각 엔티티가 전화해야 함 super()

노트:

  • 상속을 사용할 때주의가 필요합니다. 의 예 : 인스턴스 평등 class Aclass B extends A응용 프로그램의 구체적인 세부 사항에 따라 달라질 수 있습니다.
  • 비즈니스 키를 ID로 사용하는 것이 이상적입니다.

귀하의 의견을 기대합니다.


나는 이러한 토론을 알고 있었고 올바른 일을 알기 전까지는 아무것도하지 않는 것이 좋기 때문에 과거에는 항상 옵션 1을 사용했습니다. 이러한 시스템은 모두 여전히 성공적으로 실행되고 있습니다.

그러나 다음에 데이터베이스 생성 ID를 사용하여 옵션 2를 시도 할 수 있습니다.

ID가 설정되지 않은 경우 해시 코드와 같음은 IllegalStateException을 발생시킵니다.

이렇게하면 저장되지 않은 엔터티와 관련된 미묘한 오류가 예기치 않게 표시되지 않습니다.

사람들은이 접근법에 대해 어떻게 생각합니까?


이것은 Java 및 JPA를 사용하는 모든 IT 시스템에서 공통적 인 문제입니다. 문제점은 equals () 및 hashCode () 구현을 넘어 조직이 엔티티를 참조하는 방법과 클라이언트가 동일한 엔티티를 참조하는 방법에 영향을줍니다. 나는 내 견해를 표현하기 위해 내 자신의 블로그썼을 때 비즈니스 키를 갖지 못하는 데 충분한 고통을 보았다 .

간단히 말해서, RAM 이외의 스토리지에 의존하지 않고 생성되는 비즈니스 키로 의미있는 접두어가있는 짧고 사람이 읽을 수있는 순차적 ID를 사용하십시오. 트위터의 눈송이 는 아주 좋은 예입니다.


IMO에는 equals / hashCode를 구현하기위한 3 가지 옵션이 있습니다

  • 응용 프로그램 생성 ID (예 : UUID)를 사용하십시오.
  • 비즈니스 키를 기반으로 구현
  • 기본 키를 기반으로 구현

응용 프로그램 생성 ID를 사용하는 것이 가장 쉬운 방법이지만 몇 가지 단점이 있습니다.

  • 128 비트가 단순히 32 또는 64 비트보다 크기 때문에 PK로 사용할 때 결합 속도가 느려집니다.
  • "디버깅은 더 어렵다"자신의 눈으로 확인하기 때문에 어떤 데이터가 정확한지 확인하는 것은 매우 어렵다

이러한 단점을 해결 하려면이 방법을 사용하십시오.

조인 문제를 극복하기 위해 UUID를 자연 키로 사용하고 시퀀스 값을 기본 키로 사용할 수 있지만 조인 기반으로하기를 원하므로 ID가 포함 된 컴포지션 자식 엔터티에서 equals / hashCode 구현 문제가 계속 발생할 수 있습니다 기본 키에. 자식 엔터티 ID에서 자연 키를 사용하고 부모를 참조하기 위해 기본 키를 사용하는 것이 좋습니다.

@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
}

IMO는 모든 단점을 피하고 동시에 시스템 내부를 노출시키지 않고 외부 시스템과 공유 할 수있는 가치 (UUID)를 제공하므로 가장 깨끗한 접근 방식입니다.

사용자가 좋은 아이디어라고 생각할 수 있지만 몇 가지 단점도 있습니다. 비즈니스 키를 기반으로 구현하십시오.

대부분의 경우이 비즈니스 키는 사용자가 제공하는 일종의 코드 이며 여러 속성의 합성 횟수는 적습니다.

  • 가변 길이 텍스트를 기반으로하는 결합이 느리기 때문에 결합이 느립니다. 키가 특정 길이를 초과하면 일부 DBMS에서 색인 작성에 문제가있을 수도 있습니다.
  • 내 경험상 비즈니스 키는 변경되는 경향이 있으며이를 참조하는 개체에 대한 계단식 업데이트가 필요합니다. 외부 시스템이이를 참조하는 경우 불가능합니다

IMO는 비즈니스 키를 독점적으로 구현하거나 사용해서는 안됩니다. 사용자가 해당 비즈니스 키로 신속하게 검색 할 수있는 훌륭한 추가 기능이지만 시스템은 운영에 의존해서는 안됩니다.

기본 키를 기반으로 구현하면 문제가 있지만 그렇게 큰 문제는 아닙니다.

외부 시스템에 ID를 노출해야하는 경우 제안한 UUID 방식을 사용하십시오. 그렇지 않은 경우 여전히 UUID 접근 방식을 사용할 수 있지만 반드시 그럴 필요는 없습니다. equals / hashCode에서 DBMS 생성 ID를 사용하는 문제는 ID를 지정하기 전에 오브젝트가 해시 기반 콜렉션에 추가되었을 수 있다는 사실에서 비롯됩니다.

이 문제를 해결하는 확실한 방법은 id를 할당하기 전에 해시 기반 컬렉션에 객체를 추가하지 않는 것입니다. ID를 이미 할당하기 전에 중복 제거를 원할 수 있기 때문에 이것이 항상 가능한 것은 아니라는 것을 알고 있습니다. 해시 기반 컬렉션을 계속 사용하려면 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
}

나는 정확한 접근법을 직접 테스트하지 않았기 때문에 지속 전후 이벤트에서 콜렉션 변경이 어떻게 작동하는지 잘 모르겠지만 아이디어는 다음과 같습니다.

  • 해시 기반 컬렉션에서 개체를 일시적으로 제거
  • 그것을 유지
  • 해시 기반 컬렉션에 개체를 다시 추가

이 문제를 해결하는 또 다른 방법은 업데이트 / 지속 후에 모든 해시 기반 모델을 간단히 다시 작성하는 것입니다.

결국, 그것은 당신에게 달려 있습니다. 개인적으로 시퀀스 기반 접근 방식을 사용하고 외부 시스템에 식별자를 노출해야하는 경우에만 UUID 접근 방식을 사용합니다.


UUID가 많은 사람들에게 정답이라면 비즈니스 계층의 팩토리 메소드를 사용하여 엔티티를 작성하고 작성시 기본 키를 지정하지 않는 이유는 무엇입니까?

예를 들면 다음과 같습니다.

@ManagedBean
public class MyCarFacade {
  public Car createCar(){
    Car car = new Car();
    em.persist(car);
    return car;
  }
}

이 방법으로 우리는 영속 제공자로부터 엔티티에 대한 기본 기본 키를 얻을 것이고, 우리의 hashCode () 및 equals () 함수는 그것에 의존 할 수 있습니다.

또한 Car의 생성자를 보호 된 것으로 선언 한 다음 비즈니스 방법에 반영을 사용하여 액세스 할 수 있습니다. 이런 식으로 개발자는 새로운 방법으로 자동차를 인스턴스화하지 않고 팩토리 방식을 통해 의도합니다.

어때요?


나는이 질문에 스스로 대답하려고 노력했지만이 게시물과 특히 DREW를 읽을 때까지 찾은 솔루션에 완전히 만족하지 않았습니다. 나는 그가 게으른 UUID를 만들고 그것을 최적으로 저장하는 방식을 좋아했습니다.

그러나 각 솔루션의 장점으로 엔티티의 첫 번째 지속성 전에 hashCode () / equals ()에 액세스 할 때만 지연을 생성하는 UUID 만 더 유연하게 추가하고 싶었습니다.

  • equals ()는 "객체가 동일한 논리적 개체를 참조 함"을 의미합니다
  • 작업을 두 번 수행하는 이유 (성능 문제)로 데이터베이스 ID를 최대한 많이 사용
  • 아직 유지되지 않은 엔터티에서 hashCode () / equals ()에 액세스하는 동안 문제를 방지하고 실제로 지속 된 후에도 동일한 동작을 유지합니다.

아래의 혼합 솔루션에 대한 피드백을 정말 정확하게 계산하겠습니다.

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;
    }

실제로 옵션 2 (기본 키)가 가장 자주 사용되는 것 같습니다. 자연스럽고 IMMUTABLE 비즈니스 키는 드물지만 합성 키를 생성하고 지원하는 것은 너무 무거워서 결코 일어날 수없는 상황을 해결할 수 없습니다. 한 번 봐 가지고 스프링 데이터 JPA AbstractPersistable (: 유일한 구현 최대 절전 모드 구현 사용을Hibernate.getClass ).

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;
}

HashSet / HashMap에서 새 객체를 조작하는 것을 알고 있습니다. 반대로 옵션 1 (나머지 Object구현)은 직후에 중단됩니다 merge. 이는 매우 일반적인 상황입니다.

비즈니스 키가없고 REAL이 해시 구조에서 새 엔티티를 조작해야하는 hashCode경우 Vlad Mihalcea가 권고 한대로 상수로 대체하십시오 .


아래는 스칼라를위한 간단 하고 테스트 된 솔루션입니다.

  • 이 솔루션은 질문에 주어진 3 가지 범주 중 어느 것에도 적합하지 않습니다.

  • 모든 엔티티는 UUIDEntity의 서브 클래스이므로 DRY (Dont-repeat-yourself) 원칙을 따릅니다.

  • 필요한 경우 (더 많은 의사 난수를 사용하여) 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()
}

참고 URL : https://stackoverflow.com/questions/5031614/the-jpa-hashcode-equals-dilemma

반응형