public String printUser(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam(); //연관된 team도 조회 가능
}
JPA는 이러한 문제를 해결하기 위해 엔티티가 실제 사용될 때까지 db 조회를 지연하는 지연 로딩을 지원한다.
따라서 team.getName() 처럼 실제 팀 엔티티의 값을 사용하는 시점에 db에서 조회할 수 있도록 한다.
이때, 실제 엔티티 객체 대신 db 조회를 지연할 수 있도록 만드는 가짜 객체를 프록시 객체라고 한다.
EntityManager.getReference() 메서드를 사용하면 JPA는 db를 조회하지 않고 프록시 객체를 반환한다.
프록시 클래스는 실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉 모양이 같다.
따라서, 사용하는 입장에서는 이것이 진짜 객체인지 프록시 객체인지 구분하지 않고 사용할 수 있다.
프록시 객체는 실제 객체를 참조하고 있다.
따라서, 프록시 객체의 메서드를 호출하면 프록시 객체는 실제 객체의 메서드를 호출하여 반환한다.
getName() 메서드 호출target.getName()을 호출해서 결과를 반환한다.em.getReference()를 해도 프록시가 아닌 실제 객체 반환Member member = em.getReference(Member.class, "id1");
transaction.commit();
em.close();
member.getName(); //org.hibernate.LazyInitializationException 예외 발생team.getId()를 호출해도 프록시를 초기화하지 않는다.Team team = em.getReference(Team.class, "team1");
team.getId(); // 초기화 X
boolean isLoad = em.getEntityManagerFactory()
.getPersistenceUnitUtil().isLoaded(entity);
@ManyToOne(fetch = FetchType.EAGER)member.getTeam().getName()@ManyToOne(fetch = FetchType.LAZY)@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
즉시로딩이기 떄문에 회원과 팀을 조회하는 2개의 쿼리가 날라갈 것 같지만, JPA 구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 통해 한번에 처리한다.
//LEFT OUTER JOIN (기본값)
@JoinColumn(name = "TEAM_ID", nullable = true)
//INNER JOIN
@JoinColumn(name = "TEAM_ID", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
-------------------------------------
Member member = em.find(Member.class, "member1');
Team team = member.getTeam();
team.getName(); // 팀 객체 실제 사용
실제 객체의 데이터가 필요한 순간에 db를 조회해서 프록시 객체를 초기화한다.

@Entity
public class Member {
@Id
private String id;
private String username;
private Integer age;
@ManyToOne(fetch = FetchType.EAGER)
private Team team;
@OneToMany(mappedBy = "member, fetch = FetchType.LAZY)
private List<Order> orders;
}

//주문내역 조회
Member member = em.find(Member.class, "member1");
List<Order> orders = member.getOrders();
System.out.println("orders = " + orders.getClass().getName());
// 출력 결과: orders = org.hibernate.collections.internal.PersistenBag
orders 컬렉션은 아직 초기화되지 않는다. member.getOrders().get(0) 처럼 컬렉션에서 실제 데이터를 조회할 때 초기화된다.FetchType.EAGER로 설정되어있으므로 주문내역을 조회할 때 연관된 상품도 조회된다.@ManyToOne, @OneToOne: 즉시 로딩@OneToMany, @ManyToMany: 지연 로딩특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이 기능을 사용한다. JPA는 CASCADE 옵션으로 영속성 전이를 제공한다.****
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent", cascade = CasecadeType.PERSIST)
private List<Child> children = new ArrayList<>();
}
@Entity
public class Child {
@Id
@GeneratedValue
private Long id;
@ManyToOne
private Parent parent;
}
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
child1.setParent(parent);
child2.setParent(parent);
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent); //부모 저장, 연관된 자식들 저장
영속성 전이는 연관관계 매핑과 아무 관련이 없다.
단지 엔티티를 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함을 제공할 뿐이다.
@OneToMany(mappedBy = "parent", cascade = CasecadeType.REMOVE)
private List<Child> children = new ArrayList<>();
영속성 전이는 엔티티를 삭제할 때도 사용할 수 있다. cascade = CasecadeType.REMOVE 로 설정한다.
부모 엔티티만 삭제하면 연관된 자식 엔티티도 함께 삭제된다.
Parent findParent = em.find(Parent.class, lL);
em.remove(parent);
만약, 영속성 전이를 설정하지 않고 위의 코드를 실행하면, db에서 부모 로우를 삭제하는 순간 자식 테이블에 걸려있는 외래 키 제약조건으로 인해, 데이터베이스의 외래키 무결성 예외가 발생한다.
ALL : 모두 적용PERSIST : 영속MERGE : 병합REMOVE : 삭제REFRESHDETACHPERSIST와 REMOVE는 플러시를 호출할 때 전이가 발생한다.
고아 객체 제거 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능
이 기능을 사용하면 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제된다.
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<>();
---------------------------------------------------------------------
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0);
즉, 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 판단하고 삭제하는 기능이다.
따라서 이 기능은 참조하는 곳이 하나일 때만 사용해야 한다.
또한, 부모를 제거하면 자식도 제거된다.
일반적으로 엔티티는 em.persist() 를 통해 영속화되고 em.remove() 를 통해 제거된다. 이것은 엔티티 스스로 생명 주기를 관리한다는 의미이다. 그런데 두 옵션(CASCADE + orphanRemoval)을 모두 활성화하면
부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.
//자식을 저장하려면 부모에 등록만 하면 된다(CASCADE).
Parent parent = em.find(Parent.class, parentId);
parent.addChild(child1);
------------------------------------------------------------------------------
//자식을 삭제하려면 부모에서 제거하면 된다(orphanRemoval)
Parent parent = em.find(Parent.class, parentId);
parent.getChildren().remove(child1);
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
private int age;
}
String, int : 기본값 타입
기본값 타입은 공유하면 안된다.
@Embedded@EmbeddablePeriod.isWork() 처럼 해당 값 타입만 사용하는 메서드를 만들 수 있다.@Entity
public class Member {
@Id
@GeneratedValue
priate Long id;
private String name;
@Embedded
private Period workPeriod; //근무 기간
@Embedded
private Address homeAddress; //집 주소
}
@Embeddable
public class Period {
@Temporal(TemporalType.DATE)
private Date startDate;
@Temporal(TemporalType.DATE)
private Date endDate;
public boolean isWork(Date date){
...
}
}
@Embeddable
public class Address {
@Column(name="city")
private String city;
private String street;
private String zipcode;
}

