14장 컬렉션과 부가 기능

컬렉션

JPA 는 Collection, Map, Set, List 등 의 컬렉션을 지원한다. 그리고 다음과 같은 경우에 컬렉션을 사용할수있다.

  • @OneToMany, @ManyToMany 를 사용해서 1:N, N:N 관계를 매핑할때

  • @ElementCollection 을 사용해서 값 타입을 하나 이상 보관할때

자바 컬렉션 특징

  1. Collection : 자바가 제공하는 최상위 컬렉션

  2. Map : key : value 값으로 되어있고 Key는 중복이 없어야 하지만 value는 중복가능

  3. Set : 중복 허용 x, 순서 보장 x

  4. List : 중복 허용 o, 순서보장 o

래퍼 컬렉션

하이버네이트는 엔티티를 영속상태로 만들 때 컬렉션 필드를 하이버네이트 에서 준비한 컬렉션으로 감싸서 사용한다. 원본 컬렉션을 감싸고 있는 내장 컬렉션을 생성해서 내장 컬렉션을 사용하도록 참조를 변경한다. 그래서 원본을 내장 컬렉션이 감싸고있어서 래퍼 컬렉션이라고도 부른다.

Collection, List

이 둘은 엔티티를 추가 할때 따로 중복검사를 진행하지 않고 저장된다. 엔티티 추가 시 지연로딩 컬렉션 초기화도 하지 않는다.

Set

add() 메서드로 객체를 추가할 때 마다 equals() 메서드로 동일한 객체가 있는지 비교함. 없으면 true

있으면 false를 반환한다. HashSet은 해시 알고리즘을 사용하여 hascode() 도 함께 사용해서 비교함

List + @OrderColumn

List 에다 @OrderColumn 을 추가 한다면 순서가 있는 컬렉션으로 인식한다.

기본 컬럼 외에 새로운 컬럼에 순서값을 저장해서 그것으로 조회한다.

단점

  1. 주인 엔티티는 postion 값을 모르기때문에 INSERT 시 Position 값이 저장이 안되서 해당 position을 조회해서 업데이트 하는 쿼리가 한번 더 날라감

  2. List 가 변경되면 바꿔야 하는 부분이 너무 많다. position 1번이 빠지면 모든 position이 하나씩 줄어들어야 하기 때문에 UPDATE SQL 을 몇번이나 날려야 한다.

  3. 중간에 POSITION 값이 없으면 List 에 NULL 이 보관되고 해당 부분을 삭제하거나 수정하지 않으면 순회할때 NullPointerException 발생한다.

@OrderBy

OrderBy 는 DB 의 ORDER BY 절을 사용해서 컬렉션을 정렬한다. 그렇기에 List 뿐 아니라 다른 모든 컬렉션에 사용할수있다.

@Converter

엔티티의 데이터를 변환해서 DB 에 저장 할 수 있다.

@Entity
public class Member {
	@Id
	private String id;
	private String username;

	@Convert(converter=BooleanToYNConverter.class)
	private boolean vip;

	...
}

@Converter
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {
	
	@Override
	public String convertToDatebaseColumn(Boolean attribute) {
		return (attribute != null && attribute) ? "Y" : "N";
	}

	@Override
	public Boolean convertToEntityAttribute(String dbData) {
		return "Y".equals(dbData);
	}
}
public interface AttributeConverter<X,Y> {
	public Y convertToDatabaseColumn (X attribute);
	public X convertToEntityAttribute (Y dbData);
}

  • convertToDatabaseColumn()

엔티티의 데이터를 DB 컬럼에 저장할 데이터로 변환한다.

  • convertToEntityAttribute()

DB 에서 조회한 데이터를 엔티티 데이터로 변환한다.

컨버터는 컬럼 레벨 에서 뿐 아니라 클래스 레벨 에서도 선언할 수 있다.

또한 글로벌 설정도 가능하여서 모든 Boolean 타입에 컨버터를 적용할수있다.

@Converter(autoApply = true)

@Converter(autoApply = true)
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {
	
	@Override
	public String convertToDatebaseColumn(Boolean attribute) {
		return (attribute != null && attribute) ? "Y" : "N";
	}

	@Override
	public Boolean convertToEntityAttribute(String dbData) {
		return "Y".equals(dbData);
	}
}

로컬에서 실행해보니 false = NO, true = YES 로 잘 저장 되었다.

다시 DB에서 불러낼때도 false 값으로 잘 나온다.

