자바 ORM 표준 JPA 프로그래밍 스터디 - 2주차

큰모래·2023년 4월 30일
0

객체는 참조를 통해 관계를 맺고, 테이블은 외래 키를 사용해서 관계를 맺는다.

이 패러다임의 불일치를 매핑시키는 것을 신경 써야 한다.

단방향 연관관계

  • 객체 연관관계
    • 현재 MemberTeam은 단방향 관계이다.
    • Member 객체는 Member.team 필드를 통해서 팀 객체와 연관관계를 맺는다.
    • MemberTeam 필드를 통해서 참조가 가능하지만, Team 필드는 Member 필드 참조가 불가능
  • 테이블 연관관계
    • TEAM_ID 외래 키를 통해서 연관관계를 맺는다.
    • 멤버 테이블과 팀 테이블은 양방향 관계이다.
    • 외래 키 하나로 Member join Team, Team join Member 두 개가 모두 가능하다.
  • 객체 연관관계와 테이블 연관관계의 가장 큰 차이
    • 참조를 통한 연관관계는 언제나 단방향이다.
    • 양방향 관계를 맺을려면 반대쪽 테이블에서도 참조 필드 값을 저장해야 한다.
    • 이는, 엄밀히 따지면 양방향 관계가 아닌, 서로 다른 2개의 단방향 관계이다.

순수한 객체 연관관계

public class Member {
		private String id;
		private String username;

		private Team team;

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

public class Team {
		private String id;
		private String name;
}

public static void main(String[] args) {
		Member member1 = new Member("member1", "회원1");
		Member member2 = new Member("member2", "회원2");
		Team team1 = new Team("team1", "팀1");

		member1.setTeam(team1);
		member2.setTeam(team1);
		
		Team findTeam = member1.getTeam();
}

테이블 연관관계

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 INTO TEAM(TEAM_ID, NAME) VALUES('team1', '팀1');
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME)
VALUES('member1', 'team1', '회원1');
INSERT INTO TEAM(TEAM_ID, NAME) VALUES('team1', '팀1');
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME)
VALUES('member2', 'team1', '회원2');

SELECT T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
WHERE M.MEMBER_ID = 'member1';

객체 관계 매핑

@Entity
public 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;
		}
}

@Entity
public class Team {
		
		@Id
		@Column(name = "TEAM_ID")
		private String id;

		private String name;
}
  • @ManyToOne
    • 다대일 관계 매핑 정보를 나타내는 어노테이션
  • @JoinColumn(name = “TEAM_ID”)
    • 외래 키를 매핑할 때 사용하는 어노테이션
    • name 속성에는 매핑할 외래 키 이름으로 지정한다.
    • 생략하면 기본 전략을 통해 매핑한다. (필드명 + “_” + 참조하는 테이블 컬럼명)

연관관계 사용

저장

  • JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
//팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);

//멤버1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1);
em.persist(member1);

//멤버2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1);
em.persist(member2);

조회 (객체 그래프 탐색)

Member member = em.find(Member.class, "member1");
Team team = member.getTeam();

조회 (JPQL)

  • 팀1에 소속되는 회원만 조회할 때 JPQL 객체지향 쿼리를 사용해서 조회할 수 있다.
  • 쿼리를 보면 회원이 연관관계를 가지는 팀과 조인하는 것을 확인할 수 있다.
  • where 절은 조인한 t.name을 검색 조건으로 사용해서 팀1에 속한 회원만 조회한다.
    • :teamName : :로 시작하는 것은 파라미터를 바인딩받는 문법이다.
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", "팀1")
		.getResultList();

for (Member member : resultList) {
		System.out.println(member.getUsername());  // 회원1, 회원2 출력
}

수정

  • 수정은 따로 메서드가 없다.
  • 영속 상태의 엔티티 값을 변경하면, 트랜잭션을 커밋할 때 플러시가 일어나고 변경 감지를 통해서 UPDATE 쿼리가 날라간다.
Team team2 = new Team("team2", "팀2");
em.persist(team2);

Member member = em.find(Member.class, "member1");
member.setTeam(team2);

연관관계 제거

Member member1 = em.find(Member.class, "member1");
member1.setTeam(null);

연관된 엔티티 삭제

member1.setTeam(null); //연관관계 제거
member2.setTeam(null); //연관관계 제거
em.remove(team); //팀 삭제

양방향 연관관계

회원 → 팀 (Member.team) ,다대일 관계

팀 → 회원 (Team.members) , 일대다 관계, 컬렉션(List) 사용

데이터베이스는 외래키 하나로 양방향 조회가 가능하므로 수정할 필요가 없다.

양방향 연관관계 매핑

  • mappedBy
    • 양방향 매핑일 때 사용
    • 반대쪽 매핑의 필드 이름을 설정하면 된다.
//회원 엔티티는 그대로

//팀 엔티티
@Entity
public class Team {
		
