연관 관계 매핑 - 기초

상윤·2024년 5월 13일
0

BackEnd

목록 보기
7/11

엔티티들은 대부분 다른 엔티티와 연관관계가 있다. 그런데 객체 참조를 사용해서 관계를 맺고, 테이블은 외래 키를 사용해서 관계를 맺는다. 이 둘은 완전히 다른 특징을 갖는다. 오늘은 이를 정리하는 시간을 가져보려 한다.

단방향 연관관계

연관관계 중에선 다대일(N:1) 단반향 관계를 가장 먼저 이해해야 한다.
회원과 팀 관계를 통해 단방향 연관관계에 대해 알아보자

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

객체 연관관계
회원 객체는 Member.team 필드(멤버변수)로 팀 객체와 연관을 갖는다.
회원 객체와 팀 객체는 단방향 관계이다. 회원은 Member.team 필드를 통해서 팀을 알 수 있지만 반대로 팀은 회원을 알 수 없다.
member -> team 의 조회는 member.getTeam()으로 가능
team -> member 를 접근하는 필드는 없다.

테이블 연관관계
회원 테이블은 TEAM_ID 외래 키로 팀 테입르과 연관관계를 맺는다.
회원 테이블과 팀 테이블은 양방향 연관관계이다. 회원의 TEAM_ID 외래키를 통해 회원과 팀 조인이 가능하다.
MEMBER 테이블의 TEAM_ID 외래 키 하나로 MEMBER JOIN TEAM 과 TEAM
JOIN MEMBER 둘 다 가능하다.

-SQL query

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

객체 연관관계와 테이블 연관관계의 차이점
참조를 통한 연관관계는 언제나 단방향이다. 양방향으로 만들고 싶을 땐 반대쪽에도 필드를 추가해서 참조를 보관해야 한다. 결국 연관관계를 하나 더 만들어야 한다. 이렇게 양쪽에서 서로 참조하는 것을 양방향 연관관계라 한다. 하지만 정확한 해석은 서로 다른 단방향 관계 2개이다.
반면 테이블은 외래 키 하나로 양방향으로 조인할 수 있다.

객체는 참조(주소)로 연관관계를 맺는다.
테이블은 외래 키로 연관관계를 맺는다.
객체 참조는 get 메서드를 사용하지만 테이블 조인은 JOIN을 사용한다.
외래 키를 사용하는 테이블의 연관관계는 양방향이다. (A JOIN B 가 가능 시 B JOIN A 가 가능)

이처럼 객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라 한다.

테이블 연관관계
회원 테이블과 팀의 관계

CREATE TABLE MEMBER (
    MEMBER_ID VARCHAR(255) NOT NULL,
    TEAM_ID VARCHAR(255),
    USERNAME VARCHAR(255),
    PRIMARY KEY (MEMBER_ID) 
)

CREATE TABLE TEAM (
    TEAM_ID VARCHAR(255) NOT NULL,
    NAME VARCHAR(255), 
    PRIMARY KEY (TEAM_ID) 
)

ALTER TABLE MEMBER ADD CONSTRAINT FK_MEMBER_TEAM
  FOREIGN KEY (TEAM_ID)
  REFERENCES TEAM
  
