SpringBoot 연관 관계 매핑, 지연로딩, 영속성 전이

김정훈·2024년 7월 24일

Spring

목록 보기
22/24

연관 관계 매핑

1. 일대일(1:1) : @OneToOne

회원1 - 프로필1

@Builder
@Data
@Entity
@NoArgsConstructor @AllArgsConstructor

public class Member extends BaseEntity {
    @Id @GeneratedValue
    private Long seq;

    @Column(length=60, nullable = false, unique = true)
    private String email;

    @Column(length=65, nullable = false)
    private String password;

    @Column(length=40, nullable = false, name="name")
    private String userName;

    // @Lob
    @Transient
    private String introduction;

    @Column(length=10)
    @Enumerated(EnumType.STRING)
    private Authority authority;
    
    @OneToOne(mappedBy = "profile_seq")
    private MemberProfile profile;
}
@Data
@Builder
@Entity
@NoArgsConstructor @AllArgsConstructor
public class MemberProfile {
    @Id @GeneratedValue
    private Long seq;
    private String profileImage;
    private String status;
    
    @OneToOne(mappedBy="profile")
    @ToString.Exclude
    private Member member;
}
@SpringBootTest
@ActiveProfiles("test")
@Transactional
public class Ex10 {

    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private MemberProfileRepository profileRepository;

    @PersistenceContext
    private EntityManager em;

    @BeforeEach
    void init(){
        MemberProfile profile = MemberProfile.builder()
                .profileImage("이미지")
                .status("상태")
                .build();
        profileRepository.saveAndFlush(profile);

        Member member = Member.builder()
                .email("user01@test.org")
                .password("123456678")
                .userName("사용자91")
                .authority(Authority.USER)
                .profile(profile)
                .build();
        memberRepository.saveAndFlush(member);

        em.clear();
    }

     @Test
    public void test(){
        Member member = memberRepository.findById(1L).orElse(null);
        MemberProfile profile = member.getProfile();
        System.out.println(profile);
    }
    
    @Test
    public void test2(){
        MemberProfile profile = profileRepository.findById(1L).orElse(null);
        Member member =  profile.getMember();
        System.out.println(member);
    }
}

test1, member에서 profile조회

test2, profile에서 member조회

2. 일대다(1:N) : @OneToMany

Meber - BoardData 관계
ManyToOne이 존재해야 사용가능
mappedBy 연관관계 주인 설정 - 관계의 주인의 외래키쪽

lombok의 toString() 함께 사용시 순환참조 문제 발생 가능성

  • toString()을 구성할 때 getter 메서드를 사용해서 구성하기 때문
  • BoardData 👉 toString() 👉 getMember() 👉 toString() 👉 List<BoardData> itmes 👉 toString() 👉 getMember() ...

해결방법

  • toString을 멤버 변수를 직접 출력하는 것으로 직접 정의 👉 ❌ 해야할게 많음

  • @ToString.Exclude 👉 ToString 포함 배제 (⭕️)
  • @ToString.Include 👉 ToString 포함
@Builder
@Data
@Entity
@NoArgsConstructor @AllArgsConstructor

public class Member extends BaseEntity {
    @Id @GeneratedValue
    private Long seq;

    @Column(length=60, nullable = false, unique = true)
    private String email;

    @Column(length=65, nullable = false)
    private String password;

    @Column(length=40, nullable = false, name="name")
    private String userName;

    // @Lob
    @Transient
    private String introduction;

    @Column(length=10)
    @Enumerated(EnumType.STRING)
    private Authority authority;

    @ToString.Exclude //ToString 추가 배제
    @OneToMany(mappedBy = "member") //BoardData 쪽에 있는 member를 가르킴 (관계의주인)
    private List<BoardData> items;
}
	@Test
    void test2(){
        Member member = memberRepository.findById(1L).orElse(null);
        List<BoardData> items = member.getItems();
        items.forEach(System.out::println);
    }

