연관관계 매핑 기초

LeeKyoungChang·2022년 3월 8일
0
post-thumbnail

자바 ORM 표준 JPA 프로그래밍 - 기본편 수업을 듣고 정리한 내용입니다.

 

객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다.

시작하기에 앞서

(1) 용어 이해

  • 방향(Direction) : 단방향, 양방향
  • 다중성(Multiplicity) : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이해
  • 연관관계의 주인(Owner) : 객체 양방향 연관관계는 관리 주인 이 필요

 

(2) 연관관계가 필요한 이유

‘객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다.’
–조영호(객체지향의 사실과 오해)

 

(3) 예제 시나리오

  • 회원과 팀이 있다.
  • 회원은 하나의 팀에만 소속될 수 있다.
  • 회원과 팀은 다대일 관계다.

 

(4) 객체를 테이블에 맞추어 모델링 (연관관계가 없는 객체)

스크린샷 2022-03-07 오후 5 32 19

객체를 테이블에 맞추어 모델링하면 연관관계가 없는 객체가 된다.
→ 참조 대신, MEMBER 테이블의 외래 키, TEAM_ID를 그대로 사용한다.

 

(5) 객체를 테이블에 맞추어 모델링 (참조 대신에 외래 키를 그대로 사용)

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name="MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @Column(name="TEAM_ID")
    private Long teamId;
	
	  ~
}

@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;
	  
		~
}
  • Team 객체를 참조하지 않고, 외래 키 값 TEAM_ID를 그대로 사용한다.
create table Member{
	MEMBER__ID bigint not null,
	TEAM_ID bigint,
	USERNAME varchar(255),
	primary key (MEMBER_ID)
}

create table Team{
	id bigint not null,
	name varchar(255),
	primary key (id)
}

 

(6) 객체를 테이블에 맞추어 모델링 (외래 키 식별자를 직접 다룸)

//팀 저장
Team team = new Team();
team.setName("TeamA"); em.persist(team);

//회원 저장
Member member = new Member();
member.setName("member1"); member.setTeamId(team.getId()); em.persist(member);
  • member.setTeamId(team.getId()); : 외래 키 식별자를 직접 다룬다.

 

(7) 객체를 테이블에 맞추어 모델링 (식별자로 다시 조회, 객체 지향적인 방법은 아니다.)

// 조회
Member findMember = em.find(Member.class, member.getId());

// 연관관계가 없음
Team findTeam = em.find(Team.class, team.getId());
  • member를 조회해왔음에도 불구하고 member가 속한 팀을 알기 위해서
    member가 가진 team.id, 즉 식별자로 테이블을 검색해서 가져와야 한다.
  • 이는 연관관계가 없기 때문이며, 객체지향적인 방법이 아니다!
스크린샷 2022-03-07 오후 6 16 13

 

⚠️ 문제점
객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.

  • 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
  • 객체는 참조를 사용해서 연관된 객체를 찾는다.

 

📚 1. 단방향 연관관계

📖 A. 객체 지향 모델링

✔️ 객체 연관관계 사용

스크린샷 2022-03-07 오후 5 32 19
  • TEAMid가 아닌 TEAM의 참조값을 그대로 가져온다.
  • MEMBER 객체의 TEAM 레퍼런스와 MEMBER 테이블의 TEAM_ID(FK)를 매핑해야 한다.

 

✔️ 객체의 참조와 테이블의 외래 키를 매핑

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name="MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

//    @Column(name="TEAM_ID")
//    private Long teamId;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
  • Member : 다수, team : 하나 → N:1(다대일 관계)
  • @JoinColumn(name = "TEAM_ID") : 객체 연관관계 TEAM 참조와 테이블 연관관계 TEAM_ID 외래 키와 매핑 → 이는 ORM 매핑이 완성된 것이다.

 

Team

@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
	
	private String name;
	// getter, setter
	...
	}
}

 

✔️ ORM 매핑

스크린샷 2022-03-07 오후 6 28 36

 

✔️ 연관관계 저장

// 팀 저장
Team team = new Team();
team.setName("TeamA"); em.persist(team);