리스너

엔티티의 생명주기에 따른 이벤트를 처리할 수 있다.

  1. PostLoad : 엔티티가 영속성 컨텍스트에 조회된 직후 또는 refresh 를 호출한 후 시점에 호출

  2. PrePersist : 엔티티가 persist() 메서드에 호출 되어 영속성 컨텍스트의 관리를 받기 직전 시점에 호출

  3. PreUpdate : 엔티티가 업데이트 직전 commit, flush 하기 직전 시점에 호출

  4. PreRemove : remove() 메서드 호출해서 엔티티를 영속성 컨텍스트에서 삭제 하기 직전 시점에 호출또한 삭제 명렁어로 영속성 전이가 일어날 때도 호출된다. orphanremoval 에서는 flush. commit 시에 호출 된다.

  5. PostPersist : flush. commit 을 호출한 후 엔티티가 DB에 저장 된 직후 호출된다. 하지만 만약 식별자 생성 전략이 IDENTITY 면 식별자(ID값)를 생성하기 위해 persist() 를 호출하면서 DB에 엔티티를 저장한다. 이때는 persist()를 호출한 직후가 호출 시점이 된다.

  6. PostUpdate : flush. commit 호출해서 엔티티를 DB에서 수정한 직후에 호출된다.

  7. PostRemove : flush. commit 을 호출해서 엔티티를 DB에서 삭제한 직후에 호출된다.

이벤트 적용 위치

  1. 엔티티에 직접 적용

@Entity
public class Product {

    @Id
    @GeneratedValue
    public Long id;
    private String name;

    @PrePersist
    public void prePersist() {
        System.out.println("prePersist id = " + id);
    }
  1. 별도의 리스너를 작성해서 사용

리스너 클래스를 만들어서 여러개 리스너 메서드를 조합해서 원하는 엔티티에 선언해주면 된다.

public class ProductListener {

    @PrePersist
    public void prePersist(Product product) {
        System.out.println("id = " + product.getId());
    }

    @PostPersist
    public void postPersist(Product product) {
        System.out.println("id = " + product.getId());
    }

}

@Entity
@EntityListeners(ProductListener.class)
public class Product {
    @Id
    @GeneratedValue
    public Long id;
    private String name;
  1. 기본 리스너를 사용

모든 엔티티에 적용되는 방식이다. xml 형태로 META-INF/orm.xml 에 작성해주면 된다.

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings ...>
	<persistence-unit-metadata>
		<persistence-unit-defaults>
			<entity-listeners>
				<entity-listener class="jpabook.jpashop.domain.
					test.listener.DefaultListener" />
			</entity-listeners>
		</persistence-unit-defaults>
	<persistence-unit-metadata>

</entity-mappings>

아래에 어노테이션으로 더 세밀하게 설정할수있다.

  • @ExcludeDefaultListeners : 기본 리스너 무시

  • @ExcludeSuperclassListeners : 상위 클래스 이벤트 리스너 무시

엔티티 그래프 (Entity Graph)

JPA에서 제공하는 페치조인을 좀 더 편하게 사용할수있는 기능이다.

사용 방법

  1. Repository 단에 어노테이션으로 작성

해당 Member 엔티티는 Board(게시판) 와 연관관계에 놓여있다.

findAll 을 통해 모든 멤버들을 리스트로 찾고 해당 멤버들이 작성한 게시글 까지 불러온다.

어노테이션을 통해 선언해주고 attributePaths 옵션에 연관관계에 놓인 불러올 엔티티를 적어주었다.''

+ 아래 처럼 @Query 를 같이 사용해도 된다.

Before

select
        member0_.id as id1_1_,
        member0_.city as city2_1_,
        member0_.detail_address as detail_a3_1_,
      .
      .
      .
    from
        member member0_ limit ?
        

    select
        count(member0_.id) as col_0_0_ 
    from
        member member0_
Hibernate: 
    select
        board0_.member_id as member_i4_0_1_,
        board0_.id as id1_0_1_,
  .
  .
  . 
    from
        board board0_ 
    where
        board0_.member_id in (
            ?, ?, ?, ?, ?
        )

After

select
        member0_.id as id1_1_0_,
        board1_.id as id1_0_1_,
        member0_.city as city2_1_0_,
      .
      .
      .
    from
        member member0_ 
    left outer join
        board board1_ 
            on member0_.id=board1_.member_id

Last updated