INSERT QUERY
INSERT INTO TEAM (TEAM_ID, NAME) VALUES (’ teaml ’, ’ 팀1 ’);
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME)
VALUES (’memberl ’, ’ teaml ’, ’ 회원 1 ’);
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME)
VALUES (’member2' z ’ teaml', ' 회원2 *);

SELECT QUERY
SELECT T.* FROM MEMBER M
	JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
WHERE M.MEMBER_ID = 'memberl*

객체 관계 매핑

매핑한 회원 엔티티
@ManyToOne
@ JoinColumn (name="TEAM_ID")
private Team team;

매핑한 팀 엔티티
@Id
@Column (name = "TEAM_ID") 
private String id;

객체 연관관계 : 회원 객체의 Member.team 필드 사용
테이블 연관관계 : 회원 테이블의 MEMBER.TEAM_ID 외래 키 컬럼을 사용

Member.team과 MEMBERM.TEAM_ID를 매핑하는 것이 연관관계 매핑이다.

@ManyToOne : 이름 그래도 다대일(N:1) 관계라는 매핑 정보다. 회원과 팀은 다대일 관계다. 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야한다.
@JoinColumn(name="TEAM_ID") : 조인 컬럼은 외래 키를 매핑할 때 사용한다. name 속성에는 매핑할 외래 키 이름을 지정한다. 회원과 팀 테이블은 TEAM_ID 외래 키로 연관관계를 맺으므로 이 값을 지정하면 된다. 이 어노테이션은 생략할 수 있다.

JoinColumn

JoinColumn은 외래 키를 매핑할 때 사용한다.

referenceColumnName: 외래 키가 참조하는 대상 테이블의 컬럼명(기본값은 기본키 컬럼명)
foreignKey(DDL): 외래 키 제약조건을 직접 지정할 수 있다.
속성 : unique,nullable,insertable,updatable,columnDefinition,table

©JoinColumn 생략 시
@ManyToOne
private Team team;

기본 전략: 필드명 + + 참조하는 테이블의 컬럼명
필드명(team) +
(밑줄) + 참조하는 테이블의 컬럼명(TEAMJD) =
team_TEAMJD 외래 키를 사용한다.

@ManyToOne

다대일 관계에서 사용하는 어노테이션 @ManyToOne
optional : false로 설정하면 연관된 엔티티가 항상 있어야 한다.(default : true)
fetch : 글로벌 페치 전략을 설정
default
•@ManyT oOne=FetchT ype. EAGER
•@OneT oMany=FetchT ype. LAZY
casecade 영속성 전이 기능 사용
targetEntity : 연관된 타입 정보를 설정.(제네릭으로 타입 정보를 알 수 있으므로 잘 사용하지 않음)

CRUD

저장

public void testSave() {
    //팀1 저장
    Team teaml = new Team("teaml", "팀 1"); 
    em.persist(teaml);
    //회원 1 저장
    Member memberl = new Member("memberl", "회원 1");
    memberl.setTeam(teaml); //연관관계 설정 member! -> team1		
    em.persist(memberl);
    //회원2 저장
    Member member2 = new Member ("member2", "회원2") ;
    member2. setTeam (teaml); //연관관계 설정 member2 -> team1 
    em.persist(member2);
}

JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
EntityManager를 사용해 team을 저장하고, member를 저장한다.
JPA는 참조한 팀의 식별자를 외래키로 사용해서 적절한 등록 쿼리를 생성한다.

JPA의 등록 쿼리
INSERT INTO TEAM (TEAM_ID, NAME) VALUES ('teaml', ’팀 1’)
INSERT INTO MEMBER (MEMBER_ID, NAME, TEAM_ID) VALUES (’memberl*,
	'회원 1\ 'teaml')
INSERT INTO MEMBER (MEMBER_ID, NAME, TEAM_ID) VALUES (,member2,, 
	'회원 2’, 'teaml’)

조회
객체 그래프 탐색
객체지향 쿼리 사용

객체 그래프 탐색
member.getTeam()을 사용하여 member와 관련된 team 엔티티 조회

Member member = em.find(Member.class, "member1");
Team team = member.getTeam (); //객체 그래프 탐색
System.out.printin ("팀 이름 = " + team.getName ());

객체지향 쿼리 사용 JPQL

JOIN 검색
private static void queryLogicJoin(EntityManager em) {
  String jpql = "select m from Member m join m.team t where " + "t.name=:teamName";
  
  List<Member> resultList = em.createQuery(jpql, Member.class) . setParameter ("teamName", "팀!'') ;
  .getResultList();
  
  for (Member member : resultList) {
  	System.out.printin("[query] member.username=" + member.getUsername());
  }
}

member가 갖고 있는 team필드를 통해 join. where 절에서 조인한 t.name을 검색 조건으로 사용해서 팀1에 속한 회원만 검색.
:teamName 과 같이 :로 시작하는 것은 파라미터 바인딩을 받는 문법임

실행되는 쿼리
SELECT M.* FROM MEMBER MEMBER 
INNER JOIN
	TEAM TEAM ON MEMBER.TEAM_ID = TEAM1_.ID WHERE
TEAM1_.NAME=* 팀1'

수정

private static void updateRelation(EntityManager em) {
  //새로운 팀2
  Team team2 = new Team("teain2","팀2");
  em.persist(team2);
  //회원 1 에 새로운 팀2 설정
  Member member = em. find (Member. class, "memberl");
  member.setTeam(team2);
}

실행 쿼리
UPDATE MEMBER
SET
	TEAM_ID=’team2’,
WHERE
	ID='memberl’

엔티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변 경 감지 기능이 작동한다. 그리고 변경사항을 데이터베이스에 자동으로 반영한다.이것은 연관관계를 수정할 때도 같은데, 참조하는 대상만 변경하면 나머지는 JPA 가 자동으로 처리 한다.

연관관계 제거

private static void deleteRelation(EntityManager em) {
	Member memberl = em.find(Member.class, "memberl”) ; memberl. setTeam(null); //연관관계 제거 
}
UPDATE MEMBER
SET 
	TEAM_ID=null, ...
WHERE
	ID='memberl'

연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다.

memberl. setTeam(nul1); //회원1 연관관계 제거
member2. setTeam(null); //회원2 연관관계 제거
em. remove (team); //팀 삭제

양방향 연관관계

객체 연관관계를 살펴봤을 때, 회원과 팀은 다대일 관계지만 팀에서 회원은 일대다 관계이다. 일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다. Team.members를 List 컬렉션으로 추가한다.

양방향 연관관계 매핑

@Entity
pub丄ic class Member {
    @Id
    @Column(name = "MEMBER_ID") 
    private String id;
    private String username;
    @ManyToOne
    @JoinColumn(name="TEAM_ID") 
    private Team team;
    //연관관계 설정
    public void setTeam(Team team) { 
    	this.team = team;
    }
    
    //Getter, Setter ... 
}

@Entity
public class Team {
  @Id
  @Column(name = "TEAM_ID")
  private String id;
  
  private String name;
  
  //==추가==//
  @OneToMany (mappedBy = "team")
  private List<Member> members = new ArrayList<Member> () ;
  
  //Getter, Setter ...
}

일대다 관계에선 팀 엔티티에 컬렉션인 List를 사용해서 Member를 추가했다. 그리고 일대다 관계를 매핑하기 위해 @OneToMany 매핑을 사용해했다. mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드값 이름을 주면 된다.

일대다 컬렉션 조회

public void biDirection() {
  Team team = em.find(Team.class, "teaml");
  List<Member> members = team.getMembers (); //(팀 -> 회원)
  //객체 그래프 탐색
  for (Member member : members) {
  	System. out. print In ("member. username = ’’ + member.getUsername());
  }
}

연관관계 주인
mappedBy는 역할이 무엇이고 왜 필요할까?
객체에는 양방향 연관관계라는 것이 없다. 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 묶어 양방향인 것 처럼 보이게 할 뿐이다.
테이블은 왜래 키 하나로 두 테이블의 연관관계를 관리한다. 엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외래 키를 관리하면 된다. 그런데 엔티티를 양방향으로 매핑하면 회원 -> 팀, 팀->회원 두 곳에서 서로를 참조한다. 따라서 객체의 연관관계를 관리하는 포인트는 2곳으로 늘어난다.
엔티티를 양방향 연과관계로 설정하면 객체의 참조는 둘인데 외래키는 하나다. 따라서 둘 사이에 차이가 발생한다. 이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해 테이블의 외래키를 관리해야 한다. 이것을 연관관계 주인이라 한다.

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

주인은 mappedBy 속성을 사용하지 않는다.
주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.

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

연관관계의 주인을 정한다는 뜻은 외래 키 관리자를 선택하는 것으로 외래키를 갖고 있는 테이블의 엔티티를 선택해야한다

양방향 연관관계의 주의점

양방향 연관관계를 설정하고가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않는 것이다.
데이터베이스에 외래 키 값이 정상적으로 저장되지 않으면 이것부터 의심해야 한다.

public void testSaveNonOwner() {
  //회원1 저장
  Member memberl = new Member("memberl", "회원 1"); em.persist(memberl);
  
  //회원2 저장
  Member member2 = new Member ("member2", "회원2"); em.persist(member2);
  Team teaml = new Team("teaml", "팀 1"); //주인이 아닌 곳만 연관관계 설정
  teaml.getMembers().add(memberl);
  teaml. getMembers () . add (member2) ;
  
  em.persist(teaml);
}

외래키 TEAM_ID에 team1이 아닌 null 값이 출력될 것이다. 이는 연관관계 주인이 아닌 Team.members에만 값을 저장했기 때문이다. 연관 관계의 주인만이 외래 키의 값을 변경 할 수 있다.

public void testORM_양방향 () {
  //팀1 저장
  Team teaml = new Team("teaml", "팀 1"); 
  em.persist(teaml);
  
  Member memberl = new Member ("memberl", "회원 1");
  //양방향 연관관계 설정
  memberl.setTeam(teaml); //연관관계 설정 memberl -> teaml
  teaml.getMembers () .add (memb은rl);//연관관계 설정 teaml -> memberl 
  em.persist (memberl);
  
  Member member2 = new Member ("member2", "회원2");
  //양방향 연관관계 설정
  member2.setTeam(teaml); //연관관계 설정 member2 -> teaml
  teaml.getMembers () .add (member2);//연관관계 설정 teaml -> member2 
  em.persist(member2);
}

양쪽에 연관관계를 설정했다. 따라서 순수한 객체 상태에서도 동작하며, 테이블의 외래 키도 정상 입력된다. 물론 외래 키의 값은 연관관계의 주인인 Member.team 값을 사용한다.

member 1. setTeam (teaml); //연관관계의 주인
teaml. getMembers () .add(memberl); //주인이 아니다. 저장 시 사용되지 않는다.

■Member.team: 연관관계의 주인, 이 값으로 외래 키를 관리한다.
■Team.members: 연관관계의 주인이 아니다. 따라서 저장 시에 사용되지 않는다.

연관관계 편의 메소드
양방향 연관관계는 결국 양쪽 다 신경 써야 한다. 다음처럼 member.setTeam(team) 과 team.getMembers().add(member) 를 각각 호출하다 보면 실수로 둘 중 한 곳만 호출하여 양방향이 깨질 수 있다. 때문에 두 코드는 하나인 것처럼 사용하는 것이 안전하다.

public class Member {

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

setTeam() 메소드를 수정해서 하나로 양방향 관계를 모두 설정하도록 변경하였다.
이렇게 한 번에 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드라 한다.

주의사항
메소드의 버그

memberl.setTeam(teamA)를 호출한 직후 객체 연관관계인 그림

teamB로 변경할 때 teamA —> member 1 관계를 제거
하지 않았다. 연관관계를 변경할 때는 기존 팀이 있으면 기존 팀과 회원의 연관관
계를 삭제하는 코드를 추가해야 한다.

public void setTeam(Team team) {
    //기존 팀과 관계를 제거
    if (this.team != null) {
    	this.team.getMembers{).remove(this);
    }
    this.team = team;
    team.getMembers().add(this);
}

0개의 댓글