// 회원 저장
Member member = new Member(); member.setName("member1");
member.setTeam(team); //단방향 연관관계 설정, 참조 저장 em.persist(member);
  • memberteam 참조를 저장한다.
  • 그러면 JPA가 알아서 team 엔티티에서 pk 값을 추출해서 memberfk로 설정한다.

 

실행 결과

Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    /* insert hellojpa.Team
        */ insert 
        into
            Team
            (name, TEAM_ID) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (TEAM_ID, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?)
스크린샷 2022-03-24 오후 6 41 08

 

✔️ 참조로 연관관계 조회 - 객체 그래프 탐색

//조회
Member findMember = em.find(Member.class, member.getId());

//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();

 

✔️ 연관관계 수정

// 새로운 팀B
Team teamB = new Team(); teamB.setName("TeamB"); em.persist(teamB);

// 회원1에 새로운 팀B 설정 member.setTeam(teamB);

 

📚 2. 양방향 연관관계와 연관관계 주인

✔️ 양방향 객체 연관관계
스크린샷 2022-03-08 오후 12 10 24

  • 회원과 팀은 다대일 관계이다.
  • 반대로 팀에서 회원은 일대다 관계이다.
  • 일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다.

현재, 객체 연관관계

  • 회원 → 팀 (Member.team)
  • 팀 → 회원 (Team.members)

 

✔️ 테이블 연관관계
스크린샷 2022-03-08 오후 12 10 29

TEAM 테이블이 위처럼 되는 이유

  • 내가 소속된 팀 이름을 알고 싶으면 TEAM 테이블의 TEAM_IDMEMBER 테이블에 조인하면 된다.
  • 팀에 소속된 멤버들을 알고 싶으면 MEMBERTEAM_IDTEAM 테이블에 join하면 된다.

➡️ 따라서, 데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할 수 있다.

 

📖 A. 양방향 연관관계 매핑

Member

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name="MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
	
		// Getter, Setter ...
}

 

Team

@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

	// Getter, Setter ...
}
  • 팀과 회원은 일대다 관계이다.
  • 따라서, 팀 엔티티에 List<Member> members를 추가하였다.
  • 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용하였다.
    • @OneToManymappedBy 속성은 양방향 매핑일 때 사용하는데, 반대쪽 매핑의 필드 이름을 값으로 주면 된다.
    • 반대쪽 매핑이 Member.team이므로 team을 값으로 주었다! (mappedBy는 뒤에서 알아본다.)

 

📖 B. 일대다 컬렉션 조회

