JPA - 일대일 연관관계 (1)

이유석·2023년 1월 11일
3

JPA - Entity

목록 보기
7/14
post-thumbnail
post-custom-banner

일대일 연관관계를 회원(Member)과 해당 회원의 사물함(Locker)을 예시로 설명해보도록 하겠습니다.

  • 회원과 사물함이 있다.
  • 회원은 하나의 사물함을 소유할 수 있다.
  • 사물함은 한명의 회원에 의해서만 사용된다.
  • 즉, 회원과 사물함은 일대일(1:1)의 관계입니다.

일대일 연관관계

  • 일대일 (1:1) 관계는 양쪽이 서로 하나의 관계만 갖습니다.

    • 예) 회원은 하나의 사물함만 갖고, 사물함도 한명의 회원에 의해서만 사용됩니다.
  • 일대일 관계에는 주 테이블대상 테이블이 관계를 맺습니다.

    • 주 테이블 : 해당 관계에서 주체가 될 수 있는 대상 (회원)
    • 대상 테이블 : 해당 관계에서의 주체에 의해 관계가 맺어지는 대상 (사물함)
  • 일대일 관계는 주 테이블이나 대상 테이블 둘 중 어느 곳이나 외래키를 가질 수 있습니다.
    • 주 테이블에 외래키 저장
      - 객체지향 개발자들이 선호합니다.
      - 외래 키에 해당하는 대상 테이블을 객체 참조와 비슷하게 사용할 수 있기 때문입니다.
    • 대상 테이블에 외래키 저장
      - 데이터베이스 개발자들이 선호합니다.
      - 테이블 관계를 일대일 (1:1)에서 일대다 (1:N)으로 변경할 때, 테이블 구조를 그대로 유지할 수 있습니다.

주 테이블에 외래키, 단방향

객체 및 테이블 모델링

  • 회원 객체와 사물함 객체는 단방향 관계입니다.
  • 주 테이블인 회원 객체(Member)는 Member.locker 필드를 통해서 회원이 소유한 사물함 객체(Locker)에 접근할 수 있습니다.
  • 대상 테이블인 사물함 객체(Locker)는 해당 사물함을 소유하고 있는 회원 객체(Member)에 접근할 수 없습니다.

위 관계를 통해서 객체 및 테이블 모델링을 한 결과는 아래와 같습니다.

  • 테이블 연관관계에서 주 테이블에 저장된 외래키에 UNIQUE 제약조건이 추가된 것을 확인할 수 있습니다.

객체 관계 매핑

해당 객체 모델링을 코드로 나타내어 보도록 하겠습니다.

코드 설명

Member 클래스 (일대일 에서 주 테이블에 해당합니다.)

public class Member {

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

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
    
    @Column(name = "USERNAME")
    private String username;

	// Getter, Setter, Constructor
}

Locker 클래스 (일대일 에서 대상 테이블에 해당합니다.)

public class Locker {

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

    @Column(name = "NAME")
    private String name;
    
    // Getter, Setter, Constructor
}

주 테이블에 외래키를 저장하는 단방향 관계일 경우, 주 테이블에 해당하는 클래스에 대상 테이블에 해당하는 클래스를 참조 필드로 작성해주시면 됩니다.
이때, 해당 참조 필드위에 OneToOne@JoinColumn(name = "외래키 이름")을 추가하여 줍니다.

@JoinColumn

외래키를 매핑할 때 사용합니다.
자세한 속성은 JPA 단방향 연관관계 에 있는 설명과 동일합니다.

JoinColumn 어노테이션은 생략 가능합니다.
@JoinColumn을 생략하면 외래 키를 찾을 때 기본 전략을 사용합니다.
기본 전략 : 필드명 + _ + 참조하는 테이블의 컬럼명

@OneToOne

일대일 관계에서 사용합니다.

속성기능기본값
optionalfalse로 설정하면 연관된 엔티티가 항상 있어야 한다true
mappedBy연관관계의 주인 필드를 선택한다.
양방향 매핑시 사용됩니다.
fetch글로벌 패치 전략을 설정한다.
(자세한 내용은 추후에)
@OneToOne=FetchType.EAGER (즉시 로딩)
cascade영속성 전이 기능을 사용한다.
(자세한 내용은 추후에)
orphanRemovaltrue 로 설정 시, 고아 객체를 즉시 삭제합니다. false (고아 객체를 삭제하지 않습니다.)
targetEntity연관된 엔티티의 타입 정보를 설정한다.
이 기능은 거의 사용하지 않음

연관관계 사용

연관관계를 등록, 수정, 삭제, 조회하는 예제를 통해 연관관계를 어떻게 사용하는지 알아보겠습니다.

저장

