Chapter14. 컬렉션과 부가 기능

김신영·2023년 3월 2일
0

JPA

목록 보기
11/14
post-thumbnail

Collection

JPA는 자바에서 기본으로 제공하는 Collection, List, Set, Map 컬렉션을 지원하고 다음 경우에 이 컬렉션을 사용할 수 있다.

  • @OneToMany, @ManyToMany를 사용해서 일대다나 다대다 엔티티 관계를 매핑할 때
  • @ElementCollection 을 사용해서 값 타입을 하나 이상 보관할 때
  • Map 의 경우, @MapKey를 사용해서 매핑할 수 있다.

JPA와 컬렉션

  • 하이버네이트는 엔티티를 영속 상태로 만들 때
  • 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 감싸서 사용한다.
@Entity
@Getter
public class Team {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@OneToMany
	@JoinColumn(name = "TEAM_ID")
	private Collection<Member> members = new ArrayList<>();
}

public static void main(String[] args) {
	Team team = new Team();

	System.out.println("before persist = " + team.getMembers().getClass())

	em.persist(team);

	System.out.println("after persist = " + team.getMembers().getClass())
}

// before persist = class java.util.ArrayList
// after persist = class org.hibernate.collection.internal.PersistentBag
  • 하이버네이트는 컬렉션을 효율적으로 관리하기 위해 엔티티를 영속 상래토 만들 때,
  • 원본 컬렉션을 감싸고 있는 하이버네이트 내장 컬렉션을 생성하고
  • 이 Wrapper Collection을 사용하도록 참조를 변경한다.

하이버네이트 내장 컬렉션과 특징

컬렉션 인터페이스하이버네이트 내장 컬렉션중복 허용순서 보관
Collection, ListPersistenceBag
SetPersistentSet
List + @OrderColumnPersistentList

Collection, List

  • 엔티티를 추가할 때, 중복된 엔티티가 있는지 검사하지 않는다.
  • 같은 엔티티가 있는지 찾거나 삭제할 때는 equals() 메서드를 사용
  • 따라서 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화 하지 않는다.
public class Parent {
	@Id
	@GeneratedValue
	private Long id;

	@OneToMany
	@JoinColumn
	private Collection<Child> childs = new ArrayList<>();
}

Set

  • equals(), hascode() 메서드를 함께 사용해서 비교한다.
  • 엔티티를 추가할 때 중복된 엔티티가 있는지 비교한다.
  • 따라서 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화한다.
public class Parent {
	@Id
	@GeneratedValue
	private Long id;

	@OneToMany
	@JoinColumn
	private Set<Child> childs = new HashSet<>();
}

List + @OrderColumn

  • @OrderColumn 사용 시, 순서가 있는 컬렉션으로 인식한다.
  • 데이터베이스에 순서 값을 저장해서 조회할 때 사용한다.
public class Parent {
	@Id
	@GeneratedValue
	private Long id;

	@OneToMany
	@OrderColumn(name = "INDEX")
	private Collection<Child> childs = new ArrayList<>();
}

public class Child {
	@Id
	@GeneratedValue
	private Long id;

	@ManyToOne
	@JoinColumn(name = "PARENT_ID")
	private Parent parent;
}

@OrderColumn 의 단점

  • @OrderColumn 은 Parent 엔티티에서 매핑하므로, Child는 index 값을 알 수 없다.
  • List가 변경될 시, 많은 엔티티의 index값이 변경된다. (UPDATE 쿼리 n번 실행)
  • 중간에 INDEX 값이 없으면, List에는 null 이 보관된다.

@OrderBy

  • ORDER BY 절을 사용해서 컬렉션 순서를 정렬한다.
  • 모든 컬렉션에 사용할 수 있다.
  • 하이버네이트는 Set에 @OrderBy 를 적용하면, 순서를 유지하기 위해 HashSet 대신에 LinkedHashSet을 내부에서 사용한다.
public class Team {
	@Id
	@GeneratedValue
	private Long id;

	@OneToMany
	@OrderBy("username desc, age asc")
	private Set<Member> members = new HashSet<>();
}

public class Member {
	@Id
	@GeneratedValue
	private Long id;

	private String username;

  private Integer age;

	@ManyToOne
	@JoinColumn(name = "TEAM_ID")
	private Team team;
}

@Converter

  • @Converter 를 사용하면, 엔티티의 데이터를 변환해서 데이터베이스에 저장할 수 있다.
    • boolean 값을 'Y' 혹은 'N' 문자로 저장하고 싶을 때!!!!
@Entity
public class Member {
	@Id
	private Long id;

	private String username;