일대다 방향으로 객체 그래프 탐색

  try {
//팀 저장
            Team team = new Team();
            team.setName("TeamA");
            em.persist(team);
//회원 저장
            Member member = new Member();
            member.setName("member1");
            member.setTeam(team);
            em.persist(member);

            Member member2 = new Member();
            member2.setName("member2");
            member2.setTeam(team);
            em.persist(member2);

            em.flush();
            em.clear();

            //조회
            Team team2 = em.find(Team.class, 1L);
            System.out.println("team2 = " + team2);
            List<Member> members = team2.getMembers(); // (팀 -> 회원) 객체 그래프 탐색

            for (Member  m : members) {
                System.out.println("member.username = " + m.getName());
            }
            
            tx.commit();
member.username = 회원1
member.username = 회원2

 

📖 C. 연관관계의 주인

Team@OneToMany에서 mappedBy 속성

  • 객체에는 양방향 연관관계가 없다.
  • 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐이다.
  • 반면, 데이터베이스 테이블은 외래 키 하나로 양쪽이 서로 조인할 수 있다!

➡️ 따라서, 테이블은 외래 키 하나만으로 양방향 연관관계를 맺는다!

 

✔️ 객체와 테이블이 관계를 맺는 차이
객체 연관관계 = 2개

  • 회원 → 팀 연관관계 1개(단방향)
  • 팀 → 회원 연관관계 1개(단방향)

테이블 연관관계 = 1개

  • 회원 ↔️ 팀의 연관관계 1개(양방향)

➡️ 엔터티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나이다. 따라서 둘 사이에 차이가 발생한다.

그렇다면 둘 중 어떤 관계를 사용해서 외래 키를 관리해야할까?
: JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계 주인이라고 한다.

 

✔️ 객체의 양방향 관계
스크린샷 2022-03-08 오후 1 15 34

  • 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다.
  • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
A → B (a.getB())
B → A (b.getA())

class A{
	B b;
}

class B{
	A a;
}

 

✔️ 테이블의 양방향 연관관계

스크린샷 2022-03-08 오후 1 15 37
  • 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
  • MEMBER.TEM_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

 

✔️ 양방향 매핑의 규칙: 연관관계의 주인
양방향 연관관계 매핑 시 지켜야할 규칙 : 두 연관간계 중 하나를 연관관계의 주인으로 정해야한다.
연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다.
반면에 주인이 아닌 쪽은 읽기만 할 수 있다.

어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하면 된다!

  • 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
스크린샷 2022-03-08 오후 1 25 45

그렇다면 Member.team, Team.members 둘 중 어떤 것을 연관간계의 주인으로 정해야할까?

회원 → 팀(Member.team) 방향

class Member {
 
 @ManyToOne
 @JoinColumn(name = "TEAM_ID")
 private Team team;
 // ...
}

 

팀 → 회원(Team.members) 방향

class Team {
  
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
  // ...
}

🔔 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다!

  • 그림을 보면, 여기서는 회원 테이블에 있는 TEAM_ID 외래 키를 관리할 관리자를 선택해야 된다.
  • 만약 회원 엔티티에 있는 Member.team을 주인으로 선택하면 자기 테이블에 있는 외래 키를 관리하면 된다.
  • 하지만 팀 엔티티에 있는 Team.members를 주인으로 선택하면 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 한다.
  • 왜냐하면 이 경우 Team.members가 있는 Team 엔티티는 TEAM 테이블에 매핑되어 있는데 관리해야할 외래 키는 MEMBER에 있기 때문이다.

 

✔️ 연관관계의 주인은 외래키가 있는 곳으로
연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야한다!
여기서는 회원 테이블이 외래 키를 가지고 있으므로 Member.team이 주인이 된다.
주인이 아닌 Team.members에는 mappedBy="team" 속성을 사용해서 주인이 아님을 설정해야 한다.
여기서 mappedBy의 값으로 사용된 team은 연관관계의 주인인 Member 엔티티의 team필드를 말한다!

스크린샷 2022-03-08 오후 1 38 23

 

📌 양방향 매핑 규칙 정리

  • 객체의 두 관계중 하나를 연관관계의 주인으로 지정한다.
  • 연관관계의 주인만이 외래 키를 관리할 수 있다.(등록, 수정)
  • 주인이 아닌쪽은 읽기만 가능하며 외래 키를 변경하지 못한다.
  • 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아니면 mappedBy 속성으로 주인을 지정한다.
  • 외래키가 있는 곳을 주인으로 정해야한다!

 

💡 참고

  • 데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다.
  • 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다.
  • 따라서 @ManyToOne에는 mappedBy 속성이 없다.

 

📖 D. 양방향 연관관계 저장

public void save() {
 
 // 팀1 저장
 Team team1 = new Team("team1", "팀1");
 em.persist(team1);
 
 // 회원1 저장
 Member member1 = new Member("member1", "회원1");
 member.setTeam(team1);  // 연관관계 설정 team1 -> team1
 em.persist(member1);
 
 // 회원2 저장
 Member member2 = new Member("member2", "회원2");
 member2.setTeam(team1); // 연관관계 설정 member2 -> team1
 em.persist(member2);
}
  • 팀1을 저장하고 회원1, 회원2에 연관관계의 주인인 Member.team 필드를 통해서 회원과 팀의 연관관계를 설정하고 저장했다.

데이터베이스에서 회원테이블을 조회할 시

MEMBER_IDUSERNAMETEAM_ID
member1회원1team1
member2회원2team1
  • TEAM_ID 외래 키에 팀의 기본 키 값이 저장되어 있다.
  • 양방향 연관관계는 연관관계의 주인이 외래 키를 관리한다.
  • 따라서, 주인이 아닌 방향은 값을 설정하지 않아도 데이터베이스에 외래 키 값이 정상 입력된다.
team1.getMembers().add(member1); // 무시(연관관계의 주인이 아님)
team1.getMembers().add(member2); // 무시(연관관계의 주인이 아님)
  • 이런 코드가 추가로 있어야 할 것 같지만, Team.members는 연관관계의 주인이 아니다.
  • 주인이 아닌 곳에 입력된 값은 외래 키에 영향을 주지 않는다.

 

📖 E. 양방향 연관관계의 주의점

Member member1 = new Member("member1", "회원1");
em.persist(member1);

Member member2 = new Member("member2", "회원2");
em.persist(member2);

Team team1 = new Team("team1", "팀1");
team1.getMembers().add(member1);
team1.getMembers().add(member2);

em.persist(team1);

회원1, 회원2를 저장하고 팀의 컬렉션에 담은 후에 팀을 저장한다.
위 코드를 실행한 결과, 회원 테이블을 조회한다.

MEMBER_IDUSERNAMETEAM_ID
member1회원1null
member2회원2null

외래 키 TEAM_IDteam1이 아닌 null 값이 입력되어 있는데, 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문이다. → 연관관계의 주인만이 외래 키의 값을 변경할 수 있다.
연관관계의 주인인 Member.team에 아무 값도 입력하지 않았기 때문에, TEAM_ID 외래 키의 값도 null이 저장된다.

 

📖 F. 순수한 객체까지 고려한 양방향 연관관계

정말 연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 저장하지 않아도 되는걸까?
: 사실은 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 안전하다. 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.

예를 들어 JPA를 사용하지 않고 엔티티에 대한 테스트 코드를 작성한다고 가정해볼때 ORM은 객체와 관계형 데이터베이스 둘 다 중요하다. 데이터베이스뿐만 아니라 객체도 함께 고려해야 한다.

public void test(){
 
 Team team1 = new Team("team1", "팀1");
 Member member1 = new Member("member1", "회원1");
 Member member2 = new Member("member2", "회원2");
 
 member1.setTeam(team1);
 member2.setTeam(team2);
 
 List<Member> members = team1.getMembers();
 System.out.println("members.size = " + members.size());
}
member.size = 0

코드를 보면 Member.team에만 연관관계를 설정하고 반대 방향은 연관관계를 설정하지 않았다.
마지막 줄에서 팀에 소속된 회원이 몇 명인지 출력해보면 결과는 0이 나온다.
이것은 우리가 기대하는 양방향 연관관계의 결과가 아니다.
따라서 양방향은 양쪽다 관계를 설정해야 한다.
이처럼 회원 → 팀 을 설정하면 다음 코드처럼 반대방향인 팀 → 회원도 설정해야한다.
양쪽 모두 관계를 설정한 코드

public void test(){
  
  Team team1 = new Team("team1", "팀1");
  Member member1 = new Member("member1", "회원1");
  Member member2 = new Member("member2", "회원2");
  
  member1.setTeam(team1);
  team1.getMembers().add(member1);
  
  member2.setTeam(team2);
  team1.getMembers().add(member2);
  
  List<Member> members = team1.getMembers();
  System.out.println("members.size = " + members.size());
}
member.size = 2
  • 양쪽 모두 관계를 설정하니 기대했던 결과 2가 나온다.

 

JPA를 사용해서 완성한 예제

public void test(){
  
  Team team1 = new Team("team1", "팀1");
  em.persist(team1);
  
  Member member1 = new Member("member1", "회원1");
  member1.setTeam(team1);
  team1.getMembers().add(member1);
  em.persist(member1);
  
  Member member2 = new Member("member2", "회원2");
  member2.setTeam(team2);
  team1.getMembers().add(member2);
  em.persist(member2);
}

양쪽에 연관관계를 설정한다.
순수한 객체 상태에서도 동작하며, 테이블의 외래 키도 정상 입력된다.

 

📖 G. 연관관계 편의 메서드

양방향 연관관계는 결국 양쪽 다 신경 써야 한다.
member.setTeam(team)team.getMembers().add(member)를 각각 호출하다 보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있다.

그래서 Member 클래스의 setTeam() 메서드를 수정해서 코드를 리팩토링 해보면

public class Member{
  
  private Team team;
  
  public void setTeam(Team team) {
    this.team = team;
    team.getMembers().add(this);
  }
  // ...
}

setTeam() 메서드 하나로 양방향 관계를 모두 설정하도록 변경했다.

setTeam() 메서드를 사용하는 코드

public void test() {
  
  Team team1 = new Team("team1", "팀1");
  em.persist(team1);
  
  Member member1 = new Member("member1", "회원1");
  member1.setTeam(team1);
  em.persist(member1);
  
  Member member2 = new Member("member2", "회원1");
  member2.setTeam(team1);
  em.persist(member2);
}

➡️ 이렇게 한 번에 양방향 관계를 설정하는 메서드를 연관관계 편의 메서드라고 한다.

 

📖 H. 연관관계 편의 메서드 작성 시 주의사항

member.setTeam(team1);
member.setTEam(team2);
Member findMember = teamA.getMember(); // member1이 여전히 조회된다.
  • teamB로 변경할 때 teamAmember1 관계를 제거하지 않았기 때문에 teamA.getMember() 메서드를 실행했을 때 member1이 남아있다.
  • 따라서 연관관계를 변경할 때는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 한다.

Member.setTeam()

public void setTeam(Team team){
 
 if(this.team != null) {	// this.team이 null이 아니면 이 member객체는 team이 있음을 의미
   this.team.getMembers().remove(this);		// 해당 팀의 멤버에서 삭제
 }
 this.team = team;
 team.getMembers().add(this);
}

 

JpaMain

//팀 저장
            Team team = new Team();
            team.setName("TeamA");
            em.persist(team);

            Team team2 = new Team();
            team2.setName("TeamB");
            em.persist(team2);
//회원 저장
            Member member = new Member();
            member.setName("member1");
            member.setTeam(team);
            member.setTeam(team2);
            em.persist(member);

            Member member2 = new Member();
            member2.setName("member2");
            member2.setTeam(team);
            member2.setTeam(team2);
            em.persist(member2);



            em.flush();
            em.clear();

            //조회
            Team team3 = em.find(Team.class, 1L);
            Team team4 = em.find(Team.class, 2L);
            System.out.println("team3 = " + team3);
            System.out.println("team3 = " + team4);
            List<Member> members = team3.getMembers(); // (팀 -> 회원) 객체 그래프 탐색
            List<Member> members2 = team4.getMembers(); // (팀 -> 회원) 객체 그래프 탐색

            for (Member  m : members) {
                System.out.println("member.username = " + m.getName());
            }

            for (Member  m : members2) {
                System.out.println("member2.username = " + m.getName());
            }
            
            tx.commit();

 

Member

@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 Long teamId;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    public Team getTeam() {
        return team;
    }

    public void setTeam(Team team) {
        if(this.team != null) {	// this.team이 null이 아니면 이 member객체는 team이 있음을 의미
            this.team.getMembers().remove(this);		// 해당 팀의 멤버에서 삭제
        }
        this.team = team;
        team.getMembers().add(this);
    }
	
	// getter, setter
	...
	}
}

 

