예제 Entity, Member와 Order
Member와 Order 두가지 Entity를 생성해준다. H2데이터베이스를 사용했다.
두 엔티티는 아래와 같다.
@Entity
@Table(name = "MEMBER")
@SequenceGenerator(
name = "HIBERNATE_SEQUENCE",
sequenceName = "MEMBER_SEQ",
initialValue = 1, allocationSize = 1
)
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@NotNull
private String name;
@Nullable
private String city;
@Nullable
private String street;
@Nullable
private String zipcode;
//getter, setter
}
@Entity
@Table(name = "ORDERS")
@SequenceGenerator(
name = "HIBERNATE_SEQUENCE",
sequenceName = "ORDER_SEQ",
initialValue = 1, allocationSize = 1
)
public class Order {
@Id
@GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
private Long memberId;
@Temporal(TemporalType.TIMESTAMP)
private Date orderDate; //@Temporal는 java.util.Date or java.util.Calendar 만 가능
@Enumerated(EnumType.STRING)
private OrderStatus status;
//getter, setter
}
JOIN
이제 두 엔티티를 조인해보자.
Member는 Order의 MEMBER_ID를 기준으로 join한다. 쿼리로 표현하면 아래와 같다.
SELECT *
FROM MEMBER T1
JOIN ORDERS ON T2 (T1.MEMBER_ID = T2.MEMBER_ID)
이를 JPA로 나타내기위한 Anotation이 바로 @OneToMany, @ManyToOne이다.
Member는 @OneToMany를 통해 Order와 join된다. Member하나는 여러개의 Order를 가질수 있다.
Order는 @ManyToOne을 통해 Member와 join된다. 여러개의 Order는 하나의 Member를 가질 수 있기 때문이다.
일단 @ManyToOne만 써보자
@Entity
@Table(name = "ORDERS")
@SequenceGenerator(
name = "HIBERNATE_SEQUENCE",
sequenceName = "ORDER_SEQ",
initialValue = 1, allocationSize = 1
)
public class Order {
@Id
@GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne //JOIN
@JoinColumn(name = "MEMBER_ID") //FK를 써준다.
private Member member;
@Temporal(TemporalType.TIMESTAMP)
private Date orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
//getter, setter
}
@ManyToOne을 이용해 연결하고 @JoinColumn을 통해 FK를 명시해주었다.
이렇게 하면, Order에서 getter와 setter를 통해 Member에 접근 가능하다.
//getter, setter
public Member getMember() {
return member;
}
public void setMember(Member member) {
this.member = member;
}
//member를 생성하여
Member member = new Member();
member.setName("ghost");
member.setCity("songpa");
member.setStreet("123");
member.setZipcode("apart");
//order에 set
Order order = new Order();
order.setStatus(OrderStatus.ORDER);
order.setMember(member);
order.setOrderDate(new Date());
하지만, member에서 order를 찾는 것은 불가능하다. order에서 member방향만 연결되어있다.
일반적으로 DB에서는 FK를 통해 member에서 order를 찾을 수 있고, order에서 member를 찾을 수도 있다. 반면 객체에서는 불가능하다. 단방향(order -> member 또는 mebmer -> order)를 두개 구현해주어야 양방향에서 찾을 수 있다.
양방향 연관관계
양방향 연관관계를 맺어보자. 이번에는 Member에 @OneToMany 를 달아주어 양방향 연관관계를 맺을 수 있다.
@Entity
@Table(name = "MEMBER")
@SequenceGenerator(
name = "HIBERNATE_SEQUENCE",
sequenceName = "MEMBER_SEQ",
initialValue = 1, allocationSize = 1
)
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@NotNull
private String name;
@Nullable
private String city;
@Nullable
private String street;
@Nullable
private String zipcode;
@OneToMany(mappedBy = "member") //mappedBy는 연관관계의 주인을 나타낸다.
private List<Order> orders;
//getter, setter
}
@OneToMany로 연관관계가 완성되었다.
양방향에서는 연관관계의 주인을 설정해주어야한다. 연관관계의 주인만 DB연관관계와 매핑되고, 외래키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
주인이 아닌 곳에서 상대방이 주인이라고 알려주기 위해 mappedBy를 사용한다. 주인은 mappedBy속성을 사용하지 않는다.
주인은 외래키가 존재하는 곳이다. 지금 외래키인 MEMBER_ID는 ORDER에 존재하고 있으므로, ORDER가 연관관계의 주인이다. 따라서 MEMBER에 mappedBy를 써준다. mappedBy = 상대방 entity의 join대상 객체, 즉 member이다.
이렇게 조인이 끝났다.
MemberId로 select해보기
MemberId로 where조건을 주어 ORDER를 select해보자
쿼리로 보자면 아래와 같다.
SELECT *
FROM MEMBER T1
JOIN ORDERS T2 ON (T2.MEMBER_ID = T1.MEMBER_ID)
WHERE MEMBER_ID = '?'
객체로 짜보자
@Transactional
public void save(Member member) {
em.persist(member);
}
@Transactional
public Order save(Order order) {
em.persist(order);
return order;
}
//given
Member member = new Member();
member.setName("ghost");
member.setCity("universe");
member.setStreet("123");
member.setZipcode("a");
memberRepository.save(member);
Order order = new Order();
order.setStatus(OrderStatus.ORDER);
order.setMember(member);
order.setOrderDate(new Date());
orderRepository.save(order);
Order order2 = new Order();
order2.setStatus(OrderStatus.ORDER);
order2.setMember(member1);
order2.setOrderDate(new Date());
orderRepository.save(order2);
//when
List<Order> orders = member.getOrders();
//then
Assertions.assertThat(orders.size()).isEqualTo(2);
테스트에 통과하지 않는다!
하지만 테스트에 통과하지 않는다. 왜일까?
순수 객체 입장에서보면 order에서 member를 set해주긴했는데 member에서 order를 get해주는 연결은 해주지 않았다.
아래와 같은 코드가 더 필요하다.
public void setMember(Member member) {
this.member = member;
member.getOrders().add(this);
}
member를 할당할때 member에서 함께 order도 할당해준다. 따로따로 하면 실수할 수 있으니 한번에 묶어놓는게 낫다.
또 문제가 있는데, 여기서
setMember(member1)
setMember(member2)
이렇게 두번하면, 여전히 member1이 조회된다.
member1의 의 연관관계가 설정된 상태에서 삭제되지 않고 member2도 연결되었기 때문이다 따라서 아래와 같은 코드를 추가해야한다.
public void setMember(Member member) {
if(this.member != null) {
this.member.getOrders().remove(this);
}
this.member = member;
member.getOrders().add(this);
}
원리
JPA에서 내부적으로 일어나는 일
이게 어떻게 가능할까?
사실, 객체의 입장에서 보자면, getMemberId를 통해 memberId를 가져온 후 id로 member를 가져오는 등 세부 과정이 필요하다.
하지만, 이러한 과정을 JPA에서 모두 해준다. 그냥 member로 가져와, 라고 하면, memberId를 가져오는 쿼리, 가져온 id로 member를 조회하는 쿼리 두가지를 내부적으로 실행해서 가져다주는 것이다.
그래서 이러한 객체지향적인 코드가 가능하다!
(JPA는 쿼리에서 분리되어 도메인 설계를 객체지향적으로 하기 위해 나왔다.)
동일성 보장
사실 그냥 객체로 Member를 뽑았을 때는 동일성 보장이 되지 않는다.
class MemberDAO {
public Member getMember(String memberId) {
String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID = ?";
...
}
}
String memberId = 100;
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);
member1 == member2 // 다름
객체의 주소가 다르기 때문에 동일하지 않다.
이 문제를 JPA가 알아서 해결해 준다. JPA에서는 같은 트랜잭션일 때 깥은 객체가 조회되는 것을 보장한다.
String memberId = 100;
Member member1 = jpa.getMember(memberId);
Member member2 = jpa.getMember(memberId);
member1 == member2 //같음
따라서 where조건을 주어 조회할 때 Member객체 자체를 넣어 걱정없이 조회할 수 있는 것이다~