package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team team = new Team();
team.setName("TeamA");
em.persist(team); // persist 하면 항상 id 값이 들어간다.
Member member = new Member();
member.setUserName("member1");
member.setTeamId(team.getId());
em.persist(team); // persist 하면 항상 id 값이 들어간다.
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
멤버 테이블에 TEAM_ID 값을 그대로 가지고 있다.
즉, 테이블에 맞춰 외래키 값을 그대로 가지고 있다는 문제가 있다.
Ex)
java
열기
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team team = new Team();
team.setName("TeamA");
em.persist(team); // persist 하면 항상 id 값이 들어간다.//
Member member = new Member();
member.setUserName("member1");
member.setTeamId(team.getId());
em.persist(member); // persist 하면 항상 id 값이 들어간다.
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
persist() 순서대로 팀 쿼리 => 멤버 쿼리 나간다.
H2 DB는 내부적으로 sequence를 쓴다.
객체를 테이블에 맞춰 데이터 중심으로 모델링하면 협력 관계를 만들 수 없다.
테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
객체는 참조를 사용해서 연관된 객체를 찾는다.
테이블과 객체 사이에는 이런 큰 간격이 있다.
객체 지향 모델링 (객체 연관관계 사용)
Ex) 객체 지향 모델링
멤버 테이블에 다음과 같이 애너테이션을 추가하면 기본키 - 외래키 관계를 매핑한다.
java
열기
@ManyToOne@JoinColumn(name = "TEAM_ID")private Team team;
다음 코드를 추가하면 JPA가 자동으로 team 이라는 PK를 꺼내서 FK로 가져온다.
member.setTeam(team);
Team findTeam = findMember.getTeam();
persist() 하면 영속성 컨텍스트에 들어간다.
그래서 find() 하면 1차 캐시에서 꺼내올 수 있다. => 다음 예제는 데이터베이스에서 가져오는 방법
java
열기
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
// 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team); // persist 하면 항상 id 값이 들어간다.
Member member = new Member();
member.setUserName("member1");
member.setTeam(team);
em.persist(member); // persist 하면 항상 id 값이 들어간다.
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
System.out.println("findTeam = " + findTeam.getName());
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
멤버에 대해 getTeam()을 하면 바로 팀 이름이 나오게끔 세팅완료
그러면 이제 객체 지향의 특성을 살릴 수 있다.
Ex) 영속성 컨텍스트 말고 데이터베이스에서 가져오려면?
java
열기
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
// 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team); // persist 하면 항상 id 값이 들어간다.
Member member = new Member();
member.setUserName("member1");
member.setTeam(team);
em.persist(member); // persist 하면 항상 id 값이 들어간다.
em.flush(); // 영속성 컨텍스트에 있는 것들에 대해 쿼리를 날린다.
em.clear(); // 영속성 컨텍스트 초기화
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
System.out.println("findTeam = " + findTeam.getName());
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
아까 1차 캐시에서 가져올 때와는 다르다.
insert 쿼리 두 개 날아가고 그 다음에 select쿼리 실행된다.
멤버와 팀을 조인해서 실행한다.
Ex) 연관 관계 수정하기
커밋하기 전에 아래 코드를 추가하면 어떤 멤버의 팀을 수정 가능
그럼 DB에 외래 키 값이 update 된다.
java
열기
Team newTeam = em.find(Team.class, 100L);
findMember.setTeam(newTeam);
5.2 양방향 연관 관계와 연관 관계의 주인 (1) - 기본
양방향 연관 관계
테이블 연관관계는 단방향에서의 테이블 연관 관계와 똑같다.
테이블은 왜 전혀 변화가 없을까?
테이블은 외래키 하나로 양방향을 모두 이어준다. 따라서, 테이블에는 방향 개념이 없다.
문제는 객체이다.
따라서, Team에 List members를 넣어줘야 양쪽으로 파악 가능하다.
객체와 테이블 간에 연관 관계를 맺는 차이
객체 연관 관계: 2개
회원 => 팀 연관관계 1개 (단방향)
팀 => 회원 연관관계 1개 (단방향)
객체의 양방향 관계는 사실 단방향 2개라고 보는 것이 맞다.
테이블 연관 관계: 1개
회원 <=> 팀 연관관계 1개 (양방향)
따라서, 둘 중 하나로 외래키를 관리해야한다.
Member의 team이 바뀌었을 때, Team의 members가 바뀌었을 때, 이 둘 중 무엇이 바뀌었을 때, MEMBER 테이블이 업데이트 되어야할까?
"연관 관계의 주인"
연관 관계의 주인(Owner)
양방향 매핑 규칙
객체의 두 관계 중 하나를 연관 관계의 주인으로 지정한다.
연관 관계의 주인만이 외래 키를 관리한다. (관리: 등록, 수정)
주인이 아닌 쪽은 읽기만 가능
주인은 mappedBy 속성을 사용하면 안 된다.
주인이 아니면 mappedBy 속성으로 주인을 지정한다.
주인이 아닌 쪽에 데이터를 넣어봐야 아무 일도 일어나지 않는다. (오류도 없다.) 단순 조회만 가능
외래 키가 있는 곳을 주인으로 정한다.(다대일 중 "다"에 해당한다.) 멤버랑 팀 중에 멤버를 말한다.
현재 Member.Team이 연관 관계의 주인이다.
Ex) 양방향 연관 관계 - 반대 방향으로 객체 그래프 탐색
Team에 다음과 같이 추가한다.
mapped by = "team" : 에서의 team은 멤버 클래스의 변수 team을 의미한다.
java
닫기
<hide/>
@OneToMany(mappedBy = "team")private List<Member> members = new ArrayList<>();
flush(), clear() 해줘야 DB에서 깔끔하게 값을 가져온다.
멤버 => 팀 => 멤버 ... 이를 양방향 연관관계라고 한다.
team.getId() 에서 SELECT 쿼리가 실행된다.
java
열기
package hellojpa;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
// 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team); // persist 하면 항상 id 값이 들어간다.
Member member = new Member();
member.setUserName("member1");
member.setTeam(team);
em.persist(member); // persist 하면 항상 id 값이 들어간다.
em.flush(); // 영속성 컨텍스트에 있는 것들에 대해 쿼리를 날린다.
em.clear(); // 영속성 컨텍스트 초기화
Member findMember = em.find(Member.class, member.getId());
List<Member> members = findMember.getTeam().getMembers();
for (Member m : members) {
System.out.println("m = " + m.getUserName());
}
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
List에 세팅한 게 없지만 그래도 출력된다.
왜? JPA에서
위에 코드에서 flush() 위에 아래 코드를 추가해야 문제가 생기지 않는다.
왜?
완전히 flush(), clear()가 되면 문제가 없다. 하지만?
team.getMembers().add(member);
flush(), clear() 가 없으면 영속성 컨텍스트에 멤버와 아이디가 그대로 들어가 있다.
※ 중요※ 연관 관계의 주인과 mappedBy
mappedBy를 알려면 객체와 테이블 간에 연관 관계를 맺는 차이를 이해해야한다.
5.3 양방향 연관 관계와 연관 관계의 주인 (2) - 주의점, 정리
Ex) 양방향 연관 관계
java
열기
package hellojpa;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUserName("member1");
em.persist(member);
Team team = new Team();
team.setName("TeamA");
team.getMembers().add(member);
em.persist(team);
em.flush(); // 영속성 컨텍스트에 있는 것들에 대해 쿼리를 날린다.
em.clear(); // 영속성 컨텍스트 초기화
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
연관관계의 주인에 값을 입력하지 않으면 아무 일도 일어나지 않는다.
// team.getMembers().add(member); // 읽기 전용이므로 쿼리 날라가지도 않는다.
Note) 실행 결과
팀 아이디가 null이다 왜그럴까?
연관 관계의 주인은 멤버이지 팀이 아니다.
따라서, 다음과 같이 코드를 수정해야한다.
member.setTeam() 을 추가한다. => changeTeam()
java
열기
package hellojpa;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team team = new Team();
team.setName("TeamA");
// team.getMembers().add(member);
em.persist(team);
Member member = new Member();
member.setUserName("member1");
member.setTeam(team);
em.persist(member);
em.flush(); // 영속성 컨텍스트에 있는 것들에 대해 쿼리를 날린다.
em.clear(); // 영속성 컨텍스트 초기화
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
그리고 매인 클래스의 team.getMembers().add() ... 이 부분은 삭제하도록 한다.
java
열기
package hellojpa;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUserName("member1");
member.changeTeam(team);
em.persist(member);
// team.getMembers().add(member); 삭제
em.flush();
em.clear();
Team findTeam = em.find(Team.class, team.getId()); // SELECT FROM TEAM
List<Member> members = findTeam.getMembers(); // SELECT FROM MEMBER
System.out.println("==========");
for(Member m : members){
System.out.println("m = " + m.getUserName());
}
System.out.println("==========");
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
양방향 연관 관계 주의
순수 객체 상태를 고려해서 항상 양쪽에 값을 설정한다.
연관관계 편의 메서드를 생성한다.
양방향 매핑 시에 무한 루프를 조심한다.
ex) toString() lombok, JSON 생성 라이브러리(컨트롤러에서 response로 엔티티를 보내버리면 엔티티가 가진 연관 관계가 양방향일 때)
컨트롤러에는 엔티티를 반환하지 않도록 한다.
양방향 매핑 정리
단방향 매핑만으로 이미 연관관계 매핑은 완료
양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다.
JPQL에서 역방향으로 탐색할 일이 많다.
우선, 단방향 매핑을 잘 해두고 양방향은 나중에 필요할 때 추가해도된다. (테이블에 영향을 주지 않기 때문이다.)
5.4 실전 예제 2 - 연관 관계 매핑 시작
테이블 구조객체 구조 - 참조를 사용하도록 변경
Ex)
Order 테이블
java
열기
// 외래키 값을 매핑해서 그대로 가지고 있었지만 필요 없어진다.// @Column(name = "MEMBER_ID")// private Long memberId;@ManyToOne@JoinColumn(name ="MEMBER_ID")private Member member;
OrderItem 클래스가 "다"에 해당한다. (하나의 Order에 여러 개의 OrderItem이 있기 때문)
OrderItem 클래스
두 개의 필드(itemId, orderId)를 지운다.
이렇게 하면 외래 키 값을 가져오는 것이 아니라 order, item이라는 객체를 가진다.