엔티티는 대부분 다른 엔티티와 연관 관계를 맺고있다.
JPA에서는 엔티티에 연관 관계를 매핑해두고 필요할 때 해당 엔티티와 연관된 엔티티를 사용하여 좀 더 객체지향적으로 프로그래밍 할 수 있도록 도와준다.
일대일(1:1) : @OneToOne
일대다(1:N) : @OneToMany (다대일 관계가 있어야 일대다 관계가 성립한다) - ManyToOne에 종속적인 애노테이션이다.
mappedBy다대일(N:1) : @ManyToOne
다대다(N:M) : @ManyToMany ex) 하나의 해시태그에 여러개의 게시글
ex) 다대일 관계
한명의 회원이 여러개의 게시글을 쓴다 가정
게시글(자식) - 회원(부모)
(BOARD_DATA테이블 - MEMBER테이블)
공통적인 값(ex. 회원번호)으로 조인 해야한다.
- FOREIGN KEY: 다른 테이블의 기본키를 참조하는 키, 참조 무결성 제약조건
부모 레코드에 자식 레코드가 있는 경우 부모 레코드 삭제 불가- 외래키 제약조건: 특정 테이블에서 PRIMARY KEY 제약 조건을 지정한 열을 다른 테이블의 특정 열에서 참조하겠다는 의미로 지정
외래키는 [게시글쪽(BOARD_DATA) - Many]에 존재한다.
보통은 많이 있는 쪽[Many]에 외래키가 존재함
관계를 바꿀 수 있는 주체는 자식
Many쪽
One쪽
다대일 예시
🔸BOARD_DATA 엔티티 클래스
@Data
@Builder
@Entity
@NoArgsConstructor @AllArgsConstructor
public class BoardData extends BaseEntity { //자동으로 날짜와 시간 가져옴
@Id @GeneratedValue
private Long seq;
@ManyToOne
//기본적으로 외래키가 엔티티명_기본키 기준으로 만들어진다 - member_seq
private Member member; //다대일 관계에서 게시글쪽이 Many라 게시글쪽에 ManyToOne 정의
@Column(nullable = false) //필수로 Notnull 제약조건 (varchar2 기본값 크기는 255자)
private String subject;
@Lob
private String content;
}
회원 한명은 여러 게시글을 작성할 수 있으므로 @ManyToOne 애노테이션을 이용하여 다대일 관계로 매핑

테이블에 외래키 생성
🔽외래키 부여 sql쿼리문도 실행되었다.🔽

@JoinColumn: 조인되는 컬럼의 이름을 변경할때


🔹 JpaRepository
*One(Member)엔티티가 계속 영속성 상태에 유지 되야하기때문에 Transaction처리를 해줘야한다.


✅ 테스트1
...
@Test
void test1(){
BoardData item = boardDataRepository.findById(1L).orElse(null); //조회
System.out.println(item);
}

조인 쿼리 생성되었다!
👩🏫inner조인이 아닌 left조인을 쓴 이유?
-> inner조인(교집합)일경우 member쪽에 null이면 데이터가 나오지 않기때문에 left조인(왼쪽 데이터 전부, 오른쪽 데이터는 있으면 나오고 없으면 null)을 기본으로 만든다.
@Test
void test1(){
BoardData item = boardDataRepository.findById(1L).orElse(null); //조회
System.out.println(item);
Member member = item.getMember(); //게시글을 작성한 회원
System.out.println(member);
}

일대다(@OneToMany): Member - BoardData
다대일(@ManyToOne): BoardData - Member
일대다: OnetoMany는 ManyToOne을 바탕으로 정의되기 때문에 다대일 관계가 정의되어있어야 한다.
🔸Member 엔티티 클래스

관계의 주인 명시해줘야함 (Many 쪽 - BoardData의 Member(외래키))
BoardData 엔티티 ⬇

외래키가 부여되는 쪽 -> member을 MappedBy에 정의해줘야함
🔽👩🏫🔽

