JPA hashCode() / equals() 딜레마
JPA 엔티티와 어떤 엔티티에 대해 몇 가지 논의가 있었습니다.hashCode()
/equals()
JPA를 사용하다(전부는 아니더라도) 대부분 휴지 상태에 의존하지만, JPA 구현에 대해 중립적으로 논의하고 싶습니다(그런데 저는 Eclipse Link를 사용하고 있습니다).
가능한 모든 구현에는 다음과 같은 장점과 단점이 있습니다.
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 구현(적어도 Eclipse Link)이 제대로 작동하지 않을 수 있습니다.
업데이트 2:
답변 감사합니다.대부분의 답변은 뛰어난 품질을 갖추고 있습니다.
유감스럽게도 실제 어플리케이션에 가장 적합한 접근법이나 어플리케이션에 가장 적합한 접근법을 결정하는 방법은 아직 잘 모르겠습니다.그래서 저는 질문을 보류하고 더 많은 토론과/또는 의견을 희망합니다.
이 주제에 대한 매우 좋은 기사를 읽어보십시오.하이버네이션으로 아이덴티티를 도용하지 마세요.
이 기사의 결론은 다음과 같습니다.
오브젝트 ID는 오브젝트가 데이터베이스에 유지되는 경우 올바르게 구현하기가 매우 어렵습니다.하지만, 문제는 전적으로 오브젝트가 저장되기 전에 ID 없이 존재하도록 허용하는 것에서 비롯된다.Hibernate와 같은 객체 관계 매핑 프레임워크에서 벗어나 객체 ID를 할당함으로써 이러한 문제를 해결할 수 있습니다.대신 오브젝트가 인스턴스화되는 즉시 오브젝트 ID를 할당할 수 있습니다.이를 통해 객체 ID가 단순하고 오류가 없으며 도메인 모델에 필요한 코드 양을 줄일 수 있습니다.
항상 equals/hashcode를 덮어쓰고 비즈니스 ID를 기반으로 구현합니다.내게는 가장 합리적인 해결책인 것 같아.다음 링크를 참조하십시오.
「equals/ Code」를 과 동작하지 않는 .「 Equals/Hash Code 」 、 「 Equals/Hash Code 」
편집:
이 방법이 효과적인 이유를 설명하려면:
- JPA 어플리케이션에서는 해시 기반 컬렉션(HashMap/HashSet)을 사용하지 않습니다.필요하다면 Unique List 솔루션을 작성하는 것이 좋습니다.
- 실행 시 비즈니스 ID를 변경하는 것은 데이터베이스 애플리케이션에 적합한 방법이 아니라고 생각합니다.드물게 다른 해결책이 없는 경우에는 엘리먼트를 제거하고 해시 기반 컬렉션에 다시 넣는 등의 특별한 처리를 합니다.
- 제 모델의 경우, 저는 컨스트럭터에 비즈니스 ID를 설정하고 있으며, 설정기를 제공하지 않습니다.속성 대신 필드를 변경하기 위해 JPA 구현을 허용했습니다.
- UUID 솔루션이 과잉인 것 같습니다.비즈니스 ID가 자연스러운데 UUID를 사용하는 이유는 무엇입니까?역시 데이터베이스에 비즈니스 ID의 고유성을 설정합니다.그러면 데이터베이스의 각 테이블에 대해 3개의 인덱스를 갖는 이유는 무엇입니까?
저는 개인적으로 이 세 개의 주(州)를 모두 다른 프로젝트에 사용했습니다.그리고 저는 옵션 1이 실제 앱에서 가장 실용적이라고 생각합니다.제 경험상 hashCode()/equals() 준거가 깨지면 엔티티가 컬렉션에 추가된 후 동등한 결과가 바뀌는 상황이 발생할 때마다 많은 비정상적인 버그가 발생합니다.
하지만 추가 옵션(또한 가진 그들의 찬반 양론): 있다.
A)hashCode/equals이 아닌 null로, 생성자 할당, 불변의 필드 집합을 기반으로.
(+)모든 3criterias 보장된다.
(-)분야의 가치들이 새로운 인스턴스를 만들 수 있어야 한다.
(-)complicates. 만일 당신이 그 후에 변경해야 하습니다.
B)hashCode/equals는 응용 프로그램 대신 일본 조달청(생성자에)에 의해 배정된다 1차 열쇠에 근거한다.
(+)모든 3criterias 보장된다.
(-)당신은 DB시퀀스와 같은 간단한 믿을 만한 ID세대 위한 기능을 활용할 수 없다.
새로운 기업 OMA는 분산 환경(클라이언트/서버)또는 어플리케이션 서버 클러스터에서 형성된다(-)복잡하다.
C)hashCode/equals UniversallyUniqueIdentifier기관의 생성자에 의해 할당에 근거한다.
(+)모든 3criterias 보장된다.
UniversallyUniqueIdentifier세대의(-)오버 헤드
(-) 위험이 두번 다시 같은 UniversallyUniqueIdentifier, algorythm 사용되(DB에서 독특한 인덱스에 의해 검출될 수 있)에 따라 사용된다.
대부분 저희 단체에서:2ID 가지고 있다.
- 지속성 계층은 오직(너무 지속성 공급자 및 데이터베이스 개체 사이의 관계를 알아낼 수 있을)이 하는 짓이다
- 입니다(<고객명> 「 ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★」.
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
디폴트 컨스트럭터와 세터를 사용하고 있습니다만, 2개의 컨스트럭터로 이루어진 어프로치가 고객에게 더 적합할 수 있습니다.
「 」를 사용하고 equals()/hashCode()
집합의 경우 동일한 엔티티가 한 번만 존재할 수 있다는 점에서 다음과 같은 옵션이 있습니다.옵션 2이는 엔티티의 기본 키가 정의상 변경되지 않기 때문입니다(누군가가 실제로 업데이트하면 더 이상 동일한 엔티티가 아닙니다).
그대로 받아 요. '당신의 ' 이니까.equals()/hashCode()
는 프라이머리 키를 기반으로 합니다.프라이머리 키가 설정될 때까지는, 이러한 방법을 사용하지 말아 주세요.따라서 기본 키가 할당될 때까지 엔티티를 세트에 넣지 마십시오(예, UUID 및 유사한 개념이 기본 키를 조기에 할당하는 데 도움이 될 수 있습니다).
이론적으로 옵션 3을 사용하면 "비즈니스 키"가 변경될 수 있는 심각한 단점이 있지만 "이미 삽입된 엔티티를 세트에서 삭제하고 다시 삽입하기만 하면 됩니다."라는 단점이 있습니다.이는 사실이지만 분산형 시스템에서는 데이터가 삽입된 모든 장소에서 이 작업이 수행되어야 한다는 것을 의미합니다(다른 일이 발생하기 전에 업데이트가 수행되어야 합니다).고도의 업데이트 메커니즘이 필요합니다.특히 일부 리모트 시스템에 현재 접속할 수 없는 경우에는...
옵션 1은 세트의 모든 개체가 동일한 휴지 상태 세션에 있는 경우에만 사용할 수 있습니다.휴지 상태 문서에서는 13.1.3장에서 이를 명확히 하고 있습니다. 오브젝트 ID 고려:
세션 내에서 애플리케이션은 안전하게 ==를 사용하여 개체를 비교할 수 있습니다.
그러나 세션 외부에서 ==를 사용하는 애플리케이션은 예상치 못한 결과를 초래할 수 있습니다.이것은 예기치 않은 장소에서도 발생할 수 있습니다.예를 들어 두 개의 분리된 인스턴스를 동일한 세트에 배치하면 두 인스턴스가 모두 동일한 데이터베이스 ID를 가질 수 있습니다(즉, 두 인스턴스는 동일한 행을 나타냅니다).그러나 JVM ID는 분리된 상태의 인스턴스에는 보장되지 않습니다.개발자는 영속적인 클래스에서 equals() 메서드와 hashCode() 메서드를 덮어쓰고 오브젝트 평등에 대한 자신의 개념을 구현해야 합니다.
옵션 3을 계속 지지하고 있습니다.
단, 데이터베이스 ID를 사용하여 동등성을 구현하지 마십시오.일반적으로 불변의 고유한 속성을 조합한 비즈니스 키를 사용합니다.일시적인 오브젝트가 영속적인 경우 데이터베이스 ID가 변경됩니다.임시 인스턴스(일반적으로 분리된 인스턴스와 함께)가 세트에 보관되어 있는 경우 해시 코드를 변경하면 세트의 계약이 해제됩니다.
이 말은 사실이라면
- ID를 조기에 할당할 수 없습니다(예: UUID 사용).
- 단, 일시적인 상태일 때는 반드시 오브젝트를 세트로 정리해야 합니다.
그렇지 않으면 옵션 2를 자유롭게 선택할 수 있습니다.
다음으로 상대적인 안정성의 필요성을 언급합니다.
비즈니스 키의 속성은 데이터베이스 기본 키만큼 안정적일 필요가 없습니다. 개체가 동일한 세트에 있는 경우에만 안정성을 보장해야 합니다.
이게 맞아요.여기서 볼 수 있는 현실적인 문제는 다음과 같습니다.절대 안정성을 보장할 수 없는 경우, "물체가 동일한 세트에 있는 한" 안정성을 보장할 수 있는 방법은 무엇입니까?특별한 경우(대화를 위해서만 세트를 사용한 후 버리는 경우 등)를 생각할 수 있지만, 일반적인 실용성에 의문을 가지고 있습니다.
쇼트 버전:
- 옵션 1은 단일 세션 내의 개체에만 사용할 수 있습니다.
- 가능한 경우 옵션 2를 사용합니다(PK가 할당될 때까지 개체를 집합으로 사용할 수 없으므로 가능한 빨리 PK를 할당하십시오).
- 상대적 안정성을 보장할 수 있는 경우 옵션 3을 사용할 수 있습니다.하지만 이것 조심하세요.
키를 계신 키를 .
equals
및 를 참조해 주세요.키가 안 .
Object
를 사용한 후에는 동작하지 않기 때문에 등가 및 실장.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 getClass().hashCode(); } //Getters and setters omitted for brevity }
GitHub에서 이 솔루션이 마법처럼 작동함을 증명하는 테스트 케이스를 확인해 보십시오.
비즈니스 키(옵션 3)를 사용하는 것이 가장 일반적으로 권장되는 접근법(Hibernate community wiki, "Java Persistence with Hibernate" p.398)이지만, 이것이 우리가 주로 사용하는 방법이지만, Hibernate 버그인 HH-3799가 있습니다.이 경우 휴지 상태에서는 필드가 초기화되기 전에 엔티티를 세트에 추가할 수 있습니다.이 버그는 비즈니스 키 권장 접근법에 문제가 있기 때문에 왜 주목을 받지 않았는지 모르겠습니다.
이 문제의 핵심은 동일하고 hashCode는 불변의 상태(참조 Odersky 등)에 기반해야 하며, 휴지 상태 관리 프라이머리 키를 가진 휴지 상태 엔티티는 이러한 불변의 상태가 없다고 생각합니다.프라이머리 키는 일시적인 오브젝트가 영속적으로 되면 휴지 상태에 의해 변경됩니다.비즈니스 키도 초기화 과정에서 오브젝트에 수분을 공급하면 휴지 상태에 의해 변경됩니다.
그러면 java.lang을 상속하는 옵션1만 남습니다오브젝트 ID를 기반으로 한 오브젝트 구현 또는 "Don't Hibernate Steak Your Identity"(Stijn Geukens의 답변에서 이미 참조)의 James Brundege와 "Object Generation: (오브젝트 생성:)"의 Lance Arlaus가 제안한 애플리케이션 관리 프라이머리 키를 사용합니다. 하이버네이트 통합을 위한 더 나은 접근법"을 참조하십시오.
옵션 1의 가장 큰 문제는 분리된 인스턴스를 .equals()를 사용하여 영구 인스턴스와 비교할 수 없다는 것입니다.하지만 괜찮습니다. equals와 hashCode의 계약은 각 클래스에 대해 평등이 무엇을 의미하는지 결정하는 것은 개발자에게 달려 있습니다.「hash Code 」는 「hash Code 」입니다.인스턴스와 가 있는 그 목적을 를 명시적으로 할 수 있습니다.를 들어, 「 」는 「 」의 「 」의 「 」를 참조해 주세요.boolean sameEntity
★★★★★★★★★★★★★★★★★」boolean dbEquivalent
★★★★★★★★★★★★★★★★★」boolean businessEquals
.
나는 앤드류의 대답에 동의한다.어플리케이션에서도 같은 작업을 하지만 UUID를 VARCHAR/CHAR로 저장하는 대신 UUID를 2개의 긴 값으로 분할했습니다.UUID.getLestSignificantBits() 및 UUID.getMostSignificantBits()를 참조하십시오.
또 하나 고려해야 할 것은 UUID.random에 대한 콜입니다.UUID()는 매우 느리기 때문에 지속성이나 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();
}
}
자카르타 퍼시스텐스 3.0, 섹션 4.12에는 다음과 같이 기술되어 있습니다.
동일한 추상 스키마 유형의 두 엔티티는 동일한 기본 키 값을 가진 경우에만 동일합니다.
Java 코드가 다르게 동작해야 할 이유는 없습니다.
아직 경우)인 경우 할 수 메서드는 으로는 "이러한"을 암묵적으로 .이러한 메서드는 이상적으로는 다음과 같이 암묵적으로 확장해야 합니다.NullPointerException
를를를 를를 를를 를를 를를 를를 。어느 쪽이든 애플리케이션 코드가 비관리 엔티티를 해시 기반 데이터 구조에 넣는 것을 효과적으로 방지합니다.예: ", ", ", ", ", " 등)이 있는 경우에는 한 더 확대해 요?version
IllegalStateException
되는 옵션입니다 결정론적 방법으로 fail-fast를 실행하는 것이 항상 권장되는 옵션입니다.
주의사항:폭파 동작도 기록합니다.문서화는 그 자체로도 중요하지만, 장래에 후배 개발자가 당신의 코드로 바보 같은 짓을 하는 것을 막을 수 있기를 바랍니다(Null Pointer를 억제하는 경향이 있습니다).그 일이 일어난 장소와 그들의 마지막 생각은 부작용이다.(웃음)
오, 그리고 항상 사용getClass()
instanceof
에는 symmetryequals-method가 ifb
a
, , , 「 」a
must must must must must must must must must must 와 같아야 합니다.b
의 경우, ★★★★★★★★★★★★★★★★★★★★★★★」instanceof
를 a
의 인스턴스가 아닙니다.b
를 참조해 주세요.
는 개인적으로 ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★.getClass()
non-time 클래스(유형은 state이기 때문에 서브클래스가 비어 있거나 동작만 포함되어 있어도 서브클래스는 스테이트를 추가합니다)를 실장하고 있는 경우에서도,instanceof
마지막 수업이었으면 좋았을 텐데그러나 엔티티 클래스는 최종적인 것이 아니므로(2.1파운드) 선택지가 없습니다.
않을 도 있다.getClass()
지속성 공급자의 프록시가 개체를 래핑하기 때문입니다.이것은 과거에 문제가 되었을지도 모르지만, 정말로 문제가 되어서는 안 된다.프로바이더가 다른 엔티티에 대해 다른 프록시 클래스를 반환하지 않는 것은 그다지 현명한 프로바이더는 아닙니다(웃음).일반적으로 문제가 생기기 전에는 문제를 풀지 말아야 한다.동면하다 우아하게 요.getClass()
(이것을 참조해 주세요).
마지막으로 엔티티인 엔티티 서브클래스가 있고 사용되는 상속 매핑 전략이 기본값("단일 테이블")이 아니라 "결합된 서브타입"으로 구성된 경우 해당 서브클래스 테이블의 프라이머리 키는 슈퍼클래스 테이블과 동일합니다.매핑 전략이 "구체 클래스별 테이블"인 경우 기본 키는 슈퍼 클래스와 같을 수 있습니다.엔티티 서브클래스는 상태를 추가할 가능성이 매우 높기 때문에 논리적으로도 다를 수 있습니다.그러나 동등한 구현에서는instanceof
는 반드시 ID에만 의존할 수 있는 것은 아닙니다.다양한 엔티티에 대해서도 마찬가지일 수 있습니다.
제 생각에는instanceof
최종적이지 않은 Java 클래스에서 전혀 설 자리가 없습니다.이는 특히 영속적인 엔티티에 해당됩니다.
여기에는 이미 매우 유익한 답변들이 있지만 우리가 무엇을 하는지 알려줄게.
아무것도 하지 않습니다(즉, 덮어쓰지 않습니다).
컬렉션에 대해 equals/hashcode가 필요한 경우 UUID를 사용합니다.UUID를 생성하기만 하면 됩니다.UUID에는 http://wiki.fasterxml.com/JugHome 를 사용합니다.UUID는 CPU 사용률이 다소 높지만 직렬화 및 DB 액세스에 비해 저렴합니다.
미리 정의된 유형 식별자와 ID에 따라 다음 방법을 고려하십시오.
JPA의 구체적인 전제 조건은 다음과 같습니다.
- 동일한 "유형"과 같은 Null이 아닌 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());
}
주요 장점은 다음과 같습니다.
- 심플함
- 서브클래스가 타입 ID를 제공하는 것을 보증합니다.
- 프록시 클래스에서의 예측 동작
단점:
- 각 엔티티에 문의할 필요가 있습니다.
super()
주의:
- 상속 사용 시 주의가 필요합니다.예: 인스턴스 동일성
class A
그리고.class B extends A
출원의 구체적인 세부 사항에 따라 달라질 수 있습니다. - ID로 비즈니스 키를 사용하는 것이 이상적입니다.
당신의 의견을 기다리겠습니다.
저는 지금까지 항상 옵션 1을 사용해 왔습니다.왜냐하면 이러한 논의를 알고 있기 때문에 올바른 방법을 알 때까지 아무것도 하지 않는 것이 좋다고 생각했기 때문입니다.그 시스템은 모두 정상적으로 가동되고 있습니다.
그러나 다음 번에는 옵션 2 - 데이터베이스에서 생성된 ID를 사용해 보겠습니다.
id가 설정되어 있지 않은 경우 해시 코드 및 동등 값은 IlgalStateException을 슬로우합니다.
이렇게 하면 저장되지 않은 엔티티에 관련된 미세한 오류가 예기치 않게 나타나는 것을 방지할 수 있습니다.
사람들은 이 접근법에 대해 어떻게 생각하나요?
비즈니스 키 접근 방식은 우리에게 맞지 않습니다.딜레마를 해결하기 위해 DB 생성 ID, 임시 임시 tempId 및 override equal()/hashcode()를 사용합니다.모든 엔티티는 엔티티의 하위 항목입니다.장점:
- DB에 추가 필드 없음
- 하위 엔티티에 추가 코딩이 없으며 모든 엔티티에 대해 단일 접근 방식 사용
- 성능 문제 없음(UUID 등), DB ID 생성
- 해시맵에는 문제가 없습니다(equal 등의 사용은 염두에 둘 필요가 없습니다).
- 새 엔티티의 해시 코드가 지속된 후에도 시간에 따라 변경되지 않음
단점:
- 지속되지 않는 엔티티의 직렬화 및 직렬화 해제에 문제가 있을 수 있습니다.
- DB에서 다시 로드한 후 저장된 엔티티의 해시 코드가 변경될 수 있습니다.
- 영구 객체는 항상 다른 것으로 간주되지 않습니다(이것이 맞을 수 있습니다).
- 물론이지.
델의 코드를 봐주세요.
@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();
}
}
IMO에는 equals/hash Code를 구현하기 위한 3가지 옵션이 있습니다.
- 응용 프로그램에서 생성된 ID(UUID) 사용
- 비즈니스 키를 기반으로 구현
- 프라이머리 키를 기반으로 구현
애플리케이션 생성 ID를 사용하는 것이 가장 쉬운 방법이지만 몇 가지 단점이 있습니다.
- 128비트가 단순히 32비트 또는 64비트보다 크기 때문에 PK로 사용할 경우 조인 속도가 느립니다.
- "디버깅이 더 어렵다" 왜냐하면 일부 데이터가 정확할수록 눈으로 확인하는 것은 매우 어렵다.
이러한 단점에 대처할 수 있는 경우는, 이 방법을 사용해 주세요.
Join 문제를 해결하려면 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 접근 방식을 사용합니다.
이는 Java 및 JPA를 사용하는 모든 IT 시스템에서 공통적인 문제입니다.이 문제는 equals()와 hashCode()를 구현하는 것 이상으로 조직이 엔티티를 참조하는 방법 및 클라이언트가 동일한 엔티티를 참조하는 방법에 영향을 미칩니다.비즈니스 키가 없는 것에 대한 고통은 이제 충분히 겪었고, 제 의견을 표현하기 위해 제 블로그를 썼습니다.
즉, RAM 이외의 스토리지에 의존하지 않고 생성되는 비즈니스 키로서 의미 있는 프레픽스를 가지는, 사람이 판독할 수 있는 짧은 시퀀셜 ID 를 사용합니다.트위터의 Snowflake는 매우 좋은 예이다.
UUID가 많은 사람들에게 해답이라면 비즈니스 계층의 공장 방식을 사용하여 엔티티를 생성하고 생성 시 기본 키를 할당하면 어떨까요?
예를 들어 다음과 같습니다.
@ManagedBean
public class MyCarFacade {
public Car createCar(){
Car car = new Car();
em.persist(car);
return car;
}
}
이렇게 하면 퍼시스텐스 프로바이더로부터 엔티티의 기본 프라이머리 키를 얻을 수 있으며, hashCode() 함수 및 equals() 함수는 이에 의존할 수 있습니다.
또한 자동차의 컨스트럭터가 보호되고 있다고 선언하고 비즈니스 방법에 반영을 사용하여 액세스할 수도 있습니다.이러한 방식으로 개발자들은 새로운 제품으로 자동차를 인스턴스화하려는 것이 아니라 공장 방식을 통해 구현하고자 할 것입니다.
저건 어때?
저는 이 질문에 답하려고 노력했고, 이 게시물, 특히 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(프라이머리 키)가 가장 자주 사용되는 것 같습니다.자연스럽고 불변의 비즈니스 키는 거의 없습니다.합성 키를 만들고 지원하는 것은 너무 무거워서 문제를 해결할 수 없습니다.아마도 그런 일은 일어나지 않을 것입니다.spring-data-jpa Abstract Persistable 구현에 대해 살펴봅니다(단, 휴지 상태 구현의 경우 사용).
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가 조언한 바와 같이 일정하게 한다.
다음은 Scala를 위한 심플한(테스트 완료) 솔루션입니다.
이 솔루션은 질문에 제시된 세 가지 범주 모두에 해당되지 않습니다.
모든 내 사업체는 UUIDEntity의 하위 클래스이기 때문에 나는 Don't-repeat-Yourself(DRY) 원칙을 따릅니다.
필요에 따라 (더 많은 의사 난수를 사용하여) 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
'IT이야기' 카테고리의 다른 글
Java JIT는 JDK 코드를 실행할 때 부정행위를 합니까? (0) | 2022.06.02 |
---|---|
'구조나 조합이 아닌 조합원 요청'은 무슨 뜻입니까? (0) | 2022.06.02 |
라우터 링크로 vue-multicelect를 채우다 (0) | 2022.06.02 |
Vue에서 향후 소품 데이터 또는 소품 디폴트 전달 방법에 따라 결정 (0) | 2022.06.02 |
클릭 시 vue js get html5 특성 (0) | 2022.06.02 |