// 사물함 저장
Locker locker = new Locker(0L, "사물함");
entityManager.persist(locker);

// 회원 저장
Member member = new Member(0L, "회원");
member.setLocker(locker); // 연관관계 설정 
entityManager.persist(member);
  • JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태이어야 합니다.

  • 위 코드를 실행하였을 때, 실행되는 SQL 은 아래와 같습니다.

INSERT INTO LOCKER (LOCKER_ID, NAME) VALUES (0, "사물함");
INSERT INTO MEMBER (MEMBER_ID, USERNAME, LOCKER_ID) VALUES (0, "회원", 0);

조회

연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지가 있습니다.

객체그래프 탐색
member.getLocker() 을 사용해서 member 와 연관된 locker 엔티티를 조회할 수 있습니다.

Member member = entityManager.find(Member.class, 0L);
Locker locker = member.getLocker();

객체지향 쿼리 (JPQL) 사용

String jpql = "select m.locker from Member m where m.id = :memberId";

Locker locker = entityManager
	.createQuery(jpql, Locker.class)
    .setParameter("memberId", 0L)
    .getSingleResult();
  • 위 코드를 실행하였을 때, 실행되는 SQL 은 아래와 같습니다.
SELECT l.LOCKER_ID, l.NAME
FROM LOCKER l
	JOIN MEMBER m ON l.LOCKER_ID = m.LOCKER_ID
WHERE m.MEMBER_ID = 0;

수정

회원이 소유하고 있던 사물함을 신규 사물함으로 변경해보도록 하겠습니다.

Locker locker = new Locker(1L, "신규 사물함");
entityManager.persist(locker);

Member member = entityManager.find(Member.class, 0L);
member.setLocker(locker);
  • 위 코드를 실행하였을 때, 실행되는 SQL 은 아래와 같습니다.
UPDATE MEMBER
SET LOCKER_ID = 1, ...
WHERE
	MEMBER_ID = 0;

연관관계 제거

회원의 사물함 기간이 종료되어, 소유하고 있던 사물함과의 연관관계를 제거해보도록 하겠습니다.

Member member = entityManager.find(Member.class, 0L);
member.setLocker(null);
  • 위 코드를 실행하였을 때, 실행되는 SQL 은 아래와 같습니다.
UPDATE MEMBER
SET LOCKER_ID = null, ...
WHERE
	MEMBER_ID = 0;

삭제 (orphanRemoval = false)

회원이 서비스를 탈퇴하여 회원 객체를 삭제해보도록 하겠습니다.

Member member = entityManager.find(Member.class, 0L);
entityManager.remove(member);
  • 위 코드를 실행하였을 때, 실행되는 SQL 은 아래와 같습니다.
DELETE
FROM MEMBER
WHERE MEMBER_ID = 0;

orphanRemoval 을 true 로 설정 시, 회원 객체를 삭제할 때 고아 객체가 되는 사물함 객체 역시 삭제가 됩니다.

소스코드

주 테이블 외래키, 양방향

객체 및 테이블 모델링

  • 회원 객체와 사물함 객체는 양방향 관계입니다.
  • 주 테이블인 회원 객체(Member)는 Member.locker 필드를 통해서 회원이 소유한 사물함 객체(Locker)에 접근할 수 있습니다.
  • 대상 테이블인 사물함 객체(Locker)는 해당 사물함을 소유하고 있는 회원 객체(Member)에 Locker.member 필드를 통해서 접근할 수 있습니다. 이때, 해당 member 필드는 읽기 전용입니다.

위 관계를 통해서 객체 및 테이블 모델링을 한 결과는 아래와 같습니다.

  • 테이블 연관관계에서 주 테이블에 저장된 외래키에 UNIQUE 제약조건이 추가된 것을 확인할 수 있습니다.

객체 관계 매핑

해당 객체 모델링을 코드로 나타내어 보도록 하겠습니다.

코드 설명

Member 클래스 (일대일 에서 주 테이블에 해당합니다.)

public class Member {

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

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
    
    @Column(name = "USERNAME")
    private String username;
	
    // 순수한 객체를 고려한 연관관계 편의 메서드
    public void setLocker(Locker locker) {
        if (this.locker != null) {
            this.locker.setMember(null);
        }

        this.locker = locker;
        if (locker != null) {
            locker.setMember(this);
        }
    }
    
	// Getter, Setter, Constructor
}

Locker 클래스 (일대일 에서 대상 테이블에 해당합니다.)

public class Locker {

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

    @Column(name = "NAME")
    private String name;
    
    @OneToOne(mappedBy = "locker")
    private Member member;
    
    // Getter, Setter, Constructor
}

주 테이블에 외래키를 저장하는 양방향 관계일 경우, 주 테이블에 해당하는 클래스는 주 테이블에 외래키를 저장하는 단방향 관계와 동일합니다.