@Test
void test2(){
Member member = memberRepository.findById(1L).orElse(null);
List<BoardData> items = member.getItems();
items.forEach(System.out::println);
}
회원번호로 게시글 조회 쿼리는 수행됨

하지만 값은 나오지 않고 있다... -> 지연로딩
StackOverflowError

🔹 순환 참조 문제

Member

boardData

롬복은 toString을 getter메서드를 통해서 출력한다.
member에서 items를 출력할때(getItems) -> BoardData 엔티티 member을 출력하는데 toString메서드를 출력한다 -> getMember()가 호출 -> 또 toString 출력된다 -> List<BoardData> items 출력 (getItems)-> 또 toString() 호출 -> getMember()호출... 무한반복
회원쪽 데이터에서 게시글 데이터는 항상 필요한 필수데이터는 아님 - 필요할때는 마이페이지 정도?
필요도로 봣을때 member쪽에 items를 배제하는게 좋음

🔽게시글 데이터 조회 가능🔽

일대일(@OneToOne)
회원1 - 프로필1
회원쪽에서 프로필을 확인하는 경우가 많음
회원이 프로필 정보를 참조하도록!
외래키는 회원쪽에 있는것이 적합하다.

🔸Member 엔티티 클래스


memberProfile 테이블 생성됨

1:1
모든 회원이 한개만 가지고 있어야하기 때문에 unique제약조건이 걸려있다.
🔹 JpaRepository

✅ 테스트

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

@Test
void test1(){
Member member = memberRepository.findById(1L).orElse(null);
MemberProfile profile = member.getProfile();
// System.out.println(member);
System.out.println(profile);
//프로필 조회
}
OneToOne에도 한쪽에 관계의 주인이 존재하는데 주인은 외래키가 존재하는쪽 Member쪽임, 프로필 쪽에서도 회원을 조회 하도록 하려면 mappedBy로 관계의 주인을 명시해줘야한다.

외래키가 생성되지는 않음

이것도 양방향 조회시 순환참조가 발생한다.
@ToString.Exclude -> ToString 포함 배제로 stackoverflow 해결해주기
관계의 주인은 회원쪽이기 때문에 프로필쪽에서 배제해주자!


다대다 관계 (@ManytoMany)
- 중간 테이블 하나 생성
연결시 중간 테이블이 만들어진다.- 외래키가 만들어지지 않는다.
BoardData - HashTag
게시글 1 - 태그1 태그4
게시글 2 - 태그1 태그2
게시글 3 - 태그3 태그4
게시글에도 여러 태그가, 태그 한개에도 여러 게시글이 존재
테이블 이름은 관계의 주인이 테이블명 앞에 온다.
게시글이 더 중요! 게시글을 관계의 주인으로 ~
🔸HashTag엔티티 클래스
mappedBy로 관계의 주인을 명시
🔸BoardData엔티티 클래스

중간 테이블 생성됨

각 테이블의 기본키로 테이블이 생성된다.
엔티티명_기본키명


🔹 JpaRepository

✅ 테스트
DB에 값 보기 위해 transactional 제거해줌

중간테이블



✅ 테스트2
Transactional과 em.clear 추가
@Test
void test1(){
BoardData item = boardDataRepository.findById(1L).orElse(null);
List<HashTag> tags = item.getTags();
tags.forEach(System.out::println);
}




...
...
ManyToOne

테이블 한번 조회할때 연관되어있는 모든 테이블을 조회하는건 불필요하다. 모든 엔티티의 정보가 필요하지X, 주요 엔티티 정보만 있으면 됨
조인이 많아질수록 성능이 떨어진다.
지연 로딩 전략을 권장함 -> 필요할때 쿼리 쓰도록
즉시로딩 - 각 엔티티를 처음부터 join
지연로딩 - 처음에는 현재 엔티티만 조회, 다른 매핑된 엔티티는 사용할때만 2차 쿼리를 실행한다.
필요할때만 조회 하도록!
글로벌 전략 지연로딩 -> 필요할때만 즉시 로딩 전략으로 사용!!
ManyToOne은 기본 전략으로 즉시로딩 전략이 기본값으로 되어있다.


