🔶JPA
≣ 목차
/ 오늘의 TIL /
JPA
연관 관계의 주인
비즈니스 로직을 기준으로 연관 관계의 주인을 선택하면 안된다.
외래 키(FK) 위치를 기준으로 주인을 정하자!
mappedBy 속성이 작성되어있지 않은 객체가 연관 관계의 주인이다. (수정, 삭제 등 가능)
주인이 아닌 경우는 읽기만 가능하다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setTeam(team); //값 세팅 중요!!!!
em.persist(member);
em.flush();
em.clear();
//결과: Member의 team_id(FK)가 설정된다.
//* 순서 중요!!!
//주인에 값을 세팅하지 않은 상태인 경우
//역방향에 연관 관계를 설정하면 주인의 FK에는 Null이 들어온다.
team.getMembers().add(member);
주의사항
1. 1차 캐시
그런데 한 가지 문제점이 있다.
em.flush(), em.clear()를 하지 않는다면?
JPA의 영속성 컨텍스트가 persist한 객체를 1차 캐시로 가지고 있다.
1차 캐시로 가지고 있는 객체에서 매핑되어있는 객체의 값을 가져오려고 하면? 아무것도 없다.......
따라서, 객체지향적으로 하려면 둘다 매핑(값 세팅)을 진행해주어야 한다.
-> 순수한 객체 관계를 고려하면 항상 양쪽 다 값을 입력해야 한다.
Team team = new Team();
team.setName("TeamA");
em.persist(team); //1차 캐시로 저장되어 있다.
Member member = new Member();
member.setUsername("member1");
member.setTeam(team); //연관 관계의 주인에서 FK 값을 세팅한다.
em.persist(member); //1차 캐시로 저장되어 있다.
//중요!!!!!!!!!!!!
team.getMembers().add(member); //역방향에서도 객체를 세팅한다.
Team findTeam = em.find(Team.class, team.getId()); //1차 캐시로 저장되어 있는 team에서 id를 가져온다.
//만약 하지 않는다면 null이 세팅됨.
List<Member> members = findTeam.getMembers();
따라서 한 번에 양방향 매핑을 하고 싶은 경우 연관관계 편의 메소드를 생성할 수 있다.
어느 객체에서 만들어서 세팅할지는 선택해서 하면 된다.
대신, 한 쪽에서만 매핑할 것을 권장한다.
+) 단방향 매핑을 잘 하고, 양방향은 필요할 때 추가해도 된다. (테이블에 영향을 주지 않음)
+) JPQL에서 역방향으로 탐색할 일이 많다.
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
2. 양방향 매핑시에 무한 루프 조심하기
toString(), lombok, JSON 생성 라이브러리 등
연관관계 매핑
- 일대다 [1:N]
- @JoinColumn 사용해야 join table이 추가적으로 생성되지 않는다.
- 다대일 [N:1]
- 일대일 [1:1]
- 다대다 [N:M]
- 권장 X
상속관계 매핑
Item <- Movie, Album, Book
Item class는 abstract(추상) 방식으로 생성한다. (다형성 개념)
Item을 바로 생성하게 되면 자식 없이 부모만 존재하기 때문이다.
- @Inheritance(strategy = InheritanceType.XXX)
- JOINED: 조인 전략 (정석! 👍🏻)
- SINGLE_TALBE: 단일 테이블 전략 (@DiscriminatorColumn을 작성하지 않아도 자동으로 생성됨)
- TABLE_PER_CLASS: 구현 클래스마다 테이블 전략 (select 시 성능 이슈)
- @DiscriminatorColumn
- DTYPE(name 속성으로 변경 가능)으로 어떤 엔티티랑 매핑돼서 create되었는지 작성되어 있다.
- @DiscriminatorValue("") 자식 클래스에서 사용하면 해당 이름으로 DTYPE이 매핑된다.
장점 | 단점 | |
조인 전략 | - 테이블 정규화 - 외래 키 참조 무결성 제약조건 활용 가능 - 저장공간 효율화 |
- 조회시 조인을 많이 사용, 성능 저하 - 조회 쿼리 복잡 - 데이터 저장시 insert sql 2번 호출 |
단일 테이블 전략 | - 조인이 필요 없으므로 일반적으로 속도 빠름 - 단순한 조회 쿼리 |
- 자식 엔티티가 매핑한 컬럼은 모두 null 허용 - 테이블이 커질 수 있고 상황에 따라 조회 성능이 오히려 느려질 수 있음 |
구현 클래스마다 테이블 전략 | - 서브 타입을 명확하게 구분해서 처리할 때 효과적 - not null 제약 조건 사용 가능 |
- 여러 자식 테이블과 함께 조회할 때 성능이 느림(UNION SQL) - 자식 테이블을 통합해서 쿼리하기 어려움 |
@MappedSuperClass 를 통해 공통 필드를 따로 관리할 수 있다.
추상 클래스로 만든다.
엔티티가 아니기 때문에 테이블과 매핑되지 않는다.
자식 클래스에서 extends로 해당 어노테이션을 붙인 클래스를 상속 받아서 사용한다.
예시: 생성 날짜, 수정 날짜...
코딩테스트
https://school.programmers.co.kr/learn/courses/30/lessons/81303
풀이
1. 실제 배열을 선언하고 삽입과 삭제 연산을 하는 대신, 인덱스만으로 연산하기
각 행의 상대적 위치를 표현한다. 맨 끝 행일 경우 up은 -1, down은 N값을 이용해 표시한다.
2. 삭제 연산
N = 4일 경우 예시
행 번호 | up | down |
0 | -1 | 1 |
1 | 0 | 2 -> 3 |
2 | 1 | 3 |
3 | 2 -> 1 | 4 |
만약 k = 2 인데 삭제 연산을 한다면,
1 down은 3이어야 하고 up[down[k]] = up[k]
3 up은 1이어야 한다. down[up[k]] = down[k]
3. 복구 연산
복구 동작의 핵심은 기존 삭제 위치에 행을 삽입해야 한다.
윗 행의 down, 아래 행의 up을 stack에서 pop한 행의 번호로 세팅해야 한다.
4. 테이블 양 끝에서 연산하는 경우
기존 식을 적용하기 위해서 가상 공간을 만든다.
기존 배열 +2 공간
up | down |
-1 | 1 |
... | ... |
4 | 6 |
import java.util.*;
class Solution {
/**
* @param n 처음 표의 행 개수
* @param k 처음에 선택된 행의 위치
* @param cmd 수행한 명령어들이 담긴 문자열 배열
* @return 삭제 유무 행 O,X로 표시한 문자열 형태
*/
public static String solution(int n, int k, String[] cmd) {
//삭제된 행을 저장하는 스택
Stack<Integer> deleted = new Stack<>();
//각 행을 기준으로 연산에 따른 위치 표시 up, down
int[] up = new int[n + 2];
int[] down = new int[n + 2];
//상대적 위치에 따른 행 번호 설정
for (int i = 0; i < n + 2; i++) {
up[i] = i - 1;
down[i] = i + 1;
}
//가상 공간에 맞추기 위해 현재 위치 +1
k++;
//cmd 순회
for (String c : cmd) {
//현재 위치를 삭제하고 그 다음 위치로 이동
if (c.startsWith("C")) {
//stack에 저장
deleted.push(k);
//up과 down이 바라보는 행 번호 변경
up[down[k]] = up[k];
down[up[k]] = down[k];
//n을 넘는 경우는 바닥이므로 위의 행을 선택해야 한다. 그 외는 행을 하나씩 증가 시킨다.
k = n < down[k] ? up[k] : down[k];
}
//가장 최근에 삭제된 행 번호 복원
else if (c.startsWith("Z")) {
int restore = deleted.pop();
down[up[restore]] = restore;
up[down[restore]] = restore;
}
//행 번호 이동
else {
String[] s = c.split(" ");
int x = Integer.parseInt(s[1]);
//x 횟수만큼 진행
for (int i = 0; i < x; i++) {
//U인 경우 up, D인 경우 down
k = s[0].equals("U") ? up[k] : down[k];
}
}
}
//삭제된 행의 위치에 'X', 그렇지 않은 행 위치에는 'O'를 저장한 문자열 반환
char[] answer = new char[n];
Arrays.fill(answer, 'O');
//stack에 남아있는 행은 삭제된 행이다.
for (int i : deleted) {
answer[i-1] = 'X';
}
return new String(answer);
}
}
시간 초과
import java.util.*;
public class Solution {
/**
* @param n 처음 표의 행 개수
* @param k 처음에 선택된 행의 위치
* @param cmd 수행한 명령어들이 담긴 문자열 배열
* @return 삭제 유무 행 O,X로 표시한 문자열 형태
*/
public static String solution(int n, int k, String[] cmd) {
//행의 번호와 삭제 유무 문자가 담겨있는 List를 하나 생성한다.
List<Integer> list = new ArrayList<>();
//초기화
for (int i = 0; i < n; i++) {
list.add(i);
}
//C로 빼낸 임시 문자 저장을 위한 stack
Stack<Integer> stack = new Stack<>();
//cmd 순환
for (String s : cmd) {
char[] command = s.replaceAll("\\s+", "").toCharArray();
char c = command[0];
switch (c) {
case 'D':
//주어진 숫자만큼 idx 증가
k += ((int) command[1] - '0');
break;
case 'C':
//인덱스를 stack에 넣는다.
stack.push(list.get(k));
//행 삭제
list.remove(k);
//인덱스가 마지막인 경우는 위의 행 선택
if (k == list.size()) {
k--;
}
break;
case 'U':
//주어진 숫자만큼 idx 감소
k -= ((int) command[1] - '0');
break;
case 'Z':
//stack에 있는 마지막 값을 존재하는 행으로 되돌린다.
int pop = stack.pop();
if (pop > list.size()) {
list.add(pop);
} else {
list.add(pop, pop);
k++;
}
break;
}
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
if (stack.contains(i)) {
sb.append("X");
} else {
sb.append("O");
}
}
return sb.toString();
}
public static void main(String[] args) {
// int[][] a = {{0,0,0,0,0},{0,0,1,0,3}, {0,2,5,0,1}, {4,2,4,4,2}, {3,5,1,3,1}};
String[] b = {"D 2", "C", "U 3", "C", "D 4", "C", "U 2", "Z", "Z", "U 1", "C"};
System.out.println(solution(8, 2, b));
}
}
'Blog > TIL' 카테고리의 다른 글
[240605] 문제를 잘 파악하자 (0) | 2024.06.05 |
---|---|
[240604] 값 타입, 페치 조인 배우기 (0) | 2024.06.04 |
[240602] JPA 영속성 컨텍스트 (0) | 2024.06.02 |
[240601] 코딩 테스트 문제 풀기 (0) | 2024.06.01 |
[240531] 처음으로 배포를 해보다 (0) | 2024.05.31 |
댓글