컬렉션과 부가기능
코드는 모두 깃허브에 있음.
일단 자바의 컬렉션 인터페이스들의 특징부터 나열한다.
컬렉션
- Collection
- 자바가 제공하는 최상위 컬렉현,
Hibernate
는 중복을 허용하고, 순서를 보장하지 않는다고 가정
- 자바가 제공하는 최상위 컬렉현,
- Set
- 중복을 허용하지 않고, 순서도 보장하지 않는다.
- List
- 순서가 있는 컬렉션아며 중복을 허용한다.
- Map
Key
,Value
구조로 되어있는 컬렉션이다.
JPA와 Collection
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
Collection
과 List
는 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고
왜❓ - 중복을 허용하기 때문
단순히 저장만 하면 된다. 그렇기 때문에 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화 하지 않는다.
Set
Set
은 엔티티를 추가할 때 중복값을 확인하기 때문에 서로 비교를 해야한다.
그렇기 때문에 지연 로딩된 컬렉션을 초기화 한다.
List + @OrderColumn
@OrderColumn
은 DB에 순서값을 저장해서 조회할 때 사용한다는 의미
순서가 있기에 DB에 순서값도 관리하는데
단점이 있어 사용하지 않는다고 한다.
순서값을 DB가 가지고 있기 때문에 하나를 지운다고 가정하면 삭제된 List의 번호에는 null이 저장된다.NullPointerException
우려
@OrderBy
책에서 나온것처럼 특정 칼럼에 @OrderBy
를 주는 법도 있겠지만 이렇게 하지않고 대부분 Auditing 기능 오버라이드 하여 한다고 한다.
@Converter
컨버터는 단어 그대로 형 변환을 해주는 것이다.
예를들어 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}
이벤트의 종류와 발생 시점은 위의 이미지와 같다.
- PostLoad : 엔티티가 영속성 컨텍스트에 조회된 후 또는
refresh
호출한 후(2차 캐시에 저장되어 있어도 호출). - PrePersist :
persist()
를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에. 식별자 생성전략을 사용한 (이하@GeneratedValue
) 경우 엔티티에 식별자는 아직 존재하지 않는다. 또한 새로운 인스턴스를merge
할 때. - PreUpdate :
flush
나commit
을 호출해서 엔티티를 DB에 수정하기 직전 - PreRemove :
remove()
를 호출해 엔티티를 영속성 컨텍스트에서 삭제하기 직전. 영속성 전이가 일어날 때,orphanRemoval
(고아객체 관련)에 대해선flush
나commit
시에 - PostPersist :
flush
나commit
을 호출해서 엔티티를 DB에 저장한 직후 호출. 식별자 항상 존재함. 생성전략이IDENTITY
면 식별자를 생성하기 위해persist()
를 호출한 직후 바로 호출. - PostUpdate :
flush
나commit
을 호출해서 엔티티를 DB에 수정한 직후 - PostRemove :
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);
}
}
}
}
여러개의 리스너를 등록했을 때 호출순서는
- 기본 리스너
- 부모 클래스 리스너
- 리스너
- 엔티티
와 같다.
엔티티 그래프
엔티티 그래프는 엔티티를 조회하는 시점에 연관된 엔티티들을 함께 조회하는 기능이다.
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
인 관계들도 전부 포함해서 함께 조회한다.