성능을 위해서 지연로딩으로 바꿔주자
oneToMany의 기본 전략은 지연로딩!!

@Transactional 애노테이션과 함께 많이 사용한다.
지연로딩에선 영속성이 유지되어야지 문제가 생기지 않기때문임
지연로딩 전략을 사용하면
게시글만 조회하고 회원쪽은 필요할때 2차쿼리 형태로 실행된다


Member엔티티 클래스 OneToOne(기본값: 즉시로딩) 지연로딩 전략 적용


지연로딩 적용 후
게시글 조회시 쿼리 수행 1번
데이터 조회할때 쿼리 수행 10 번
✅ 테스트 12



쿼리 한번 수행하는데 그 갯수만큼 쿼리가 많이 실행되고 있다..
💥N+1문제💥
성능 떨어짐ㅜㅜ
👨💻⭐ Fetch 조인을 통해서 필요한 엔티티만 즉시 로딩 전략을 사용하자
1) JPQL 직접 정의: @Query 애노테이션

🔹 BoardDataRepository


⭐2) @EntityGraph 애노테이션: 쿼리 메서드 사용시 정의 가능, 바로 조회 할 엔티티 명시

✅ 테스트
@Test
void test3(){
List<BoardData> items = boardDataRepository.findBySubjectContaining("제목");
}

⭐3) QueryDsl의 fetchJoin() 메서드 사용
JPAQueryFactory -> JPAQuery 객체를 만든다, 쿼리 빌딩!
반환값: JPAQuery
생성자 매개변수: EntityManager
✅ 테스트4
@Test
void test4(){
QBoardData boardData = QBoardData.boardData;
//싱글톤 형태로 정적인 객체 만들어져있음
JPAQueryFactory factory = new JPAQueryFactory(em);
// 전용 클래스로 쿼리빌딩 할 수 있음 - Querydsl이 자동으로 만든 Q클래스로 쿼리 빌딩
JPAQuery<BoardData> query = factory
.selectFrom(boardData)
.leftJoin(boardData.member) // boardData.member를 처음부터 조인
.fetchJoin();
List<BoardData> items = query.fetch(); //목록조회: fetch()
items.forEach(System.out::println);
}

📌항상 엔티티 매니저가 필요하면 빈으로 등록해서 조립된 객체 가져오도록 하자!
객체가 조립될 필요가 있는 부분 미리 조립해버리자

👩🏫참고)
@Lazy
- 사용시점에 객체로 생성해주고 싱글톤으로 관리
- 클래스명 위, 수동 등록 빈 위..

@Test
void test4(){
QBoardData boardData = QBoardData.boardData;
//싱글톤 형태로 정적인 객체 만들어져있음 - 단점) 처음 로딩될때 너무 많은 객체를 로딩하면 느려진다.
// JPAQueryFactory factory = new JPAQueryFactory(em);
// 전용 클래스로 쿼리빌딩 할 수 있음 - Querydsl이 자동으로 만든 Q클래스로 쿼리 빌딩
JPAQuery<BoardData> query = queryFactory
.selectFrom(boardData)
.leftJoin(boardData.member) // boardData.member를 처음부터 조인
.fetchJoin();
List<BoardData> items = query.fetch(); //목록조회: fetch()
// items.forEach(System.out::println);
}
4) @BatchSize
@BatchSize(size=10) : 최대 추출 개수를 한정함
10개까지가 max사이즈, 10개를 넘어서는 조회가 필요하다면 IN쿼리가 또 실행됨
적용 전
SELECT ... FROM BoardData
SELECT ... FROM MEMBER WHERE seq = 1L
SELECT ... FROM MEMBER WHERE seq = 2L
SELECT ... FROM MEMBER WHERE seq = 3L
적용 후
1차쿼리는 수행
SELECT ... FROM BoardData
개수만큼 in조건으로 쿼리
SELECT ... FROM MEMBER seq IN(1L,2L,3L)