3. 다대일(N:1) : @ManyToOne

  • Many쪽에 정의해야한다.

  • 가장 많이 사용

  • 여러개의 게시글데이터가 한명의 회원으로 연결, 게시글 - 회원

  • 부모 : 회원(One) , 자식 : 게시글(Many)

    • Many - 외래키를 가지고 있다. 자식테이블, 연관관계의 주인
    • One - 부모테이블
  • 관계를 바꿀수 있는 주체는 자식임 👉 외래키가 자식에게 있으니까

BoardData - member 관계

@Data
@Builder
@Entity
@AllArgsConstructor
@NoArgsConstructor
public class BoardData extends BaseEntity {
    @Id @GeneratedValue //시퀀스객체 auto
    private Long seq;

    @ManyToOne //Member쪽이 one, member_seq(엔티티명_기본키 속성명)
    private Member member;

    @Column(nullable = false)
    private String subject;

    @Column(nullable = false)
    @Lob
    private String content;
}

자동으로 외래키가 생성된다. (엔티티명_기본키 속성명)

@SpringBootTest
@ActiveProfiles("test")
@Transactional
public class Ex09 {

    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private BoardDataRepository boardDataRepository;

    @PersistenceContext
    private EntityManager em;

    @BeforeEach
    void init(){
        Member member = Member.builder()
                .email("user01@test.org")
                .password("12345678")
                .userName("사용자01")
                .authority(Authority.USER)
                .build();

        memberRepository.saveAndFlush(member);

        List<BoardData> items = IntStream.rangeClosed(1,10)
                .mapToObj(i -> BoardData.builder()
                        .subject("제목"+i)
                        .content("내용"+i)
                        .member(member)
                        .build()).toList();

        boardDataRepository.saveAllAndFlush(items);
        em.clear();

    }

    @Test
    void test1(){
        BoardData item = boardDataRepository.findById(1L).orElse(null);
        Member member = item.getMember();
        System.out.println(item);
    }
}

결과
Left Join 확인

left join
	member m1_0
    	on m1_0.seq=bd1_0.m_seq

4. 다대다(N:M) : @ManyToMany

중간테이블 하나 생성
BoardData - HashTag

게시글1 태그1 태그4
게시글2 태그1 태그2
게시글3 태그3 태그4

태그1 - 게시글1, 게시글2
@Data
@Builder
@Entity
@NoArgsConstructor @AllArgsConstructor
public class BoardData extends BaseEntity {
    @Id @GeneratedValue
    private Long seq;

    @ManyToOne // member_seq - 엔티티명_기본키 속성명
    @JoinColumn(name="mSeq")
    private Member member;

    @Column(nullable = false)
    private String subject;

    @Lob
    private String content;

    @ManyToMany
    @ToString.Exclude
    private List<HashTag> tags;
}
@Data
@Entity
@Builder
@NoArgsConstructor @AllArgsConstructor
public class HashTag {

    @Id
    private String tag;
    
    @ManyToMany(mappedBy = "tags")
    private List<BoardData> items;
}

중간테이블 자동생성

@SpringBootTest
//@Transactional
public class Ex11 {

    @Autowired
    private BoardDataRepository boardDataRepository;

    @Autowired
    private HashTagRepository hashTagRepository;

    //@PersistenceContext
    //private EntityManager em;

    @BeforeEach
    void init(){
        List<HashTag> tags = IntStream.rangeClosed(1,5)
                .mapToObj(i -> HashTag.builder()
                        .tag("태그" + i).build()).toList();

        hashTagRepository.saveAllAndFlush(tags);

        List<BoardData> itmes = IntStream.rangeClosed(1,5)
                .mapToObj(i -> BoardData.builder()
                        .subject("제목" + i)
                        .content("내용" + i)
                        .tags(tags)
                        .build()).toList();

        boardDataRepository.saveAllAndFlush(itmes);
    }

    //게시판으로 태그조회
    @Test
    void test1(){
        BoardData item = boardDataRepository.findById(1L).orElse(null);
        List<HashTag> tags = item.getTags();
        tags.forEach(System.out::println);
    }
    
