질문 : 엔티티와 DTO , DAO 에 대한 설명
엔티티 : 데이터 베이스의 테이블을 클래스 관점에서 기술한 것 ( 객체 지향 관점 )
DTO : Data Transfer Object
DAO : Data Access Object
질문 : 삭제 ? ? ? ?
(공식 문서) 삭제 상태의 엔티티는 ID값이 있고 영속성 컨텍스트에 연결되어 있으며 데이터 베이스에서 제거되도록 예약되어 있습니다.
저게 무슨 말이지 ????
findByID로 조회는 성공하지만 값은 빼올수 없다.
질문 : 영속성 컨텍스트가 필요한 이유 ? → 데이터베이스와의 관점
영속성을 유지하기 위해서는 데이터 베이스와의 상호작용이 필요한데 이러한 작업을 효율적으로 관리하고 엔티티의 상태를 관리하는데 좋기 때문이다.
엔티티의 변경을 바로 데이터 베이스에 반영한다면 비용적 측면에서 부담이 심하다. 그렇기 때문에 변경을 추적해줄 대상이 있어 데이터 베이스에 대한 접근을 효율적으로 수행
EntityManager 인스턴스는 영속성 컨텍스트와 연결됩니다. 영속성 컨텍스트는 영구 엔터티 ID에 대해 고유한 엔터티 인스턴스가 있는 엔터티 인스턴스 집합입니다. 영속성 컨텍스트 내에서 엔터티 인스턴스와 해당 생명 주기가 관리됩니다. EntityManager API는 영구 엔터티 인스턴스를 생성 및 제거하고 기본 키로 엔터티를 찾고 엔티티를 쿼리하는 데 사용됩니다.
- 영속성 컨텍스트는 영속성이 부여된 엔티티의 생명 주기를 관리하는 엔티티 인스턴스들의 집합
- 영속성 컨텍스트는 엔티티를 식별자(@Id)로 구분
Hibernate에서는 데이터 베이스의 데이터를 로드할 때 일부분만 로드하도록 제공합니다.
- 지연로딩은 어떻게 가능한걸까??
정답은 ‘프록시’
지연 로딩은 왜 필요할까?
비용 측면에서 바라볼 수 있다.
LazyInitializationException
Member
@Entity
public class Member {
@ManyToOne
@JoinColumn(name = "team_id")
Team team;
}
Team
@Entity
public class Team {
@OneToMany(fetch = FetchType.EAGER || LAZY,mappedBy = "team")
List<Member> members = new LinkedList<>();
}
Test
Optional<Member> tmp = memberRepository.findById(1l);
findById와 getById의 차이점 ?
getById : 기본적으로 프록시 객체를 반환한다. (실제 데이터 접근이 일어날때까지 select 문은 호출되지 않는다) → getReferenceById()
findById : select문 바로 호출
tmp.get().getTeam().getName()
호출 시 ( 연관된 Team에 대한 직접 접근 시 )tmp.get().getTeam().getId()
가 호출된다면 ?나타나는 결과와 왜 그런지 ?
결과 : team을 조회하는 select문이 호출되지 않고 바로 team에 대한 Id를 가져올 수 있다.
왜 : Id를 이용한 조회시 프록시 객체가 초기화 되지 않기 때문이다.
@Override
public final Serializable getIdentifier() {
if ( isUninitialized() && isInitializeProxyWhenAccessingIdentifier() ) {
initialize();
}
return id;
}
Id 식별자 접근시 프록시를 초기화 하는 옵션을 켰을 때 true가 되는 조건문 기본적으로 false
hibernate.jpa.compliance.proxy
tmp.get().getTeam().id
는 ?
이유는
자바 빈 규약에 맞는 get + Id 형태일 경우에만 getIdentifier() 가 호출된다.
만약 findId
와 같이 getter에 대한 자바 빈 규약을 만족시키지 못하거나, getTeamId
와 같이 식별자 이름과 매칭되지 않을 경우 getIdentifier 메서드를 호출하지 못하고 프록시가 초기화
id값은 프록시가 아니라 프록시 내부의 인터셉터에 들어있고, 프록시 객체가 가진 필드값들은 모두 null이라는 것이다. 필드가 public이어서 바로 접근한다면 null에 접근하게 되는 것입니다.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Team team = (Team) o;
return Objects.equals(id, team.id);
}
@Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
Team t1 = member.get().getTeam();
Team t2 = member.get().getTeam();
System.out.println(t1.equals(t2)); // false
이유는 ?
정답은 프록시 차이 t1 객체는 equals() 메소드를 호출할 때 프록시 초기화된다. 하지만 t2 객체는 여전히 프록시 객체, 즉 getClass() 값이 다르기 때문에 둘의 동등성은 보장되지 않는다.
→ instanceOf를 사용하도록 하자
https://tecoble.techcourse.co.kr/post/2022-10-17-jpa-hibernate-proxy/
영속성 컨텍스트
에 찾는 Entity
가 이미 있으면, em.getReference()
해도 실제 Entity
가 반환된다-> JPA
는 동일 객체임을 보장하기 위해 프록시
를 먼저 조회하면 프록시 객체
로 맞추고, 아니면 실제 Entity
에 맞추는 내부로직
을 가지고 있음JPA
는 하나의 트랜잭션
에서 같은 객체
의 조회는 항상 동일 객체
를 보장한다!) @Test
public void test2()
{
Optional<Member> member = memberRepository.findById(1l);
Optional<Member> member2 = memberRepository.findById(1l);
}
위의 경우에는 캐시가 적용되지 않아 Select 문이 2번 호출된다. 이유는 ?
정답은 “트랜잭션” , 캐시의 동작 범위는 동일한 트랜잭션 내에서 지속된다. @Transactional 이 붙지 않아 2번의 조회마다 별도의 트랜잭션이 수행되기 때문이다.
@Test
@Transactional
public void test2()
{
Optional<Member> member = memberRepository.findByName("진주원");
Optional<Member> member2 = memberRepository.findById(1l);
}
private Object doLoad(
final LoadEvent event,
final EntityPersister persister,
final EntityKey keyToLoad,
final LoadEventListener.LoadType options) {
final EventSource session = event.getSession();
final boolean traceEnabled = LOG.isTraceEnabled();
if ( traceEnabled ) {
LOG.tracev(
"Attempting to resolve: {0}",
MessageHelper.infoString( persister, event.getEntityId(), session.getFactory() )
);
}
CacheEntityLoaderHelper.PersistenceContextEntry persistenceContextEntry = CacheEntityLoaderHelper.INSTANCE.loadFromSessionCache(
event,
keyToLoad,
options
); // 1차 캐시로부터 확인
Object entity = persistenceContextEntry.getEntity();
if ( entity != null ) {
return persistenceContextEntry.isManaged() ? entity : null;
}
// 2차 캐시로부터 확인
entity = CacheEntityLoaderHelper.INSTANCE.loadFromSecondLevelCache( event, persister, keyToLoad );
if ( entity != null ) {
if ( traceEnabled ) {
LOG.tracev(
"Resolved object in second-level cache: {0}",
MessageHelper.infoString( persister, event.getEntityId(), session.getFactory() )
);
}
}
else {
if ( traceEnabled ) {
LOG.tracev(
"Object not resolved in any cache: {0}",
MessageHelper.infoString( persister, event.getEntityId(), session.getFactory() )
);
}
entity = loadFromDatasource( event, persister ); // 데이터베이스로부터 조회
}
if ( entity != null && persister.hasNaturalIdentifier() ) {
final PersistenceContext persistenceContext = session.getPersistenceContextInternal();
final PersistenceContext.NaturalIdHelper naturalIdHelper = persistenceContext.getNaturalIdHelper();
naturalIdHelper.cacheNaturalIdCrossReferenceFromLoad(
persister,
event.getEntityId(),
naturalIdHelper.extractNaturalIdValues(
entity,
persister
)
);
}
return entity;
}
변화를 어떻게 감지하는 것일까?
정답은 “스냅샷”
https://www.youtube.com/watch?v=kJexMyaeHDs&t=854s
List<Person> samePersons = session
.byMultipleIds( Person.class )
.enableSessionCheck( true )
.multiLoad( 1L, 2L, 3L );
enableSessionCheck( true )
: 영속성 컨텍스트에 이미 로드된 엔터티를 건너뛰도록 Hibernate에 지시@Where(clause = "account_type = 'DEBIT'")
@OneToMany(mappedBy = "client")
private List<Account> debitAccounts = new ArrayList<>();
@Where(clause = "account_type = 'CREDIT'")
@OneToMany(mappedBy = "client")
private List<Account> creditAccounts = new ArrayList<>();
엔티티 타입 자체에도 지정할 수 있다. (대표적인 예시)
account에 대해 @Where(clause = “active = true”) 사용