코드는 모두 깃허브에 있음.
일단 자바의 컬렉션 인터페이스들의 특징부터 나열한다.
Hibernate
는 중복을 허용하고, 순서를 보장하지 않는다고 가정Key
, Value
구조로 되어있는 컬렉션이다.Hibernate
는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 Hibernate
가
준비한 컬렉션으로 감싸서 사용한다.
다음 예시를 보자
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
@OneToMany
private Collection<Member> members = new ArrayList<>();
}
@DataJpaTest
class TeamTest {
@PersistenceUnit
EntityManagerFactory emf;
EntityManager em;
EntityTransaction tx;
@BeforeEach
void setUp() {
em = emf.createEntityManager();
tx = em.getTransaction();
tx.begin();
}
@Test
void 컬렉션_테스트() {
Team team = new Team();
System.out.println(team.getMembers().getClass());
em.persist(team);
System.out.println(team.getMembers().getClass());
}
}
테스트코드 지만 단순히 이 결과를 확인하기 위해서 콘솔 출력을 진행하였다.
결과는 이렇게 나온다.
{: text-center}
처음 객체를 포장할때는 Team
엔티티 클래스에서 명시한 ArrayList로 포장을 하는데 엔티티를 영속상태로 바꿔주는 순간 PersistentBag
으로 변경된다.
Hibernate
는 컬렉션을 효율적으로 사용하려고 영속상태로 만들때 원본의 컬렉션을
감싼 내장 컬렉션을 생성하여 이 감싼 내장 컬렉션을 사용하도록 참조를 변경한다.
그렇기 때문에 컬렉션을 사용하려면 즉시 초기화를 해주고 사용하는걸 권장한다.
다음은 Hibernate
의 내장 컬렉션들과 특징이다.
컬렉션 | 내장컬렉션 | 중복 | 순서 |
---|---|---|---|
Collection , List | PersistentBag | O | X |
Set | PersistentSet | X | X |
List + @OrderColumn | PersistentList | O | O |
Collection
과 List
는 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고
왜❓ - 중복을 허용하기 때문
단순히 저장만 하면 된다. 그렇기 때문에 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화 하지 않는다.
Set
은 엔티티를 추가할 때 중복값을 확인하기 때문에 서로 비교를 해야한다.
그렇기 때문에 지연 로딩된 컬렉션을 초기화 한다.
@OrderColumn
은 DB에 순서값을 저장해서 조회할 때 사용한다는 의미
순서가 있기에 DB에 순서값도 관리하는데
단점이 있어 사용하지 않는다고 한다.
순서값을 DB가 가지고 있기 때문에 하나를 지운다고 가정하면 삭제된 List의 번호에는 null이 저장된다.
NullPointerException
우려
책에서 나온것처럼 특정 칼럼에 @OrderBy
를 주는 법도 있겠지만 이렇게 하지않고 대부분 Auditing 기능 오버라이드 하여 한다고 한다.
컨버터는 단어 그대로 형 변환을 해주는 것이다.
예를들어 boolean 타입은 DB에 저장될 때 0과 1로 저장이 된다. 대신에 Y나 N으로 저장하고 싶다면
컨버터를 사용하면 된다.
@Converter
public class BooleanYNConverter implements AttributeConverter<Boolean, String> {
@Override
public String convertToDatabaseColumn(Boolean attribute) {
return (attribute != null && attribute) ? "Y" : "N";
}
@Override
public Boolean convertToEntityAttribute(String dbData) {
return "Y".equals(dbData);
}
}
이렇게 AttributeConverter
를 구현해주고 @Converter
를 명시해준다.
//방법 1
@Convert(converter = BooleanYNConverter.class, attributeName = "적용할 변수")
public class Test {
//방법 2
@Convert(converter = BooleanYNConverter.class)
private boolean 변수명;
}
이렇게 있다. 그리고 추가로 모든 boolean에 대해서 적용을 시켜준다면
클래스최상단에 @Converter(autoApply = true)
를 주면 된다.
JPA 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트 처리 가능
{: text-center}
이벤트의 종류와 발생 시점은 위의 이미지와 같다.
refresh
호출한 후(2차 캐시에 저장되어 있어도 호출).persist()
를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에. 식별자 생성전략을 사용한 (이하 @GeneratedValue
) 경우 엔티티에 식별자는 아직 존재하지 않는다. 또한 새로운 인스턴스를 merge
할 때.flush
나 commit
을 호출해서 엔티티를 DB에 수정하기 직전remove()
를 호출해 엔티티를 영속성 컨텍스트에서 삭제하기 직전. 영속성 전이가 일어날 때, orphanRemoval
(고아객체 관련)에 대해선 flush
나 commit
시에 flush
나 commit
을 호출해서 엔티티를 DB에 저장한 직후 호출. 식별자 항상 존재함. 생성전략이 IDENTITY
면 식별자를 생성하기 위해 persist()
를 호출한 직후 바로 호출.flush
나 commit
을 호출해서 엔티티를 DB에 수정한 직후flush
나 commit
을 호출 엔티티를 DB에 삭제한 직후적용 위치는 3가지이다.
@Entity
public class Entity {
@Id @GeneratedValue
private Long id;
...
//아래로 쭉 구현
@PrePersist
public void prePersist() {
...
}
@PostPersist
public void postPersist() {
...
}
...
}
이거는 JPA Auditing 생각해보면 될거같다. 결국 AuditingEntityListener 이 리스너도 안에 어노테이션으로 아래와 같이 구현되어있다.
@Configurable
public class AuditingEntityListener {
private @Nullable ObjectFactory<AuditingHandler> handler;
public void setAuditingHandler(ObjectFactory<AuditingHandler> auditingHandler) {
Assert.notNull(auditingHandler, "AuditingHandler must not be null!");
this.handler = auditingHandler;
}
@PrePersist
public void touchForCreate(Object target) {
Assert.notNull(target, "Entity must not be null!");
if (handler != null) {
AuditingHandler object = handler.getObject();
if (object != null) {
object.markCreated(target);
}
}
}
@PreUpdate
public void touchForUpdate(Object target) {
Assert.notNull(target, "Entity must not be null!");
if (handler != null) {
AuditingHandler object = handler.getObject();
if (object != null) {
object.markModified(target);
}
}
}
}
여러개의 리스너를 등록했을 때 호출순서는
1. 기본 리스너
2. 부모 클래스 리스너
3. 리스너
4. 엔티티
와 같다.
엔티티 그래프는 엔티티를 조회하는 시점에 연관된 엔티티들을 함께 조회하는 기능이다.
Named 엔티티 그래프는 Named쿼리 자체의 빈도수가 낮기때문에 다루지 않겠다.
EntityGraph<Team> graph = em.createEntityGraph(Team.class);
graph.addAttributeNodes("속성");
JPAQuery<Emp> query = queryFactory.selectFrom(Q클래스).where(조건);
query = query.setHint("javax.persistence.fetchgraph", graph);
query.fetchOne();
이렇게 엔티티 그래프를 정의하고 Hint
로 그래프를 넣어주면 되는 방식이다.
엔티티 그래프는 항상 조회하는 엔티티의 ROOT경로에서 시작해야 한다.
만약 Member엔티티에 Team이 포함되어 있다면 Member조회 후 Team으로 가야되는데 역으로 갈 수는 없다.
영속성 컨텍스트에 엔티티가 이미 로딩되어 있다면 엔티티 그래프 적용 ❌
fetchgraph와 loadgraph의 차이는 loadgraph는 엔티티 그래프의 설정한 속성과 함께 글로벌 페치전략이 FetchType.EAGER
인 관계들도 전부 포함해서 함께 조회한다.