가 있었다 몇 가지 논의 JPA 엔티티와 여기에 대해 hashCode()
/ equals()
JPA의 엔티티 클래스에 사용되어야 구현입니다. 그들 중 대부분은 Hibernate에 의존하지만 JPA 구현 중립적으로 논의하고 싶습니다 (그런데 EclipseLink를 사용하고 있습니다).
가능한 모든 구현에는 다음과 관련하여 자체 장점 과 단점 이 있습니다.
hashCode()
/equals()
계약 적합성 에 대한 (불변)List
/Set
운영- 이든 동일한 개체 (예를 들면 서로 다른 세션에서, 느리게로드 된 데이터 구조로부터 동적 프록시)가 검출 될 수있다
- 엔티티가 분리 (또는 비 지속) 상태 에서 올바르게 작동하는지 여부
내가 알 수 있듯이 세 가지 옵션이 있습니다 .
- 재정의하지 마십시오. 에 의존
Object.equals()
하고Object.hashCode()
hashCode()
/equals()
일- 동일한 객체를 식별 할 수 없음, 동적 프록시 문제
- 분리 된 엔티티에 문제가 없음
- 기본 키를 기준으로 재정의
hashCode()
/equals()
깨졌다- 올바른 ID (모든 관리 대상 엔터티)
- 분리 된 엔터티 문제
- Business-Id (기본이 아닌 키 필드, 외래 키는 어떻습니까)를 기준으로이를 재정의하십시오 .
hashCode()
/equals()
깨졌다- 올바른 ID (모든 관리 대상 엔터티)
- 분리 된 엔티티에 문제가 없음
내 질문은 :
- 옵션 및 / 또는 프로 / 콘택 포인트가 누락 되었습니까?
- 어떤 옵션을 선택했으며 그 이유는 무엇입니까?
업데이트 1 :
” hashCode()
/ equals()
깨진”이라는 말은 연속적인 hashCode()
호출이 다른 값을 리턴 할 수 있음을 의미하며 , 이는 (정확하게 구현 된 경우) Object
API 문서 의 의미에서 손상되지 않지만 Map
, Set
또는 해시 기반 Collection
. 따라서 JPA 구현 (적어도 EclipseLink)은 경우에 따라 올바르게 작동하지 않습니다.
업데이트 2 :
답변 주셔서 감사합니다. 대부분 뛰어난 품질을 가지고 있습니다.
불행히도 실제 응용 프로그램에 가장 적합한 방법이나 응용 프로그램에 가장 적합한 방법을 결정하는 방법은 여전히 확실하지 않습니다. 따라서 질문을 계속 열어두고 더 많은 토론이나 의견이 있기를 바랍니다.
답변
주제에이 아주 좋은 기사를 읽기 : 음주 최대 절전 모드가 ID를 도용시키지 .
기사의 결론은 다음과 같습니다.
객체가 데이터베이스에 유지 될 때 객체 아이덴티티는 기만적으로 올바르게 구현하기가 어렵습니다. 그러나 문제는 개체를 저장하기 전에 ID없이 개체를 존재하게하는 것에서 비롯됩니다. 우리는 Hibernate와 같은 객체 관계형 매핑 프레임 워크로부터 객체 ID를 할당하는 책임을지게함으로써 이러한 문제를 해결할 수있다. 대신, 객체가 인스턴스화되는 즉시 객체 ID를 할당 할 수 있습니다. 따라서 개체 ID를 간단하고 오류없이 만들 수 있으며 도메인 모델에 필요한 코드 양이 줄어 듭니다.
답변
항상 equals / hashcode를 재정의하고 비즈니스 ID를 기반으로 구현합니다. 나에게 가장 합리적인 해결책 인 것 같습니다. 다음 링크를 참조하십시오 .
이 모든 것을 요약하면 equals / hashCode를 처리하는 다른 방법으로 작동하거나 작동하지 않는 항목이 있습니다.
편집 :
이것이 왜 효과가 있는지 설명하려면 :
- 일반적으로 JPA 응용 프로그램에서 해시 기반 컬렉션 (HashMap / HashSet)을 사용하지 않습니다. 필요한 경우 UniqueList 솔루션을 만드는 것을 선호합니다.
- 런타임에 비즈니스 ID를 변경하는 것이 모든 데이터베이스 응용 프로그램에 대한 모범 사례가 아니라고 생각합니다. 드물게 다른 솔루션이없는 경우 요소를 제거하고 해시 기반 컬렉션에 다시 넣는 것과 같은 특별한 처리를 수행합니다.
- 내 모델의 경우 생성자에 비즈니스 ID를 설정하고 해당 설정자를 제공하지 않습니다. JPA 구현 이 속성 대신 필드 를 변경 하도록했습니다.
- UUID 솔루션은 과도하게 보입니다. 자연적인 사업체 ID가있는 경우 왜 UUID입니까? 결국 데이터베이스에서 비즈니스 ID의 고유성을 설정했습니다. 그러면 데이터베이스의 각 테이블에 대해 세 개의 인덱스가있는 이유는 무엇 입니까?
답변
엔티티에는 일반적으로 두 개의 ID가 있습니다.
- 지속성 계층에만 해당되므로 지속성 공급자와 데이터베이스가 개체 간의 관계를 파악할 수 있습니다.
- 우리의 애플리케이션 요구 사항에 대한인가 (
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
기본 생성자와 세터를 사용하지만 두 생성자가 더 적합한 방법을 찾을 수 있습니다.
답변
저는 개인적으로이 세 가지 상태를 모두 다른 프로젝트에서 이미 사용했습니다. 그리고 옵션 1은 실제 응용 프로그램에서 가장 실용적이라고 생각해야합니다. 내 경험상 hashCode () / equals () 적합성을 깨는 것은 엔티티가 컬렉션에 추가 된 후 평등의 결과가 변경되는 상황에서 매번 끝날 것이므로 많은 미친 버그로 이어집니다.
그러나 추가 옵션이 있습니다 (장단점도 있습니다).
a) 해시 코드 / 세트에 기초하여 일치 한 불변 , null이 , 할당 생성자 필드
(+) 세 가지 기준 모두 보장
새 인스턴스를 만들려면 (-) 필드 값을 사용할 수 있어야합니다
(-) 다음 중 하나를 변경해야하는 경우 처리가 복잡해집니다.
b) hashCode / 같음 (JPA 대신 애플리케이션에서 생성자에 의해 지정된 기본 키를 기준으로 함)
(+) 세 가지 기준 모두 보장
(-) DB 시퀀스와 같은 단순하고 안정적인 ID 생성 상태를 활용할 수 없습니다.
(-) 분산 된 환경 (클라이언트 / 서버) 또는 앱 서버 클러스터에서 새로운 엔티티가 생성되면 복잡
c) 엔티티의 생성자가 할당 한 UUID를 기반으로하는 hashCode / 같음
(+) 세 가지 기준 모두 보장
(-) UUID 생성 오버 헤드
(-)는 사용 된 알고리즘에 따라 동일한 UUID의 두 배가 사용될 위험이 적을 수 있습니다 (DB의 고유 색인으로 감지 될 수 있음)
답변
동일한 엔티티 가 한 번만 존재할 수 equals()/hashCode()
있다는 의미에서 세트 에 사용하려는 경우 옵션 2 만 있습니다. 옵션 2 이는 정의에 따라 엔티티 의 기본 키 가 변경되지 않기 때문입니다 (누군가가 실제로 업데이트하는 경우) 그것은 더 이상 같은 실체가 아니다)
문자 그대로 사용해야 equals()/hashCode()
합니다. 기본 키를 기반으로하므로 기본 키가 설정 될 때까지 이러한 방법을 사용해서는 안됩니다. 따라서 기본 키가 할당 될 때까지 집합에 엔터티를 넣지 않아야합니다. (예, UUID 및 이와 유사한 개념은 기본 키를 조기에 할당하는 데 도움이 될 수 있습니다.)
이제는 소위 “비즈니스 키”가 변경할 수있는 단점이 있지만 옵션 3을 사용하여 이론적으로 달성 할 수도 있습니다. “세트에서 이미 삽입 된 엔티티를 삭제하기 만하면됩니다 ( ) 다시 삽입하십시오. ” 그것은 사실이지만 분산 시스템에서는 데이터가 삽입 된 모든 곳에서 수행되어야하며 업데이트가 수행되었는지 확인해야합니다. 다른 일이 발생하기 전에). 특히 일부 원격 시스템에 연결할 수없는 경우 정교한 업데이트 메커니즘이 필요합니다.
옵션 1은 세트의 모든 오브젝트가 동일한 최대 절전 모드 세션에있는 경우에만 사용할 수 있습니다. Hibernate 문서는 13.1.3 장에서 이것을 명확하게한다 . 객체 아이덴티티 고려 :
세션 내에서 응용 프로그램은 ==를 사용하여 객체를 안전하게 비교할 수 있습니다.
그러나 세션 외부에서 ==를 사용하는 응용 프로그램은 예기치 않은 결과를 생성 할 수 있습니다. 예기치 않은 장소에서도 발생할 수 있습니다. 예를 들어, 분리 된 두 인스턴스를 동일한 세트에 배치하면 둘 다 동일한 데이터베이스 ID를 가질 수 있습니다 (즉, 동일한 행을 나타냄). 그러나 분리 된 상태의 인스턴스에 대해서는 정의에 따라 JVM ID가 보장되지 않습니다. 개발자는 영속 클래스에서 equals () 및 hashCode () 메소드를 재정의하고 자체 객체 평등 개념을 구현해야합니다.
옵션 3에 찬성하여 계속 주장하고 있습니다.
한 가지주의 사항이 있습니다. 데이터베이스 식별자를 사용하여 동등성을 구현하지 마십시오. 고유하고 일반적으로 불변 인 속성의 조합 인 비즈니스 키를 사용하십시오. 임시 오브젝트가 지속되면 데이터베이스 ID가 변경됩니다. 임시 인스턴스 (일반적으로 분리 된 인스턴스와 함께)가 세트에 유지되는 경우 해시 코드를 변경하면 세트의 계약이 중단됩니다.
이것은 사실입니다 만약 당신이
- ID를 조기에 할당 할 수 없습니다 (예 : UUID 사용)
- 그러나 객체는 일시적인 상태에서 객체를 세트에 넣기를 원합니다.
그렇지 않으면 옵션 2를 자유롭게 선택할 수 있습니다.
그런 다음 상대적인 안정성이 필요하다고 언급합니다.
비즈니스 키의 속성은 데이터베이스 기본 키만큼 안정적 일 필요는 없습니다. 객체가 동일한 세트에있는 한 안정성 만 보장하면됩니다.
맞습니다. 내가 실제로 볼 수있는 문제는 다음과 같습니다. 절대 안정성을 보장 할 수없는 경우 “객체가 같은 세트에있는 한”안정성을 어떻게 보장 할 수 있습니까? 대화를 위해서만 세트를 사용하고 버리는 것과 같은 특별한 경우를 상상할 수는 있지만 이것의 일반적인 실행 가능성에 의문을 제기합니다.
짧은 버전 :
- 옵션 1은 단일 세션 내의 개체에만 사용할 수 있습니다.
- 가능하면 옵션 2를 사용하십시오. PK가 할당 될 때까지 개체를 세트로 사용할 수 없으므로 가능한 빨리 PK를 할당하십시오.
- 상대적 안정성을 보장 할 수있는 경우 옵션 3을 사용할 수 있습니다. 그러나이 점에주의하십시오.
답변
- 비즈니스 키 가 있으면
equals
/에 해당 키 를 사용해야합니다hashCode
. - 비즈니스 키가없는 경우 기본 키
Object
와 hashCode 구현을 그대로두면 안됩니다.이 키 는 사용자merge
와 엔터티 이후에는 작동하지 않기 때문입니다 . -
이 게시물에서 제안한 엔터티 식별자를 사용할 수 있습니다 . 유일한 문제점은 다음
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 커뮤니티 Wiki , “Hibernate를 사용한 Java Persistence”, 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의 답변에서 이미 참조) 에서 James Brundege가 제안한 애플리케이션 관리 기본 키를 사용합니다. ) 및 Lance Arlaus의 “객체 생성 : 최대 절전 모드 통합 방법” .
옵션 1의 가장 큰 문제는 분리 된 인스턴스를 .equals ()를 사용하는 영구 인스턴스와 비교할 수 없다는 것입니다. 그러나 괜찮습니다. equals와 hashCode의 계약은 개발자가 각 클래스에 대한 평등의 의미를 결정하도록합니다. 따라서 equals와 hashCode가 Object에서 상속되도록하십시오. 분리 된 인스턴스를 영구 인스턴스와 비교해야하는 경우 해당 목적에 따라 boolean sameEntity
또는 boolean dbEquivalent
또는 으로 명시 적으로 새 메소드를 작성할 수 있습니다 boolean businessEquals
.