	@Convert(converter = BooleanToYNConverter.class)
	private boolean vip;
}
@Converter
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {
	@Override
	public String convertToDatabaseColumn(Boolean attribute) {
	  return (attribute != null && attribute) ? "Y" : "N";
  }

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

AttributeConveter<X, Y> 인터페이스

public interface AttributeConverter<X,Y> {

    public Y convertToDatabaseColumn (X attribute);

    public X convertToEntityAttribute (Y dbData);
}

@Convert 클래스 레벨에 설정하기

  • attributeName 속성을 설정해줘야 한다.
@Entity
@Convert(converter = BooleanToYNConverter.class, attributeName = "vip")
public class Member {
	@Id
	private Long id;

	private String username;

	private boolean vip;
}

Global Converter 설정

  • @Converter(autuApply = true) 옵션을 적용하면,
  • @Convert 설정하지 않아도 자동으로 컨버터가 적용된다.
@Converter(autuApply = true)
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {
	@Override
	public String convertToDatabaseColumn(Boolean attribute) {
	  return (attribute != null && attribute) ? "Y" : "N";
  }

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

@Convert 속성 정리

속성기능기본값
converter사용할 컨버터 클래스를 지정한다.
attributeName컨버터를 적용할 클래스 멤버 필드를 지정한다.
disableConversion글로벌 컨버터나 상속받은 컨버터를 사용하지 않는다.false

EventListener

  • JPA 리스너 기능을 사용하면, 엔티티의 생명주기에 따른 이벤트를 처리할 수 있다.

번호EventEvent 시점
1PostLoad- 엔티티가 영속성 컨텍스트에 조회된 직후
- refresh를 호출한 후
2PrePersistpersist 호출 직전 (영속성 컨텍스트에 관리되기 직전)
3PreUpdateflushcommit 호출해서, 엔티티를 데이터베이스에 반영하기 직전
4PreRemove- remove 를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전
- 삭제 명령어로 영속성 전이가 일어날 때
- orphanRemoval 에 대해서 flushcommit
5PostPersistflushcommit 을 호출해서 엔티티를 데이터베이스에 저장한 직후
6PostUpdateflushcommit 을 호출해서 엔티티를 데이터베이스에 수정한 직후
7PostRemoveflushcommit 을 호출해서 엔티티를 데이터베이스에 삭제한 직후

이벤트 적용 위치

  • 엔티티에 직접 적용
  • 별도의 리스너 등록
  • 기본 리스너 사용

엔티티에 이벤트 처리 직접 적용

@Entity
@Slf4j
public class Duck {
  @Id
  @GeneratedValue
  private Long id;

  private String name;

  @PrePersist
  public void prePersist() {
		log.info("Duck.prePersist id = " + id);
  }

  @PostPersist
  public void postPersist() {
		log.info("Duck.postPersist id = " + id);
  }

  @PostLoad
  public void postLoad() {
		log.info("Duck.postLoad id = " + id);
  }

  @PreUpdate
  public void preUpdate() {
		log.info("Duck.preUpdate id = " + id);
  }

  @PostUpdate
  public void postUpdate() {
		log.info("Duck.postUpdate id = " + id);
  }

  @PreRemove
  public void preRemove() {
		log.info("Duck.preRemove id = " + id);
  }

  @PostRemove
  public void postRemove() {
		log.info("Duck.postRemove id = " + id);
  }
}

EntityListener 등록 (@EntityListeners )

  • 특정 타입이 확실하다면, 함수 매개변수에 타입을 지정할 수 있다.
@Entity
@EntityListeners(DuckListener.class)
public class Duck {
   ...
}

public class DuckListener {
  @PrePersist
  public void prePersist(Object object) {
		log.info("Duck.prePersist id = " + id);
  }

  @PostPersist
  public void postPersist(Duck duck) {
		log.info("Duck.postPersist id = " + id);
  }
  
}

기본 리스너 사용

  • META-INF/orm.xml
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm http://xmlns.jcp.org/xml/ns/persistence/orm_2_1.xsd"
                 version="2.1">
    <persistence-unit-metadata>
        <persistence-unit-defaults>
            <entity-listeners>
                <entity-listener class="org.sterl.example.EntityListener">
                    <!-- optional, annotations will work too -->
                    <pre-persist method-name="fooMethod"/> 
                </entity-listener>
            </entity-listeners>
        </persistence-unit-defaults>
    </persistence-unit-metadata>
</entity-mappings>

여러 리스너 등록시, 이벤트 호출 순서

  1. 기본 리스너
  2. 부모 클래스 리스너
  3. 리스너
  4. 엔티티

세밀한 설정

  • @ExcludeDefaultListners
    • 기본 리스너 무시
  • @ExcludeSuperclassListeners
    • 상위 클래스 엔티티 리스너 무시
profile
Hello velog!

0개의 댓글