	@Id
	@Column(name = "TEAM_ID")
	private String id;

	private String name;

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

일대다 컬렉션 조회

Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers();

연관관계의 주인

엄밀히 따지면 객체는 양방향 연관관계가 아닌, 서로 다른 2개의 단뱡향 연관관계이다.

하지만, 테이블은 하나의 외래키로 양방향 연관관계를 관리한다.

엔티티를 양방향 연관관계로 설정하면, 객체의 참조는 2개인데, 외래키는 1개인 차이가 발생한다.

JPA는 이러한 문제를 해결하기 위해, 테이블 하나를 정해서 연관관계를 관리하는 연관관계의 주인으로 설정한다.

양방향 매핑의 규칙 : 연관관계의 주인

  • 연관관계의 주인만이 데이터베이스 연관관계와 매핑되어 외래 키를 관리(수정,삭제,등록)할 수 있다.
  • 주인이 아닌 쪽은 읽기만 가능하다.
  • mappedBy
    • 주인은 mappedBy 속성을 사용하지 않는다.
    • 주인이 아니면 mappedBy 속성을 통해 속성 값으로 연관관계의 주인을 가리킨다.
  • Member.team을 주인으로 설정하면 자기 테이블에 있는 외래 키를 관리하면 된다.
  • Team.members 를 주인으로 설정하면 회원 테이블에 있는 외래 키를 관리해야 한다.

연관관계의 주인은 외래 키가 있는 곳

  • 회원 테이블이 외래 키를 가지고 있으므로 Member.team을 주인으로 설정한다.
  • Team.members 에는 mappedBy를 통해 연관관계의 주인을 가리키도록 설정한다.

양방향 연관관계 저장

  • 단방향 연관관계의 저장 코드와 똑같다.
  • 양방향 연관관계는 주인이 외래 키를 관리하므로, 주인이 아닌 방향은 값을 설정하지 않아도 DB에는 외래 키 값이 정상적으로 입력된다.
  • team1.getMembers().add(member1)
    • 이런 코드가 필요해야 할 것 같지만, DB에 저장할 때 해당 코드는 무시된다.
//팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);

//멤버1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1);
em.persist(member1);

//멤버2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1);
em.persist(member2);

양방향 연관관계의 주의점

  • 주인이 아닌 곳에만 값을 입력하는 실수를 하지 말자!
  • DB에 외래 키 값이 정상적으로 저장이 안됐다면 이것부터 의심해봐야 한다.
  • 이렇게만 설정했을때, 회원 조회 시 TEAM_ID 필드에 null값으로 채워진다.
  • 연관관계의 주인만이 외래 키의 값을 변경할 수 있다.

//멤버1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1);
em.persist(member1);

//멤버2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1);
em.persist(member2);

Team team = new Team("team1", "팀1");
//주인이 아닌 곳만 연관관계 설정
team1.getMembers().add(member1);
team1.getMembers().add(member2);

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

  • 객체 관점에서는 양쪽 방향에 모두 값을 설정해주는 것이 가장 안전하다.
  • ORM은 객체와 관계형 데이터베이스 둘 다 중요하다.
  • 양쪽에 값을 설정함으로써 순수 객체 상태에서도 동작하고, 테이블의 외래 키도 정상 입력된다.
//팀1
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(team1);
team1.getMembers.add(member2);
em.persist(member2);

연관관계 편의 메서드

  • 양방향 연관관계는 값 설정 시 실수하기 쉬우므로 연관관계 편의 메서드를 사용하면 실수가 줄어든다.
  • setTeam() 메서드 하나로 양방향 관계를 모두 설정할 수 있다.
public class Member {
		
		private Team team;

		public void setTeam(Team team) {
				this.team = team;
				team.getMembers().add(this);
		}
}
//팀1
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", "회원2");
member2.setTeam(team1);
em.persist(member2);

연관관계 편의 메서드 주의사항

member1.setTeam(teamA);
member1.setTeam(teamB);
Member findMember = teamA.getMember(); // member1이 여전히 조회된다.
//기존 관계 제거
public void setTeam (Team team) {
		
		//기존 팀과 관계를 제거
		if (this.team != null) {
				this.team.getMembers().remove(this);
		}
		this.team = team;
		team.getMembers.add(this);
}
  • 영속성 컨텍스트가 계속해서 살아있는 상태에서 teamAgetMembers()를 호출 시 member1이 여전히 반환될 수 있으므로, 관계를 제거하는 것이 안전하다.

정리

  • 단뱡향 설정에서 이미 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
  • 그저, 반대방향으로의 객체 그래프 탐색 기능이 추가된 것이다.
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.
  • 비즈니스 로직상 더 중요하다고 연관관계의 주인으로 설정하면 안된다.
  • 단순히 외래 키 관리자 정도의 의미만 부여해야 한다.
profile
큰모래

0개의 댓글