    //태그로 게시판조회
    @Test
    void test2(){
        HashTag tag = hashTagRepository.findById("태그2").orElse(null);
        List<BoardData> items = tag.getItems();
        items.forEach(System.out::println);
        
    }
}

중간테이블

참고) 일대다, 다대일 차이

둘은 같은 관계를 서로 다른 관점에서 설명한 것입니다. 다대일은 여러 개의 엔티티가 하나의 엔티티에 연결된다는 점을 강조하고, 일대다는 하나의 엔티티가 여러 개의 엔티티에 연결된다는 점을 강조합니다. 같은 관계를 다대일 관점과 일대다 관점 중 어느 쪽에서 보는지에 따라 표현이 달라질 뿐, 본질적으로 동일한 관계를 설명하는 것입니다.

5. @JoinColumn

조인되는 컬럼의 이름을 변경

@ManyToOne //Member쪽이 one,  member_seq(엔티티명_기본키 속성명) 외부키 생성
@JoinColumn(name="mSeq") //외부키 이름 변경
private Member member;

지연로딩

지연 로딩(Lazy Loading)은 데이터베이스나 객체 관계 매핑(ORM) 시스템에서 자주 사용되는 패턴으로, 실제로 필요한 시점까지 데이터를 로드하지 않는 방법입니다. 이는 성능 최적화와 자원 관리를 위해 매우 유용합니다. 이 패턴을 사용하면 초기 로드 시점에 모든 데이터를 가져오지 않고, 필요한 데이터만 그때그때 로드하게 됩니다. 👉 성능향상
글로벌 전략으로 지연로딩, 필요할 때만 즉시 로딩 전략 사용
but, 목록을 조회할때는 목록 갯수만큼 쿼리조회를 수행하게됨 👉 해결책 : Fetch조인

Fetch 조인

필요한 엔티티만 즉시 로딩 전략을 사용

1) JPQL 직접 정의 : @Query 애노테이션

public interface BoardDataRepository extends JpaRepository<BoardData, Long>, QuerydslPredicateExecutor<BoardData> {
    @Query("SELECT b FROM BoardData b LEFT JOIN FETCH b.member")
    List<BoardData> getAllList();
}

그냥 JOIN할경우

FETCH JOIN할경우 LEFT JOIN

2) @EntityGraph

쿼리메서드 사용시 정의 가능

public interface BoardDataRepository extends JpaRepository<BoardData, Long>, QuerydslPredicateExecutor<BoardData> {
    @EntityGraph(attributePaths = "member")
    List<BoardData> findBySubjectContaining(String key);
}

3) QueryDsl의 fetchJoin()

JPAQueryFactory : 생성자 매개변수 - EntityManger
JPAQuery

@SpringBootTest
@ActiveProfiles("test")
@Transactional
public class Ex12 {
    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private BoardDataRepository boardDataRepository;

    @PersistenceContext
    private EntityManager em;

    @Test
    void test4(){
        QBoardData boardData = QBoardData.boardData;
        JPAQueryFactory factory = new JPAQueryFactory(em);
        JPAQuery<BoardData> query = factory
                .selectFrom(boardData)
                .leftJoin(boardData.member)
                .fetchJoin();
        List<BoardData> items = query.fetch();
    }
}

4) @BatchSize

@BatchSize(size = 10)
적용 전
SELECT ... FROM BoardData
SELECT ... FROM Member WHERE seq = 1L
SELECT ... FROM Member WHERE seq = 2L
SELECT ... FROM Member WHERE seq = 3L
...

적용 후
SELECT ... FROM BoardData
SELECT ... FROM Member WHERE seq IN(1L, 2L, 3L)

1. FetchType.EAGER

즉시 로딩 - 처음부터 JOIN

2. FetchType.LAZY

