JPA 관계 설정
JPA 에서 조인이란?
@(어노테이션)을 이용해서 조인한다.
( 자동 FK , 컬럼 생성 )
관계를 보자
부모 - 자식 관계 : 1 : N(다) - @OneToMany(부서) , @ManyToOne(사원)
(부서) (사원) 1 : 1 - @OneToOne(핸드폰) , @OneToOne(사람)
N : N - @ManyToMany (x)
양방향 조인이란?
부서와 사원을 예를 들면 두 테이블이 서로 바라보는 (조인이된) 형태를 말한다.
( N + 1 문제 발생 )
@OneToMany (부서)
@ManyToOne (사원)
단방향 조인이란?
( 추천 형태 )
두 개의 테이블 중 하나의 테이블만 다른 테이블을 바라보는 형태를 말한다.
(부서)
@ManyToOne(사원)
따라서 1 : N 관계를 추천하되
그중 단방향 조인을 추천함
< 1 >
단방향 조인
Employee (Model)
@Entity
@Table(name = "TB_EMPLOYEE")
@DynamicUpdate
@DynamicInsert
@SequenceGenerator(
name = "SQ_EMPLOYEE_GENERATOR"
, sequenceName = "SQ_EMPLOYEE"
, initialValue = 1
, allocationSize = 1
)
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Employee extends BaseTimeEntity {
@Id
@Column(columnDefinition = "NUMBER")
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "SQ_EMPLOYEE_GENERATOR")
private Integer eno;
@Column(columnDefinition = "VARCHAR2(255)")
private String ename;
@Column(columnDefinition = "VARCHAR2(255)")
private String job;
@Column(columnDefinition = "NUMBER")
private Integer manager;
@Column(columnDefinition = "VARCHAR2(255)")
private String hiredate;
@Column(columnDefinition = "NUMBER")
private Integer salary;
@Column(columnDefinition = "NUMBER")
private Integer commission;
// @Column(columnDefinition = "NUMBER")
// private Integer dno; // 공통 컬럼 (생략)
// TODO: 단방향 조인
// @JoinColum(name="조인컬럼명")
@ManyToOne
@JoinColumn(name = "dno") // 조인할 컬럼
private Department department; // FK 클래스
}
DepartmentRepository
( 객체 쿼리 )
@Repository
public interface DepartmentRepository extends JpaRepository<Department , Integer> {
// TODO: 객체 쿼리
@Query(value = "SELECT e FROM Employee e INNER JOIN e.department d ")
Page<Employee> selectJoinPage2(Pageable pageable);
}
DepartmentService
@Service
@Slf4j
public class DepartmentService {
@Autowired
DepartmentRepository departmentRepository;
public Page<Employee> selectJoinPage(Pageable pageable) {
Page<Employee> page = departmentRepository.selectJoinPage2(pageable);
return page;
}
}
DepartmentController
@Slf4j
@RequestMapping("/exam04")
@RestController
public class DepartmentController {
@Autowired
DepartmentService departmentService;
/**
* TODO: 단방향 조인 예제
* page=현재페이지번호(0 ~ n) , size=전체페이지수
* @param pageable
* @return
*/
@GetMapping("/dept/join/paging2")
public ResponseEntity<Object> selectNativeJoinPage(Pageable pageable) {
try {
Page<Employee> page
= departmentService.selectJoinPage(pageable);
// todo: Map 자료구조 정보 저장 : 1) 부서객체, 2) 페이징 정보 (3개)
Map<String, Object> response = new HashMap<>();
response.put("dept", page.getContent()); // 부서 객체
response.put("currentPage", page.getNumber()); // 현재페이지번호
response.put("totalItems", page.getTotalElements()); // 전체테이블건수
response.put("totalPages", page.getTotalPages()); // 전체 페이지 수
if (page.isEmpty() == false) {
// 성공
return new ResponseEntity<>(response, HttpStatus.OK);
} else {
// 데이터 없음
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
} catch (Exception e) {
log.debug(e.getMessage());
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
< 2 >
양방향 조인
( 오라클 쿼리문 사용시 할 필요가 없음 )
양방향 조인시 문제점
( 1 )
N + 1 문제가 발생
만약 N + 1 문제가 발생시 시스템이 다운되는 형상이 발생
조인이 안되고 각각의 SQL문이 생성되는 현상
따라서 기본 설정 즉시 로딩(EAGER)을 지연 로딩 ( LAZY )으로 바꾸어 주어야함
여러 해결 방법들
2) inner join fetch : 조인 쿼리 생성
또는
3) application.properites : spring.jpa.properties.hibernate.default_batch_fetch_size=1000
N+1 sql -> where in (?,? ... ?) 값으로 변경하는 옵션으로 설정도 가능
( 2 )
순환 참조 문제가 발생
해결법은
@JsonManagedReference (1)
@JsonBackReference (N)
를 사용하여 붙여주고
N 관계의 테이블에 @ToString(exclude="해당속성명")을 붙여주어야 함
( exclude="해당속성명" : 해당 속성을 빼고 출력해라라는 의미 )
양방향 조인을 할려면?
mappedBy를 통하여 연결속성을 넣어준다.
@OneToMany( mappedBy="연결 속성명" )
♠ \ 특징 / ♠
( 1 )
1 : N 의 경우
N을 참조하는 테이블은 조회시 N테이블의 건수가 여러건이 나오기 때문에
HashSet을 사용해준다.
( 2 )
fetch join 사용시 Page , Pageable 사용 불가능
Employee
@Entity
@Table(name = "TB_EMPLOYEE")
@DynamicUpdate
@DynamicInsert
@SequenceGenerator(
name = "SQ_EMPLOYEE_GENERATOR"
, sequenceName = "SQ_EMPLOYEE"
, initialValue = 1
, allocationSize = 1
)
@Getter
@Setter
@ToString(exclude = "department")
@AllArgsConstructor
@NoArgsConstructor
public class Employee extends BaseTimeEntity {
@Id
@Column(columnDefinition = "NUMBER")
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "SQ_EMPLOYEE_GENERATOR")
private Integer eno;
@Column(columnDefinition = "VARCHAR2(255)")
private String ename;
@Column(columnDefinition = "VARCHAR2(255)")
private String job;
@Column(columnDefinition = "NUMBER")
private Integer manager;
@Column(columnDefinition = "VARCHAR2(255)")
private String hiredate;
@Column(columnDefinition = "NUMBER")
private Integer salary;
@Column(columnDefinition = "NUMBER")
private Integer commission;
// @Column(columnDefinition = "NUMBER")
// private Integer dno; // 공통 컬럼 (생략)
/**
* TODO:
* 양방향 조인시 : N + 1 문제 발생 - 조인이 안되고 각각의 SQL 문이 생성되는 현상
* 1) 즉시 로딩(EAGER) : 기본설정 -> 해결방법 : 지연 로딩(LAZY)
* (fetch = FetchType.LAZY)
* optional = false : 조인방법(inner , outerjoin)
* @JoinColum(name="조인컬럼명")
* 2) inner join fetch : 조인 쿼리 생성
* 3) application.properites : spring.jpa.properties.hibernate.default_batch_fetch_size=1000
* N+1 sql -> where in (?,? ... ?) 값으로 변경하는 옵션으로 설정도 가능
*
*/
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JsonBackReference
@JoinColumn(name = "dno") // 조인할 컬럼
private Department department; // FK 클래스
}
Department
@Entity // JPA 기능을 클래스에 부여하는 어노테이션
@Table(name = "TB_DEPARTMENT") // 테이블 명 지정
// JPA 어노테이션 SQL 문 자동 생성시 NULL 값 컬럼은 제외하고 생성하는 어노테이션
@DynamicInsert
@DynamicUpdate
@SequenceGenerator( // 시퀀스 설정
name = "SQ_DEPARTMENT_GENERATOR" // 시퀀스 함수이름
, sequenceName = "SQ_DEPARTMENT" // DB에 생성된 시퀀스 이름
, initialValue = 1 // 시작값
, allocationSize = 1 // JPA에서 관리용 숫자(성능지표) : 그냥 1 넣어주면 됨
)
// 밑에는 기본 lombok 어노테이션 들
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Department extends BaseTimeEntity {
@Id // 기본키 설정
@Column(columnDefinition = "NUMBER") // DB 컬럼 자료형 지정
// TODO : 사용법 : strategy (전략) = GenerationType.땡땡땡
// generator = "시퀀스함수이름"
@GeneratedValue(strategy = GenerationType.SEQUENCE
, generator = "SQ_DEPARTMENT_GENERATOR"
)
private Integer dno;
@Column(columnDefinition = "VARCHAR2(255)")
private String dname;
@Column(columnDefinition = "VARCHAR2(255)")
private String loc;
/**
* TODO : 조인시 사원테이블이 N(다) 이기 때문에 여러건이 나온다.
* 따라서 HashSet을 사용
*
* TODO: 양방향 조인 : 1) 순환참조 문제
* => 해결 : @JsonManagedReference (부서)
* : @JsonBackReference (사원)
* 2) N + 1 문제
*/
@OneToMany(mappedBy = "department") // 사원테이블의 속성과 연관관계가 있다는 것을 알려줌
@JsonManagedReference // 순환참조 문제 해결
private Set<Employee> employees = new HashSet<>(); // 1 : N (사원)
DepartmentRepository
@Repository
public interface DepartmentRepository extends JpaRepository<Department , Integer> {
// TODO : 양방향 조인 : Department 테이블에서 -> Employee 테이블을 바라봄
// 또는 Employee 테이블에서 -> Department 테이블을 바라보기도 가능
// fetch join -> Paging 이 안된다.
@Query(value = "SELECT distinct e FROM Department e INNER JOIN fetch e.employee d")
List<Department> selectFetchJoin();
}
DepartmentService
@Service
@Slf4j
public class DepartmentService {
@Autowired
DepartmentRepository departmentRepository;
public List<Department> selectFetchJoin() {
List<Department> page = departmentRepository.selectFetchJoin();
return page;
}
}
DepartmentController
@Slf4j
@RequestMapping("/exam04")
@RestController
public class DepartmentController {
@Autowired
DepartmentService departmentService;
/**
* TODO :양방향 조인 예제
*/
@GetMapping("/dept/fetch/join")
public ResponseEntity<Object> selectFetchJoin() {
try {
List<Department> list
= departmentService.selectFetchJoin();
if (list.isEmpty() == false) {
// 성공
return new ResponseEntity<>(list, HttpStatus.OK);
} else {
// 데이터 없음
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
} catch (Exception e) {
log.debug(e.getMessage());
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
'SpringBoot > JPA' 카테고리의 다른 글
SpringBoot - Hard Delete 와 Soft Delete 개념 (0) | 2023.10.19 |
---|---|
SpringBoot - JPA - 1 대 1 관계 (0) | 2023.10.19 |
SpringBoot - JPA - 페이징 처리 (1) | 2023.10.18 |
SpringBoot - JPA - 쿼리 메소드와 @Query 어노테이션 (0) | 2023.10.17 |
SpringBoot - JPA - Repository (인터페이스) (0) | 2023.10.16 |