자바 ORM 표준 JPA 프로그래밍 - 기본편 - sec09
출처 : JPA 기본편
엔티티 타입
• @Entity로 정의하는 객체
• 데이터가 변해도 식별자로 지속해서 추적 가능
• ex) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
값 타입
• int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
• 식별자가 없고 값만 있으므로 변경시 추적 불가
• ex) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
생명주기를 엔티티의 의존 ex) 회원을 삭제하면 이름, 나이 필드도 함께 삭제
값 타입은 공유하면 ❌ ex) 회원 이름 변경시 다른 회원의 이름도 함께 변경되면 안됨
참고: 자바의 기본 타입은 절대 공유❌
• int, double 같은 기본 타입(primitive type)은 절대 공유❌
• 기본 타입은 항상 값을 복사함
• Integer같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경❌
Period.isWork()
처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있음
값 타입 말고도 entity를 가질 수 있음
아주 만약에 한 엔티티에서 같은 값 타입을 사용하면 어떻게 될까?
@Embedded
private Address homeAddress;
@Embedded
private Address workAddress;
컬럼 명이 중복됨!
=> @AttributeOverrides
, @AttributeOverride
를 사용해서 컬러 명 속성을 재정의해야함!
임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null!!
값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념, 즉 값 타입은 단순하고 안전하게 다룰 수 있어야 함
임베디드 타입 같은 값 타입을 여러 엔티티에서 공유가능 하지만! 위험함 => 부작용 발생!
Address address = new Address("city", "street", "10000");
//이걸 멤버1에서도 쓰고
member2.setHomeAddress(address); //멤버 2에서도 쓰고
값을 한쪽만 변경한다고는 하지만 둘다 변경되는 대참사 발생!
그래서! 대신 값(인스턴스)를 복사해서 사용해야 함!
Address address = new Address("city", "street", "10000");
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
생성 시점 이후 절대 값을 변경할 수 없는 객체
값 타입은 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야 함
이렇게 했을 때 false가 나옴
==
사용equals()
사용a.equals(b)
를 사용해서 동등성 비교를 해야 함equals()
메소드를 적절하게 재정의(주로 모든 필드 사용) => eqauls의 디폴트는 == 이기 때문에 재정의 해야 함실무에서는 거의 비교할 일이 없다고 함 ㅎㅎ
값 타입을 하나 이상 저장할 때 사용
@ElementCollection
, @CollectionTable
사용@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
String의 경우 Column명을 따로 지정 가능! String은 값이 하나고 우리가 정의한 것이 아니기 때문에 예외적으로 매핑을 허용해 줌
<저장>
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1","street","zipcode"));
member.getAddressHistory().add(new Address("old2","street","zipcode"));
em.persist(member);
//쿼리 문이 총 5개가 나가게 됨!
값 타입도 본인 스스로 라이프 사이클이 없음 => 생명주기가 다 멤버에 속해 있음
값 타입 컬렉션은 영속성 전에(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있음
<조회>
... 위와 동일
member.getAddressHistory().add(new Address("old2","street","zipcode"));
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
나가게 되는 쿼리문
멤버만 가져오는 것을 볼 수 있음! => 컬렉션들은 다 지연 로딩
그렇기 때문에 실제 컬렉션들의 값이 사용될 때 쿼리문이 나가게 됨!
<수정>
Memebr findMember = em.find(Member.class, member.getId());
//homeCity -> newCity
//findMember.getHomeAddress().setCity("newCity"); 이렇게 하면 안 됨
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity, a.getStreet, a.getZipcode));
//아예 다 새롭게 넣어줘야 함
//치킨 -> 한식
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
//equlas와 hashCode를 제대로 구현해 놔야 함!!!!
findMember.getAddressHistory().remove(new Address("old1","street","1000"));
findMember.getAddressHistory().add(new Address("newCity","street","1000"));
근데 마지막 Address 수정 부분의 쿼리가 나가는 걸 보면 Address가 통째로 날라갔다가 2개를 아예 새롭게 넣어주는 insert쿼리가 나감 => 원래 있던 애까지 왜 굳이 삭제하는 거지라는 의문
값 타입은 정말 값 타입이라 판단될 때만 사용!
엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안됨!
식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티!