지연 로딩, 처음에는 현재 엔티티만 조회, 다른 매핑된 엔티티는 사용할때만 2차 쿼리 실행
@Transcational 애노테이션과 함계 많이 사용 : 2차 쿼리 실행때 데이터가 영속이 없을 경우 조회가 안되는 상황 발생하기 때문에.

@Data
@Builder
@Entity
@NoArgsConstructor @AllArgsConstructor
public class BoardData extends BaseEntity {
    @Id @GeneratedValue
    private Long seq;

    @ManyToOne(fetch = FetchType.LAZY) // member_seq - 엔티티명_기본키 속성명
    @JoinColumn(name="mSeq")
    private Member member;

    @Column(nullable = false)
    private String subject;

    @Lob
    private String content;

    @ManyToMany
    @ToString.Exclude
    private List<HashTag> tags;
}
public class Member extends BaseEntity {
    @Id /* @GeneratedValue(strategy = GenerationType.AUTO) */ @GeneratedValue
    private Long seq;

    @Column(length=60, nullable = false, unique = true)
    private String email;

    @Column(length=65, nullable = false)
    private String password;

    @Column(length=40, nullable = false, name="name")
    private String userName;

    // @Lob
    @Transient
    private String introduction;

    @Column(length=10)
    @Enumerated(EnumType.STRING)
    private Authority authority;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="profile_seq")
    @ToString.Exclude
    private MemberProfile profile;

    @ToString.Exclude // ToString 추가 배제
    @OneToMany(mappedBy = "member")
    private List<BoardData> items;
}

영속성 전이

@OneToMany
부모 엔티티의 영속성 변화 상태 👉 자식 엔티티에 전달

1. CASCADE 종류

CASCADE 종류설명
PERSIST부모 엔티티가 영속화될 때 자식 엔티티도 영속화
MERGE부모 엔티티가 병합될 때 자식 엔티티도 병합
REMOVE부모 엔티티가 삭제될 때 연된된 자식 엔티티도 삭제
REFRESH부모 엔티티가 refresh되면 연관된 자식 엔티티도 refresh
DETACH부모 엔티티가 detach 되면 연관된 자식 엔티티도 detach 상태로 변경
ALL부모 엔티티의 영속성 상태 변화를 자식 엔티티에 모두 전이

REMOVE
제약조건 CASCADE ON DELETE과는 다름.
자식 레코드 삭제 이후 부모 레코드 삭제.

@Builder
@Data
@Entity
@NoArgsConstructor @AllArgsConstructor
public class Member extends BaseEntity {
    @Id /* @GeneratedValue(strategy = GenerationType.AUTO) */ @GeneratedValue
    private Long seq;

    @Column(length=60, nullable = false, unique = true)
    private String email;

    @Column(length=65, nullable = false)
    private String password;

    @Column(length=40, nullable = false, name="name")
    private String userName;

    // @Lob
    @Transient
    private String introduction;

    @Column(length=10)
    @Enumerated(EnumType.STRING)
    private Authority authority;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="profile_seq")
    @ToString.Exclude
    private MemberProfile profile;

    @ToString.Exclude // ToString 추가 배제
    @OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE)
    private List<BoardData> items;
}

2. 고아 객체 제거하기

고아 : 영속성 컨택스트와의 참조가 끊겨진 객체
@OneToMany 애노테이션에 orphanRemoval=true 옵션을 추가
PERSIST도 추가해야함.

@ToString.Exclude // ToString 추가 배제
@OneToMany(mappedBy = "member", cascade = {CascadeType.REMOVE, CascadeType.PERSIST, CascadeType.REFRESH}, orphanRemoval = true)
private List<BoardData> items;

@Lazy

사용 시점에 객체 생성
수동 등록한 빈객체 위에다 정의 or 클래스에 적용 가능.
DBConfig

@Configuration
@RequiredArgsConstructor
public class DBConfig {

    @PersistenceContext
    private EntityManager em;

    @Lazy
    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(em);
    }
}
profile
안녕하세요!

0개의 댓글