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
: 삭제REFRESH
DETACH
PERSIST
와 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
@Embeddable
Period.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;
}