대상 테이블에 해당하는 클래스에 주 테이블에 해당하는 클래스를 참조 필드로 작성해주시면 됩니다.
이때 해당 필드위에 @OneToOne(mappedBy = "반대쪽 매핑의 필드 이름값")을 추가하여 줍니다.

  • mappedBy 속성은 양방향 매핑일 때 사용하며, 반대쪽 매핑의 필드 이름값으로 설정하면 됩니다.
    이는 연관관계의 주인을 설정하기 위한 것 입니다.

연관관계 사용

연관관계를 등록, 수정, 삭제, 조회하는 예제를 통해 연관관계를 어떻게 사용하는지 알아보겠습니다.

저장

// 사물함 저장
Locker locker = new Locker(0L, "사물함");
entityManager.persist(locker);

// 회원 저장
Member member = new Member(0L, "회원");
member.setLocker(locker); // 연관관계 설정 
entityManager.persist(member);
  • JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태이어야 합니다.

  • 위 코드를 실행하였을 때, 실행되는 SQL 은 아래와 같습니다.

INSERT INTO LOCKER (LOCKER_ID, NAME) VALUES (0, "사물함");
INSERT INTO MEMBER (MEMBER_ID, USERNAME, LOCKER_ID) VALUES (0, "회원", 0);

조회 - 주 테이블에 의한 조회

객체그래프 탐색
member.getLocker() 을 사용해서 member 와 연관된 locker 엔티티를 조회할 수 있습니다.

Member member = entityManager.find(Member.class, 0L);
Locker locker = member.getLocker();

객체지향 쿼리 (JPQL) 사용

String jpql = "select m.locker from Member m where m.id = :memberId";

Locker locker = entityManager
	.createQuery(jpql, Locker.class)
    .setParameter("memberId", 0L)
    .getSingleResult();
  • 위 코드를 실행하였을 때, 실행되는 SQL 은 아래와 같습니다.
SELECT l.LOCKER_ID, l.NAME
FROM LOCKER l
	JOIN MEMBER m ON l.LOCKER_ID = m.LOCKER_ID
WHERE m.MEMBER_ID = 0;

조회 - 대상 테이블에 의한 조회

객체그래프 탐색
locker.getMember() 을 사용해서 locker 를 소유하고 있는 엔티티를 조회할 수 있습니다.

Locker locker = entityManager.find(Locker.class, 0L);
Member member = locker.getMember();

객체지향 쿼리 (JPQL) 사용

String jpql = "select l.member from Locker l where l.id = :lockerId";

Member member = entityManager
	.createQuery(jpql, Member.class)
    .setParameter("lockerId", 0L)
    .getSingleResult();
  • 위 코드를 실행하였을 때, 실행되는 SQL 은 아래와 같습니다.
SELECT m.MEMBER_ID, m.LOCKER_ID, m.USERNAME
FROM Member m
	JOIN Locker l ON m.LOCKER_ID = l.LOCKER_ID
WHERE l.LOCKER_ID = 0;

수정

회원이 소유하고 있던 사물함을 신규 사물함으로 변경해보도록 하겠습니다.

Locker locker = new Locker(1L, "신규 사물함");
entityManager.persist(locker);

Member member = entityManager.find(Member.class, 0L);
member.setLocker(locker);
  • 위 코드를 실행하였을 때, 실행되는 SQL 은 아래와 같습니다.
UPDATE MEMBER
SET LOCKER_ID = 1, ...
WHERE
	MEMBER_ID = 0;

연관관계 제거

회원의 사물함 기간이 종료되어, 소유하고 있던 사물함과의 연관관계를 제거해보도록 하겠습니다.

Member member = entityManager.find(Member.class, 0L);
member.setLocker(null);
  • 위 코드를 실행하였을 때, 실행되는 SQL 은 아래와 같습니다.
UPDATE MEMBER
SET LOCKER_ID = null, ...
WHERE
	MEMBER_ID = 0;

삭제 (orphanRemoval = false)

회원이 서비스를 탈퇴하여 회원 객체를 삭제해보도록 하겠습니다.

Member member = entityManager.find(Member.class, 0L);
entityManager.remove(member);
  • 위 코드를 실행하였을 때, 실행되는 SQL 은 아래와 같습니다.
DELETE
FROM MEMBER
WHERE MEMBER_ID = 0;

소스코드

다음 포스트에서는 일대일 연관관계 시, 대상 테이블에 외래키를 저장하는 형태에 대해서 알아보도록 하겠습니다.

profile
소통을 중요하게 여기며, 정보의 공유를 통해 완전한 학습을 이루어 냅니다.
post-custom-banner

0개의 댓글