실전 예제 - 1. 요구사항 분석과 기본 매핑
요구사항 분석
• 회원은 상품을 주문할 수 있다.
• 주문 시 여러 종류의 상품을 선택할 수 있다.
도메인 모델 분석
회원과 주문의 관계: 회원은 여러 번 주문할 수 있다. (일대다)
주문과 상품의 관계: 주문할 때 여러 상품을 선택할 수 있다.
반대로 같은 상품도 여러 번 주문될 수 있다.
주문상품 이라는 모델을 만들어서 다대다 관계를 일다대, 다대일 관계로 풀어냄
테이블 설계
엔티티 설계와 매핑
생성된 클래스
@Getter
@Setter
@Entity
public class Item {
@Id
@GeneratedValue
@Column(name = "ITEM_ID") // 테이블상 컬럼명이 ITEM_ID 이므로 매핑해준다.
private Long id;
private String name;
private int price;
private int stockQuantity;
}
@Getter
@Setter
@Entity
public class Member {
@Id @GeneratedValue // strategy 는 생략하면 AUTO 이다.
@Column(name = "MEMBER_ID")
private Long id;
private String name;
private String city;
private String street;
private String zipcode;
}
@Getter
@Setter
@Entity
public class Order {
@Id
@GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@Column(name = "MEMBER_ID")
private Long memberId;
private LocalDateTime orderDate; // LocalDateTime 은 따로 매핑필요없다, 카멜케이스는 소문자 + _와 매핑되므로 order_date 랑 매핑된다.
@Enumerated(EnumType.STRING) // enum 은 EnumType String 으로 설정해줘야한다.
private OrderStatus status;
}
@Getter
@Setter
@Entity
public class OrderItem {
@Id @GeneratedValue
@Column(name = "ORDER_ITEM_ID")
private Long id;
@Column(name = "ORDER_ID")
private Long orderId;
@Column(name = "ITEM_ID")
private Long itemId;
private int orderPrice;
private int count;
}
public enum OrderStatus {
ORDER,CANCEL
}
데이터 중심 설계의 문제점
• 현재 방식은 객체 설계를 테이블 설계에 맞춘 방식
• 테이블의 외래키를 객체에 그대로 가져옴
-> mermberId와 같은 외래키말고 Member와 같은 실제 참조값을 가져와서 가지고있어야 객체 그래프탐색이 가능하다.
• 객체 그래프 탐색이 불가능
• 참조가 없으므로 UML도 잘못됨 ->이런식으로 구성하면 id만 가지고있고 실제 참조값을 가지고있으므로, 참조가 아니니까 연결이 끊긴 모습으로 다시 그려야한다.
현재는 테이블을 기준으로 클래스를 생성했다. 즉 객체 설계를 테이블 설계에 맞춘 방식이다.
이런식을 짜면
Order order = em.find(Order.class, 1L);
Long memberId = order.getMemberId();
em.find(Member.class, memberId);
이렇게 짤수밖에없고
@Getter
@Setter
@Entity
public class Order {
@Id
@GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@Column(name = "MEMBER_ID")
private Long memberId;
private Member member; // 이런식으로 멤버를 필드로 가지고있는게 객체 지향 방식
private LocalDateTime orderDate; // LocalDateTime 은 따로 매핑필요없다, 카멜케이스는 소문자 + _와 매핑되므로 order_date 랑 매핑된다.
@Enumerated(EnumType.STRING) // enum 은 EnumType String 으로 설정해줘야한다.
private OrderStatus status;
}
Order order = em.find(Order.class, 1L);
Member member = order.getMember();
이런식으로 멤버객체를 가져오는게 객체지향 방식일것이다. [ 객체 그래프 탐색 방식 ]
이것을 해결하기위해 연관관계 매핑에 대해 배운다.
연관관계 매핑 기초
객체랑 관계형DB랑의 패러다임 차이를 해결하기 위해
연관관계 매핑을 사용하고
클래스를 객체지향스럽게 설계를 할 수 있게 된다.
객체와 테이블 연관관계의 차이를 이해
객체는 참조값을 이용해서 연관관계를 가지고
ex) Order 클래스의 필드로 Member를 가진다.
테이블은 외래키를 이용하여 연관관계를 가진다.
ex) Order 클래스의 필드로 memberId를 가진다.
객체의 참조와 테이블의 외래 키를 매핑
이제 해야할것.
예제 시나리오
• 회원과 팀이 있다.
• 회원은 하나의 팀에만 소속될 수 있다.
• 회원과 팀은 다대일 관계다.
객체를 테이블에 맞추어 모델링
(연관관계가 없는 객체)
@Getter
@Setter
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@Column(name = "TEAM_ID")
private Long teamId;
}
@Setter
@Getter
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
}
테이블 설계에 맞춰서 클래스 설계를 한 상황
객체를 테이블에 맞추어 모델링 (외래 키 식별자를 직접 다룸)
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team = new Team();
team.setName("TeamA");
/*
현재 @GeneratedValue 의 strategy 설정은 AUTO 이고, h2를 사용하므로 identity 전략이 설정되었을것이다.
identity 전략일 경우 persist 명령어 실행시 곧 바로 insert sql이 실행되고 auto increment 된 기본키 값을 가져와서
@Id가 매핑되어있는 필드에 넣어준다. 영속상태가 되려면 PK값이 필수로 필요하기 때문이다.
*/
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId());
em.persist(member);
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close();
JPA와 Hibernate는 성능 최적화를 위해 시퀀스 값을 한 번에 여러 개 미리 가져오는 방식(배치 할당 크기)을 사용
기본적으로 Hibernate는 allocationSize라는 속성을 사용하여 시퀀스 값을 한 번에 얼마나 많이 미리 가져올지 설정합니다. 기본값은 50입니다. 이를 통해 데이터베이스와의 통신 횟수를 줄여 성능을 최적화
객체를 테이블에 맞추어 모델링
(식별자로 다시 조회, 객체 지향적인 방법은 아니다.)
Member findMember = em.find(Member.class, member.getId()); // 멤버를 가져오고
Long findTeamId = findMember.getTeamId(); // 멤버의 팀 아이디를 이용해서
em.find(Team.class, findTeamId); // 멤버가 속한 팀을 가져온다.
객체를 테이블에 맞추어 데이터 중심으로 모델링하면,
협력 관계를 만들 수 없다.
• 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
• 객체는 참조를 사용해서 연관된 객체를 찾는다.
• 테이블과 객체 사이에는 이런 큰 간격이 있다.
단방향 연관관계
객체 연관관계를 사용하도록 클래스를 수정해본다.
객체 지향 모델링
(객체 연관관계 사용)
@Getter
@Setter
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
/*
jpa 에게 어떤 연관관계를 가지는지 알려줘야한다, 멤버는 하나의 팀을 가질수있고, 하나의 팀은 여러 멤버를 가질 수 있다.
이 클래스의 여러 객체는 하나의 team 을 가질 수 있다. -> @ManyToOne
이 Team 필드가 실제 멤버 테이블의 teamId 라는 외래키와 매핑을 해야한다. -> @JoinColumn(name = "TEAM_ID")
즉 Team 이라는 테이블과 join 할때 사용 되는 컬럼 (외래키)과 매핑해주면 된다. -> @JoinColumn(name = "TEAM_ID")
*/
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
수정된 코드
Team team = new Team();
team.setName("TeamA");
/*
현재 @GeneratedValue 의 strategy 설정은 AUTO 이고, h2를 사용하므로 identity 전략이 설정되었을것이다.
identity 전략일 경우 persist 명령어 실행시 곧 바로 insert sql이 실행되고 auto increment 된 기본키 값을 가져와서
@Id가 매핑되어있는 필드에 넣어준다. 영속상태가 되려면 PK값이 필수로 필요하기 때문이다.
*/
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team); // 객체 지향 모델링을 했기 때문에 다음같이 사용할 수 있다.
em.persist(member);
이렇게 하면 jpa가 멤버객체 필드에 들어가있는 team에서 pk를 꺼내고 insert할때 추가해서 sql를 실행해준다.
Member findMember = em.find(Member.class, member.getId()); // 멤버를 가져오고
Team findTeam = findMember.getTeam(); // 객체 지향 모델링을 했기 때문에 다음같이 사용할 수 있다.
조회 또한 get~ 과 같은 프로퍼티 접근법으로 가져올 수 있다.
// persist하면서 1차캐시에 해당 객체가 관리되므로 find할때 sql이 실행되지않고 1차캐시에 꺼내진다. 그러므로 콘솔에 sql문이 보이지않음
// sql을 보고싶다면 flush로 쓰기지연 sql 저장소에있는 sql을 실행시키고 + 변경감지 , 그다음 영속성 컨테이너를 비운다.
// 그러면 find할때 1차캐시에 아무것도 없으니 db에서 실제 sql를 실행시켜 가져올것이다.
em.flush();
em.clear();
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team = new Team();
team.setName("TeamA");
/*
현재 @GeneratedValue 의 strategy 설정은 AUTO 이고, h2를 사용하므로 identity 전략이 설정되었을것이다.
identity 전략일 경우 persist 명령어 실행시 곧 바로 insert sql이 실행되고 auto increment 된 기본키 값을 가져와서
@Id가 매핑되어있는 필드에 넣어준다. 영속상태가 되려면 PK값이 필수로 필요하기 때문이다.
*/
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team); // 객체 지향 모델링을 했기 때문에 다음같이 사용할 수 있다.
em.persist(member);
// persist하면서 1차캐시에 해당 객체가 관리되므로 find할때 sql이 실행되지않고 1차캐시에 꺼내진다. 그러므로 콘솔에 sql문이 보이지않음
// sql을 보고싶다면 flush로 쓰기지연 sql 저장소에있는 sql을 실행시키고 + 변경감지 , 그다음 영속성 컨테이너를 비운다.
// 그러면 find할때 1차캐시에 아무것도 없으니 db에서 실제 sql를 실행시켜 가져올것이다.
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId()); // 멤버를 가져오고
Team findTeam = findMember.getTeam(); // 객체 지향 모델링을 했기 때문에 다음같이 사용할 수 있다.
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close();
select sql 생성된것을 확인해보면
멤버 클래스의 필드에는 팀이 있으므로
팀과 join해서 팀의 id까지 가져오는 모습을 볼 수 있다.
코드상에서는 Team 필드를 가져서 객체지향적으로 코드를 짤 수 있고,
실제로 jpa가 Team에 매핑된 외래키를 이용해서 객체지향적으로 짠 코드를 sql로 처리한다.
Member findMember = em.find(Member.class, member.getId()); // 멤버를 가져오고
Team findTeam = findMember.getTeam(); // 객체 지향 모델링을 했기 때문에 다음같이 사용할 수 있다.
System.out.println("findTeam = " + findTeam.toString());
find같은경우는 바로 sql이 실행되고,
getTeam 명령문이 실행될때 team과 join해서 가져오는지
아니면 em.find(멤버)를 할때 team과 join 해서 한번에 가져오는지는
지연로딩, 즉시로딩 설정에 달려있고 나중에 설명.
@Getter
@Setter
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
teamId라는 외래키를 필드로 가지는것 대신 team 이라는 참조변수를 필드로 가짐으로써
엔티티를 객체지향적으로 사용할 수 있게된다.
하지만 필드로 기본키인 id를 가지고있으므로 약간은 테이블 관계 매핑을 고려한 클래스 설계인것같다.
양방향 매핑
양방향 연관관계여도 테이블 연관관계는 차이가없다.
서로 외래키를 알고있기 때문에 join으로 서로를 조회가능하기 때문이다.
테이블의 연관관계는 외래키 하나로 양방향이 다 있는것이다. [ 테이블의 연관관계에서는 방향이라는것이 없다. ]
이전에 단방향 객체 연관관계에서는 멤버에는 Team 참조변수가 있었지만 팀에는 멤버와 관련된 참조변수가 없었다.
이번에는 팀에도 멤버리스트라는 참조변수리스트를 넣어서 양방향 연관관계를 만든다.
@Getter
@Setter
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Setter
@Getter
@Entity
@ToString
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
// List 를 미리 초기화두는것이 관례라고 한다. ( add 할때 nullpointerexception 이 발생하지 않도록)
@OneToMany(mappedBy = "team") // 팀 하나는 여러 멤버를 가질 수 있다. 클래스 -> 필드 순으로 생각하면 될듯 ,mappedBY는 연관관계 주인을 설정하면된다.
private List<Member> members = new ArrayList<Member>();
}
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team = new Team();
team.setName("TeamA");
/*
현재 @GeneratedValue 의 strategy 설정은 AUTO 이고, h2를 사용하므로 identity 전략이 설정되었을것이다.
identity 전략일 경우 persist 명령어 실행시 곧 바로 insert sql이 실행되고 auto increment 된 기본키 값을 가져와서
@Id가 매핑되어있는 필드에 넣어준다. 영속상태가 되려면 PK값이 필수로 필요하기 때문이다.
*/
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team); // 객체 지향 모델링을 했기 때문에 다음같이 사용할 수 있다.
em.persist(member);
// persist하면서 1차캐시에 해당 객체가 관리되므로 find할때 sql이 실행되지않고 1차캐시에 꺼내진다. 그러므로 콘솔에 sql문이 보이지않음
// sql을 보고싶다면 flush로 쓰기지연 sql 저장소에있는 sql을 실행시키고 + 변경감지 , 그다음 영속성 컨테이너를 비운다.
// 그러면 find할때 1차캐시에 아무것도 없으니 db에서 실제 sql를 실행시켜 가져올것이다.
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.getName());
}
tx.commit();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close();
연관관계의 주인과 mappedBy
객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다.
객체와 테이블이 관계를 맺는 차이
객체 연관관계 = 2개
• 회원 --> 팀 연관관계 1개(단방향)
• 팀 --> 회원 연관관계 1개(단방향)
객체에서 양방향 연관관계는 사실 단방향 연관관계가 2개있는것이다.
테이블 연관관계 = 1개
• 회원 <--> 팀의 연관관계 1개(양방향)
객체의 양방향 관계
객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단뱡향 관계 2개다.
객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
A --> B [ a.getB()로 단방향 ]
B --> A [b.getA() 로 단방향 ]
테이블의 양방향 연관관계
테이블은 외래 키 하나로 두 테이블의 연관관계를 관리
MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계 가짐 (양쪽으로 조인할 수 있다.)
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
SELECT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
둘 중 하나로 외래 키를 관리해야 한다.
멤버에 있는 team으로 외래키를 관리할지
팀에 있는 members로 외래키를 관리할지
둘중 하나를 주인으로 정해야한다. == 연관관계의 주인
연관관계의 주인(Owner)
양방향 매핑 규칙
• 객체의 두 관계중 하나를 연관관계의 주인으로 지정
• 연관관계의 주인만이 외래 키를 관리(등록, 수정)
• 주인이 아닌쪽은 읽기만 가능 • 주인은 mappedBy 속성 사용X
• 주인이 아니면 mappedBy 속성으로 주인 지정
누구를 주인으로?
외래 키가 있는 있는 곳을 주인으로 정해라
여기서는 Member.team이 연관관계의 주인 [ 멤버클래스의 team필드가 team외래키와 관련있는 필드이기 때문]
@Entity
public class Member {
....
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@JoinColumn(name = "TEAM_ID")
team이라는 필드는 joinColumn 어노테이션을 이용해서 테이블의 외래키와 매핑을 해놓은 상태이다.
즉 team이라는 필드가 연관관계의 주인이다.
@Entity
public class Team {
....
// List 를 미리 초기화두는것이 관례라고 한다. ( add 할때 nullpointerexception 이 발생하지 않도록)
@OneToMany(mappedBy = "team") // 팀 하나는 여러 멤버를 가질 수 있다. 클래스 -> 필드 순으로 생각하면 될듯 mappedBY는 연관관계 주인을 설정하면된다.
private List<Member> members = new ArrayList<Member>();
}
@OneToMany(mappedBy = "team")
주인이 아닌쪽에는 mappedBy로 주인을 설정 해준다. 주인이 아닌쪽에서는 외래키를 이용한 읽기만 가능하다.
members에 어떤 값을 넣더라도, 외래키의 변화는없다.
DB에서는 외래키있는쪽이 N(다) 이다.
없는쪽이 1이다.
즉 db의 n쪽이 무조건 연관관계의 주인
@ManyToOne을 사용하는곳이 연관관계의 주인 [ OneToOne 도 있긴함 ]
양방향 매핑시 가장 많이 하는 실수 (연관관계의 주인에 값을 입력하지 않음)
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setName("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();
연관 관계 주인은 Member의 team필드이다. team필드에 팀을 참조하게해야 team_id가 들어간다.
mappedBy되어있는 필드는 읽기전용이므로 외래키를 넣어주지않는다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team); // 연관관계 주인인 곳에 값을 넣기
em.persist(member);
team.getMembers().add(member);
team_id에 값이 잘 들어간다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team); // 연관관계 주인인 곳에 값을 넣기
em.persist(member);
em.flush();
em.clear();
Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();
for (Member m : members) {
System.out.println("m = " + m.getName() + ", team = " + m.getTeam());
}
team.getMembers().add(member);
팀안의 멤버리스트에 멤버를 넣어주는 과정을 하지않아도 잘 들어가있다.
들어가있다기 보다는 sql로 인해 잘 가져와진다.
위의 select는
Team findTeam = em.find(Team.class, team.getId());
가 진행되면서 생겼을것이고
아래 select는
List<Member> members = findTeam.getMembers();
가 진행되면서 생겼을것이다.
하지만
team.getMembers().add(member);
를 해주지않는다면 문제가 발생할 수 있다.
// em.flush();
// em.clear();
// flush,clear 주석시, team 객체를 persist할때 1차캐시에 들어가있으므로 1차캐시에서 가져오게된다. 그렇게되면 멤버가 들어간것이 반영 됬을리가 없다.
// flush,clear를 해줘야 다시 db에서 가져오기때문에 최신화 되어있는 정보를 가져올 수 있다. flush,clear를 매번하기 힘드니까 양쪽 다 값을 입력하자
Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();
for (Member m : members) {
System.out.println("m = " + m.getName() + ", team = " + m.getTeam());
}
결론 : 멤버에도 값을 설정 member.setTeam(team) , 팀에도 값을 설정 team.getMembers().add(member);
양쪽에다 다 값을 세팅
양방향 연관관계 주의
• 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
• 연관관계 편의 메소드를 생성하자
-> 값을 넣는것을 까먹을 수도 있으므로 편의 메소드를 생성한다.
@Getter
@Setter
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public void changeTeam(Team team) { // 연관관계 주인에 값 세팅할때, 주인이 아닌쪽도 한번에 값이 세팅되게끔
this.team = team;
team.getMembers().add(this);
}
}
멤버의 팀 필드만 수정하고 팀의 멤버필드에 새로운 멤버를 추가해주지않는다면
데이터의 일관성이 깨지게된다. 멤버만 최신화되고 팀은 최신화 되지않기때문이다.
멤버와 팀은 영속성 컨텍스트의 1차캐시에서 관리되는데 1차캐시에서 관리되고있는 팀은 1차캐시에 저장된 그 상태로 유지되어있기에 최신화된 정보는 가지고있지않다.
update가 되려면 flush가 되어야하기 때문이다. 또한 1차캐시에 있는것은 과거의 것이기때문에 영속성 컨텍스트 또한 한번 clear로 비워줘야한다.
그 두가지 작업을 매번하는것은 번거로우니까 그냥 객체의 정보를 최신화해준다. ==> 연관관계 편의 메소드를 사용해서 최신화 해준다.
이렇게 함으로서
team.getMembers().add(member) 코드는 삭제해도 된다.
Member member = new Member();
member.setName("member1");
member.changeTeam(team);
chageTeam메소드 한번에 양쪽다 연관관계를 설정할 수 있다.
아니면 팀에서 연관관계 편의 메소드를 만들어서 사용할수도있다.
@Setter
@Getter
@ToString
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
// List 를 미리 초기화두는것이 관례라고 한다. ( add 할때 nullpointerexception 이 발생하지 않도록)
@OneToMany(mappedBy = "team") // 팀 하나는 여러 멤버를 가질 수 있다. 클래스 -> 필드 순으로 생각하면 될듯 mappedBY는 연관관계 주인을 설정하면된다.
private List<Member> members = new ArrayList<Member>();
public void addMember(Member member) { // 연관 관계 편의 메소드
members.add(member);
member.setTeam(this);
}
}
둘중 하나 사용하면 된다. 상황을 보고 정하면 된다.
• 양방향 매핑시에 무한 루프를 조심하자
예: toString(), lombok[@ToString], JSON 생성 라이브러리
멤버에도 toString이 있고, 팀에도 toSTring이 있다고 생각해보자
멤버에서 team필드를 위해 toString이 실행되고, team안에 member리스트를 위해 toSTring이 실행되고
또 멤버의 team 필드를 위해 toSTring이 실행되고...를 반복한다. 무한루프가 발생한다.
json생성 라이브러리 또한 필드를 타고타고 들어가면서 무한루프 발생.
결론 : 로컬에서toString(), 롬복의 toSTring() 사용 금지 ,
json 생성 라이브러리 관련 문제 해결법 -> 컨트롤러에서 엔티티를 절대 반환하지말것, 엔티티를 반환하면 자동으로 json으로 변환하는데 그 작업을 진행하는 중 무한루프가 발생할 수 있고 , 엔티티를 반환해버리면 나중에 엔티티를 변경해야할때 api 스펙이 변경되는것이다. 그래서 엔티티는 필요한 값만있는 dto로 구성해서 dto를 반환한다.
양방향 매핑 정리
• 단방향 매핑만으로도 이미 연관관계 매핑은 완료
[객체와 테이블 매핑은 단방향으로도 잘 되니까]
• 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
• JPQL에서 역방향으로 탐색할 일이 많음
• 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 됨 (테이블에 영향을 주지 않음)
[mappedBy를 이용하여]
연관관계의 주인을 정하는 기준
• 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안됨
• 연관관계의 주인은 외래 키의 위치를 기준으로 정해야함
실전 예제 - 2. 연관관계 매핑 시작
객체 구조
order 클래스의 member에 단방향 매핑을 하고 ( @joincolumn)
Member에서 orders를 추가해서 멤버 -> order 단방향 매핑을 추가 해서 양방향 매핑을 만든다.
[ orders는 mappedby로 member를 설정해서 member가 연관관계의 주인이고, orders는 조회만되는 단방향 연관관계이다.]
단방향으로 설계를 해놓고, 필요하면 양방향을 추가해야한다.
위의 그림에는 멤버클래스안에 orders라는 양방향용 필드가 존재한다.
오더클래스에는 orderitems가 양방향용 필드
멤버가 어떤 주문을 했는지를 알고있어야한다면 orders라는 양방향용 필드를 추가하고
mappedBy라는 양방향 설정을 해줘야한다.
[ 나중에 개발상 필요하면 해당 필드 추가하고, 양방향 설정을 한다. ]
order 클래스의 member_id 필드는 삭제하고
@ManyToOne // order 입장에서 member는 many to one , 한사람은 여러 주문을 가질수있다. order 입장에서는 자신을 주문한 사람은 한명
@JoinColumn(name = "MEMBER_ID") // 외래키와 매핑
private Member member;
를 추가한다.
order와 member를 양방향 관계로 가져가고싶다면 멤버에서 오더참조를 가진다.
[테이블 구조상 오더클래스의 member필드가 외래키를 관리한다. == > member필드가 연관관계의 주인이다, 멤버에서 연관관계 처리할때 mappedby("member")를 사용한다.]
[ 연관관계 주인 == @JoinColumn(name = "MEMBER_ID")를 이용하여 외래키를 관리하겠다고 지정했으니까 ]
[ 연관관계 주인 필드는 @JoinColumn을 이용하여 외래키와 매핑하고 주인이 아닌쪽은 mappedBy를 이용하여 주인인 필드와 매핑한다.]
@Entity
@Getter
@Setter
@Table(name = "MEMBER")
public class Member {
...
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
mappedBy -> "member"는 오더클래스의 member필드를 의미한다. [ 연관관계 주인으로 설정해야함]
[ 멤버 한사람은 여러 오더를 가질수있다. -> onetomany ]
@Entity
@Table(name = "ORDERS")
@Getter
@Setter
public class Order {
...
@ManyToOne // order 입장에서 member는 many to one , 한사람은 여러 주문을 가질수있다. order 입장에서는 자신을 주문한 사람은 한명
@JoinColumn(name = "MEMBER_ID") // 외래키와 매핑
private Member member;
}
@JoinColumn(name = "MEMBER_ID") ==> 해당 필드가 외래키를 관리하겠다고 지정 == 연관관계 주인 설정
비즈니스상 order가 orderitem 리스트를 가지고있는게 좋다고 판단될때
order와 orderitem이 양방향 연관관계를 가지도록 설정한다.
orderitem 클래스의 외래키를 저장하기 위한 필드는 삭제하고
@ManyToOne // 하나의 order 는 많은 orderitem 을 가질 수 있다. 오더아이템 입장에서는 오더는 하나이다.
@JoinColumn(name="ORDER_ID")
private Order order;
를 추가한다.
@JoinColumn(name="ORDER_ID")으로 order라는 필드가 외래키를 관리하는 연관관계 주인으로 매핑된것을 확인할 수 있다.
그다음 오더클래스에서 연관관계주인을 매핑할 필드를 설정한다.
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems = new ArrayList<>();
이렇게함으로서 오더와 오더아이템이 양방향 연관관계를 가진다.
이렇게하고 실제 사용할때는
연관관계 편의메소드를 만들어서
@Entity
@Table(name = "ORDERS")
@Getter
@Setter
public class Order {
@Id @GeneratedValue
@Column(name="ORDER_ID")
private Long id;
@ManyToOne // order 입장에서 member는 many to one , 한사람은 여러 주문을 가질수있다. order 입장에서는 자신을 주문한 사람은 한명
@JoinColumn(name = "MEMBER_ID") // 외래키와 매핑
private Member member;
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems = new ArrayList<>();
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
// 연관관계 편의 메소드
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
}
Order order = new Order();
order.addOrderItem(new OrderItem());
이런식으로 사용한다.
하지만 양방향 연관관계가 아니여도 어플리케이션 개발하는데 아무 문제가 없다.
Order order = new Order();
em.persist(order);
OrderItem orderItem = new OrderItem();
orderItem.setOrder(order);
em.persist(orderItem);
이런식으로 하면된다.
양방향연관관계를 사용하는 이유는 개발적 편의성을 위함이다.
[ex) 오더 엔티티를 조회했을때 바로 오더 아이템을 알고 싶을때 , 오더아이템을 알기위해 jpql이 복잡해질때]
== 연관관계의 주인이 아닌경우 조회밖에 할 수 없으므로
핵심은 할 수 있으면 최대한 단방향으로 해라.
하지만 실무에서 조회를 좀더 편하게하고 jpql를 작성할때 편하게 작성하려고 하다보니 양방향 연관관계를 생성할 일이 생기기도 한다.
만약 단방향연관관계인데 오더의 오더아이템들을 가져오고싶을때는
그냥 오더아이템을 다시 조회해오면 된다.
위의 예시에서는 멤버가 오더리스트를 가지고있었다 ( 양방향)
@Entity
@Getter
@Setter
@Table(name = "MEMBER")
public class Member {
@Id @GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
private String name;
private String city;
private String street;
private String zipcode;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
하지만 실제로 이렇게 할 필요가없다.
쿼리한번으로 가져오기만 하면되니까
멤버가 따로 필드로 가지고있어서 양방향 연관관계를 가질 필요가 없다.
Member 엔티티 자체에 모든 연관 정보들을 포함시키는 것보다,
각각의 엔티티가 필요한 곳에서 직접 참조하는 것이 더 좋은 설계 방법이라고 한다.
예를 들어, ‘내 주문내역’을 확인하기 위해서는 주문 엔티티(Members)에서 직접 해당 정보를 조회하도록 설계하는 것이 더 적합하다. 이 방법은 객체의 역할과 책임을 명확하게 분리하여 유지보수성과 확장성을 높일 수 있게 한다.
'인프런 > 자바 ORM 표준 JPA 프로그래밍 - 기본편' 카테고리의 다른 글
7) 고급 매핑 [ 상속관계 매핑 -조인,단일,각각] , MappedSuperclass (0) | 2024.05.29 |
---|---|
6) 다양한 연관관계 매핑 (0) | 2024.05.28 |
4) 엔티티 매핑,데이터베이스 스키마 자동생성 (0) | 2024.05.22 |
3) jpa 구조, 영속성 컨텍스트,영속,준영속,쓰기지연 (0) | 2024.05.20 |
2) 프로젝트설정,jpa 기초 (0) | 2024.05.20 |
댓글