Team

@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();

    public List<Member> getMembers() {
        return members;
    }
	
	// getter, setter
}

 

실행결과

member2.username = member1
member2.username = member2
스크린샷 2022-03-24 오후 6 46 32

 

📖 I. 양방향 매핑시 가장 많이 하는 실수

✔️ 연관관계의 주인에 값을 입력하지 않을 때

Team team = new Team();
team.setName("TeamA"); 
em.persist(team);

Member member = new Member();
member.setName("member1");


//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);

em.persist(member);
IDUSERNAMETEAM_ID
1member1null

 

✔️ 양방향 매핑시 연관관계의 주인에 값을 입력해야 한다.
(순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야 한다.)

Team team = new Team(); team.setName("TeamA"); 
em.persist(team);

Member member = new Member(); member.setName("member1");


team.getMembers().add(member); //연관관계의 주인에 값 설정
member.setTeam(team); //**
em.persist(member);
IDUSERNAMETEAM_ID
1member12

 

  • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
  • 연관관계 편의 메소드를 생성하자
  • 양방향 매핑시에 무한 루프를 조심하자

lombok 사용시 toString()을 사용하지 말자
JSON 생성 라이브러리 : 컨트롤러에서는 entity를 절대로 반환하지 말자!

  • 무한루프 문제
  • entityapi로 반환시 api 스택이 변경된다.
  • entity는 컨트롤러에 dto를 반환해서 사용해야한다.

 