임베디드 타입은 엔티티의 값일 뿐이므로, 값이 속한 엔티티의 테이블에 매핑된다.
잘 설계한 ORM 어플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다!
Address가 값 타입인 Zipcode를 포함PhoneNumber가 엔티티 타입인 PhoneServiceProvider를 참조@Entity
public class Member {
@Embedded
private Address address;
@Embedded
private PhoneNumber phoneNumber;
}
@Embeddable
public class Address {
private String street;
private String city;
private String state;
@Embedded
private Zipcode zipcode;
}
@Embeddable
public class Zipcode {
private String zip;
private String plusFour;
}
@Embeddable
public class PhoneNumber {
private String areaCode;
private String localNumber;
@ManyToOne
PhoneServiceprovider provider;
}
@Entity
public class PhoneServiceProvider {
@Id
private String name;
}
@AttributeOverride를 사용해서 매핑정보를 재정의@Entity
public class Member {
@Id
@GeneratedValue
priate Long id;
private String name;
@Embedded
private Address homeAddress;
//@Embedded
//private Address companyAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "COMPANY_CITY")),
@AttributeOverride(name = "street", column = @Column(name = "COMPANY_STREET")),
@AttributeOverride(name = "zipcode", column = @Column(name = "COMPANY_ZIPCODE"))
})
private Address companyAddress;
}

member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
address.setCity("NewCity"); // 회원 1의 address 값을 공유해서 사용
member2.setHomeAddress(address);
회원1과 회원2가 같은 address를 참조하고 있기 때문에 영속성 컨텍스트는 둘 다 city 속성이 바뀐걸로 인지

member1.setHomeAddress(new Address("OldCity");
Address address = member1.getHomeAddress();
Address newAddress = address.clone();
newAddress.setCity("NewCity");
member2.setHomeAddress(newAddress);
clone 메서드는 자신을 복사해서 반환하도록 구현 (임의로 구현한 메서드인듯)****
회원1의 주소 인스턴스를 복사해서 사용한다. 이 코드를 실행하면 의도한 대로 회원2의 주소만 NewCity로 변경된다.
//기본 타입은 값을 복사해서 준다.
int a = 10;
int b = a;
b = 4;
//객체는 참조 값을 준다.
Address a = new Address("Old");
Address b = a;
b.setCity("New"); //a도 같이 바뀜
객체의 공유참조는 피할 수 없다. 따라서 근본적인 해결책이 필요한데 가장 단순한 방법은 객체의 값을 수정하지 못하게 수정자 메서드를 모두 제거하는 것이다.
한번 만들면 절대 변경할 수 없는 객체
객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용을 원천 차단할 수 있다.
@Embeddable
public class Address {
private String city;
protected Address() {} // JPA에서 기본 생성자는 필수다.
//생성자로 초기 값을 설정한다.
public Address(String city) {
this.city = city;
}
public String getCity() {
return city;
}
}
Address address = member1.getHomeAddress();
Address newAddress = new Address(address.getCity());
member2.setHomeAddress(newAddress);
Address a = new Address("서울시", "종로구", "1번지");
Address b = new Address("서울시", "종로구", "1번지");
값 타입은 비록 인스턴스가 달라도 그 안의 값이 같다면 같은 것으로 봐야 한다.
따라서 값 타입을 비교할때는 a.equals(b)를 사용해서 동등성 비교를 해야 한다.
물론 equals() 메서드에 대한 재정의가 필요하다.
재정의할때는 보통 모든 필드의 값을 비교하도록 구현한다.
@ElementCollection, @CollectionTable@CollectionTable 을 통해 매핑한다.@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOODS",
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<>();
}
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
}

조회 ( @ElementCollection(fetch = FetchType.LAZY) )
//회원만 조회, 임베디드 타입인 homeAddress도 함께 조회 (select 쿼리 한번)
Member member = em.find(Member.class, 1L);
//위에서 같이 조회됨.
Address homeAddress = member.getHomeAddress();
//실제 컬렉션을 사용할 때 select 쿼리 호출
Set<String> favoriteFoods = member.getFavoriteFoods();
//select 쿼리 호출
for (String favoriteFood : favoriteFoods) {
System.out.println("favoriteFood = " + favoriteFood);
}
실무에서는 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신 일대다 관계를 고려하자!
또한, 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다.
따라서 데이터베이스 기본 키 제약 조건으로 인해 컬럼에 null을 입력할 수 없다.
결론 : 값 타입 컬렉션보다는 일대다 관계를 사용하자
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Embedded
private Address homeAddress;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
}
@Entity
public class AddressEntity {
@Id
@GeneratedValue
private Long id;
@Embedded
private Address address;
}