selectfrom 으로 전체 조회시 영속성 상태이지만
개별로 조회시 영속성 상태가 아니다
게시글 제목과 내용 조회
@Test
void test5(){
QBoardData boardData = QBoardData.boardData;
//낱개 조회 할때는 해당 데이터의 자료형으로 정의하면 된다.
//두개 이상일 경우 자료형 -> tuple
// JPAQuery<String> query = queryFactory.select(boardData.subject);
JPAQuery<Tuple> query = queryFactory.select(boardData.subject,boardData.content).from(boardData); //낱개 데이터 조회 영속상태와 관계 없음
List<Tuple> items = query.fetch();
for(Tuple item:items){
String subject = item.get(boardData.subject);
String content = item.get(boardData.content);
System.out.printf("subject=%s, content=%s\n",subject,content);
}
}

통계 데이터
@Test
void test6(){
QBoardData boardData = QBoardData.boardData;
JPAQuery<Long> query = queryFactory.select(boardData.seq.sum())
.from(boardData);
long sum = query.fetchOne();
//fetchOne(): 하나만 가져옴, fetchFirst(): 여러개중 한개만 가져옴
System.out.println(sum);
}

seq 1~10
✅ 테스트
@Test
void test7(){
QBoardData boardData = QBoardData.boardData;
JPAQuery<BoardData> query = queryFactory.selectFrom(boardData)
.leftJoin(boardData.member)
.fetchJoin()
.where(boardData.seq.in(2L,3L,4L));//where절에는 조건식을 넣을 수 있음
//게시글 번호에서 2, 3, 4 번만 조회시 in 조건 사용
//booleanExpression(Predicate 구현클래스)형태로 반환값이 나온다. - 상위 인터페이스: Predicate
List<BoardData> items = query.fetch();
items.forEach(System.out::println);
}




👩🏫참고
Q 클래스
조건식 메서드를 가지고 사용
eq: =(같다)
lt: < (작다)
loe: <=(작거나 같다)
gt: > (크다)
goe: >= (크거나 같다)
✅ 테스트


seq 2,3,4인 동일 출력 결과 나온다!
✅ 테스트


시작 위치, 조회할 개수 추가되어있음



1) PERSIST
2) MERGE
3) REMOVE: 부모 엔티티가 삭제될때 자식 엔티티도 삭제되지만 -> 동작은 자식 엔티티가 삭제되고 부모 엔티티가 삭제된다.(부모에 연관된 자식이 있으면 먼저 삭제가 안되기 때문)
4) REFRESH
5) DETACH
6) ALL
🔸 부모 엔티티 => Member

제약조건 CASCADE ON DELETE는 아님
✅ 테스트
@SpringBootTest
@Transactional
@ActiveProfiles("test")
public class Ex13 {
@Autowired
private MemberRepository memberRepository; //회원
@Autowired
private BoardDataRepository boardDataRepository;
@Autowired
private JPAQueryFactory queryFactory; //빈으로 등록한 완성된 객체
@PersistenceContext
private EntityManager em;
@BeforeEach
void init(){
em.setFlushMode(FlushModeType.AUTO);
Member member = Member.builder()
.email("user01@test.org")
.password("12345612")
.userName("사용자01")
.authority(Authority.USER)
.build();
memberRepository.saveAndFlush(member);
//게시글 작성 - 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(){
Member member = memberRepository.findById(1L).orElse(null);
//회원을 지우면 해당되는 게시글도 제거되도록
memberRepository.delete(member); //사용자01
memberRepository.flush();
}
}

자식 데이터 삭제 다 이뤄지고 나서 부모 데이터 삭제가 이루어짐
@OneToMany 애노테이션에 orphanRemoval=true 옵션을 추가

@Test
void test2(){
Member member = memberRepository.findById(1L).orElse(null);
List<BoardData> items = member.getItems();
//0번과 1번객체를 고아상태로 만들기
items.remove(0);
items.remove(1); //db에는 존재하지만 객체로는 존재하지 않는 상태
memberRepository.flush(); //제거됨
}