✔️ 양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료된다. 단방향으로 설계(만든 후), 양방향을 추가하자!
  • 일대 다일 때, 다대 일에는 연관관계를 단방향 매핑으로 설계를 끝낸후, 애플리케이션 개발할 때 양방향 매핑을 설계한다.
  • 객체설계할 때 양방향 설계는 별로 좋지 않다.

 

✔️ 연관관계의 주인을 정하는 기준

  • 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안된다!
  • 연관관계의 주인은 외래 키의 위치를 기준으로 정해야한다!

 

📚 3. 실전 예제 2 - 연관관계 매핑 시작

✔️ 테이블 구조

테이블 구조는 실전 예제 1 와 같다

스크린샷 2022-03-08 오후 4 33 36

 

✔️ 객체 구조

실전 예제 1과 다르게 참조를 사용하도록 변경한다.

스크린샷 2022-03-08 오후 4 34 30

 

✔️ 소스
Item

package jpabook.jpashop.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Item {

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;        //이름
    private int price;          //가격
    private int stockQuantity;  //재고수량

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public int getStockQuantity() {
        return stockQuantity;
    }

    public void setStockQuantity(int stockQuantity) {
        this.stockQuantity = stockQuantity;
    }
}

 

Member

package jpabook.jpashop.domain;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
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<>();

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public String getZipcode() {
        return zipcode;
    }

    public void setZipcode(String zipcode) {
        this.zipcode = zipcode;
    }
}

 

Order

package jpabook.jpashop.domain;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Entity
@Table(name = "ORDERS")
public class Order {

    @Id
    @GeneratedValue
    @Column(name = "ORDER_ID")
    private Long id;

    @ManyToOne
    @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);
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Member getMember() {
        return member;
    }

    public void setMember(Member member) {
        this.member = member;
    }

    public LocalDateTime getOrderDate() {
        return orderDate;
    }

    public void setOrderDate(LocalDateTime orderDate) {
        this.orderDate = orderDate;
    }

    public OrderStatus getStatus() {
        return status;
    }

    public void setStatus(OrderStatus status) {
        this.status = status;
    }
}

 

OrderItem

package jpabook.jpashop.domain;

import javax.persistence.*;

@Entity
@Table(name = "ORDER_ITEM")
public class OrderItem {

    @Id
    @GeneratedValue
    @Column(name = "ORDER_ITEM_ID")
    private Long id;

    @ManyToOne
    @JoinColumn(name="ORDER_ID")
    private Order order;

    @ManyToOne
    @JoinColumn(name ="ITEM_ID")
    private Item item;

    public Order getOrder() {
        return order;
    }

    public void setOrder(Order order) {
        this.order = order;
    }

    public Item getItem() {
        return item;
    }

    public void setItem(Item item) {
        this.item = item;
    }

    private int orderPrice; //주문 가격
    private int count;      //주문 수량

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }


    public int getOrderPrice() {
        return orderPrice;
    }

    public void setOrderPrice(int orderPrice) {
        this.orderPrice = orderPrice;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }
}

 

OrderStatus

package jpabook.jpashop.domain;

public enum OrderStatus {
    ORDER, CANCEL
}

 

JpaMain

package jpabook.jpashop;

import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderItem;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {

    public static void main(String[] args) {
        EntityManagerFactory emf =  Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {

            Order order = new Order();
            em.persist(order);

            OrderItem orderItem = new OrderItem();
            orderItem.setOrder(order);

            em.persist(orderItem);
//            order.addOrderItem(new OrderItem());


            tx.commit();
        }catch (Exception e){
            tx.rollback();
        } finally {
            em.close();
        }

        emf.close();

    }
}

 


참고

profile
"야, (오류 만났어?) 너두 (해결) 할 수 있어"

0개의 댓글