86일차 - JPA (JPQL, 페이징, 양방향 매핑 (연관관계 설정))

Yohan·2024년 6월 26일
0

코딩기록

목록 보기
128/156
post-custom-banner

JPQL

  • SQL : 테이블을 대상으로 쿼리
  • JPQL : 엔티티 객체를 대상으로 쿼리
  • @Query : 쿼리 커스텀 버전
  • 테이블 접근x, 자바 클래스에 접근
    /*
        - JPQL
        -> FROM 뒤에 클래스명이 온다는 점

        SELECT 엔터티별칭
        FROM 엔터티클래스명 AS 엔터티별칭
        WHERE 별칭.필드명

        ex) native - SELECT * FROM tbl_student WHERE stu_name = ?
            JPQL   - SELECT st FROM Student AS st WHERE st.name = ?
     */
  • StudentRepository
    // 도시명으로 학생 1명을 단일 조회
    // Optional은 null 방지 용도
    @Query(value = "SELECT st FROM Student st WHERE st.city = ?1")
    Optional<Student> getByCityWithJPQL(String city);

    // 특정 이름이 포함된 학생 리스트 조회하기
    @Query("SELECT stu FROM Student stu WHERE stu.name LIKE %?1%")
    List<Student> searchByNameWithJPQL(String name);


    // JPQL로 갱신 처리하기

    @Modifying
    // SELECT 외 나머지 쿼리는 무조건 @Modifying을 붙혀야 한다.
    @Query("DELETE FROM Student s WHERE s.name = ?1 AND s.city = ?2")
    void deleteByNameAndCity(String name, String city);
  • StudentRepository Test
    @Test
    @DisplayName("JPQL로 학생 조회하기")
    void jpqlTest() {
        //given
        String city = "제주도";
        //when
        Student student = studentRepository.getByCityWithJPQL(city)
                .orElseThrow(() -> new RuntimeException("학생이 없음!"));// 도시가 조회가 안되면 예외를 발생시켜라
        //then
        assertNotNull(student);
        System.out.println("\n\n\nstudent = " + student + "\n\n\n");
//      assertThrows(RuntimeException.class, () -> new RuntimeException());

    }

    @Test
    @DisplayName("JPQL로 이름이 포함된 학생목록 조회하기")
    void jpqlTest2() {
        //given
        String containingName = "춘";
        //when
        List<Student> students = studentRepository.searchByNameWithJPQL(containingName);
        //then
        System.out.println("\n\n\n\n");
        students.forEach(System.out::println);
        System.out.println("\n\n\n\n");
    }

    @Test
    @DisplayName("JPQL로 삭제하기")
    void deleteJpqlTest() {
        //given
        String name = "어피치";
        String city = "제주도";
        //when
        studentRepository.deleteByNameAndCityWithJPQL(name, city);
        //then
        assertEquals(0, studentRepository.findByName(name).size());
    }

test에서 @Transactional, @Rollback

  • 데이터를 눈으로 보고싶으면 @Transactional는 무조건 붙어있고
  • @Rollback만 @Rollback(false)로 바꿔놓으면 데이터확인 가능, Rollback 주석처리 절대 X

Page, Pageable 인터페이스

  • Page객체는 ORDER BY, LIMIT를 자동으로 해준다.
  • 정렬할 때 PageRequest에서 실질적으로 사용가능
  • StudentPageRepository
package com.spring.jpastudy.chap03_page;

import com.spring.jpastudy.chap02.entity.Student;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;



public interface StudentPageRepository extends JpaRepository<Student, String> {

    // 전체조회 상황에서 페이징 처리하기

    // Import해온 Page 클래스에는 필요한 것들이 이미 다 구현되어 있다.
    Page<Student> findAll(Pageable pageable);

    // 검색 + 페이징
    Page<Student> findByNameContaining(String name, Pageable pageable);
}
  • StudentPageRepositoryTest
    @Test
    @DisplayName("기본적인 페이지 조회 테스트")
    void basicPageTest() {
        //given
        int pageNo = 6;
        int amount = 10;

        // 페이지 정보 객체를 생성 (Pageable)
        // 여기서는 페이지 번호가 zero-based임,
        // 즉 1페이지는 0으로 취급한다는 뜻

        // Pageable은 인터페이스라 new Pageable이 안된다. 
        // 그래서 이미 Pageable을 구현해놓은 PageRequest를 불러온다.
        Pageable pageInfo = PageRequest.of(pageNo - 1, amount);

        //when
        Page<Student> students = repository.findAll(pageInfo);

        // 실질적인 데이터 꺼내기
        List<Student> studentList = students.getContent();

        // 총 페이지 수
        int totalPages = students.getTotalPages();

        // 총 학생 수
        long count = students.getTotalElements();

        //then
        studentList.forEach(System.out::println);
        System.out.println("totalPages = " + totalPages);
        System.out.println("count = " + count);
    }
    
    
    @Test
    @DisplayName("페이징 + 정렬")
    void pagingAndSortTest() {
        //given
        Pageable pageInfo = PageRequest.of(
                0,
                10,
                // 매개값으로는 엔터티 필드명
//                Sort.by("name").descending()

                // 여러 조건으로 정렬
                Sort.by(
                        Sort.Order.desc("name"),
                        Sort.Order.asc("city")
                        // 더 나열 가능
                )
        );
        //when
        Page<Student> studentPage = repository.findAll(pageInfo);
        //then
      
        studentPage.getContent().forEach(System.out::println);

    }

JPA 연관관계 설정

Lazy Loading, EAGAR Loading

  • Lazy Loading 쓰다가 필요하면 EAGAR Loading 사용

Lazy Loading

  • LAZY 로딩은 연관된 엔티티를 실제로 사용할 때까지 로드를 지연시키는 방식.
  • 이 방식은 연관된 엔티티가 필요하지 않은 경우 불필요한 데이터베이스 접근을 방지함으로써 성능을 최적화하는데 도움
@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Post> posts = new ArrayList<>();
}
  • 위 예시에서 User 엔티티를 로드할 때 Post 엔티티는 바로 로드되지 않는다. User의 posts 프로퍼티에 처음 접근하는 순간 Post 엔티티가 로드됨

Eager Loading

  • EAGER 로딩은 연관된 엔티티를 부모 엔티티가 로드되는 시점에 함께 로드하는 방식입니다. 이는 연관된 엔티티를 바로 사용해야 하는 경우에 유용하지만, 항상 모든 연관된 엔티티를 로드하므로 성능 이슈를 초래할 수 있음
@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
    private List<Post> posts = new ArrayList<>();
}
  • 위 예시에서 User 엔티티를 로드하는 즉시 Post 엔티티도 함께 로드

  • JPA의 로딩 전략을 이해하는 것은 중요하며, 성능 최적화와 어플리케이션의 응답 시간에 큰 영향을 줄 수 있습니다. LAZY 로딩은 필요한 경우에만 데이터를 로드하므로 보통은 이를 기본 전략으로 사용하며, EAGER 로딩은 신중하게 사용해야 합니다.

단방향 매핑

  • Department
package com.spring.jpastudy.chap04_relation.entity;


import lombok.*;

import javax.persistence.*;

@Setter
@Getter
@ToString
@EqualsAndHashCode(of = "id")
@NoArgsConstructor
@AllArgsConstructor
@Builder

@Entity
@Table(name = "tbl_dept")
public class Department {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "dept_id")
    private long id; // 부서 번호

    @Column(name = "dept_name", nullable = false)
    private String name;
}
  • Employee
  • JOIN을 원치 않는다면 (필요없는 데이터 SELECT) toString을 주의
package com.spring.jpastudy.chap04_relation.entity;

import lombok.*;

import javax.persistence.*;

@Setter @Getter
@ToString(exclude = "department")
// toString에서 필요없는 컬럼은 뺄 수 있다.
// 연관관계 필드 제외
@EqualsAndHashCode(of = "id")
@NoArgsConstructor
@AllArgsConstructor
@Builder

@Entity
@Table(name = "tbl_emp")
public class Employee { // 다 (many)

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "emp_id")
    private long id; // 사원 번호

    @Column(name = "emp_name", nullable = false)
    private String name;

    // 단방향 매핑 - 데이터베이스처럼 한 쪽에 상대방의 PK를 FK로 갖는 형태다.
    // EAGER Loading: 연관된 데이터를 항상 JOIN을 통해 같이 가져옴 (성능 이슈 초래)
    // LAZY Loading: 해당 엔터티 데이터만 가져오고 필요한 경우 연관엔터티를 가져옴 (성능 굿)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "dept_id") // FK 컬럼명
    private Department department; // 1 (one)

}

양방향 매핑

삽입, 수정, 삭제가 일어나면 무조건 양방향에서 업데이트!!!!!!!!!

  • Employee(다) 쪽에서는 Department(일)을 참조하여 사용할 수 있으나
  • Department(일)쪽에서는 Employee(다)를 참조할 수 없다.
    -> 양방향 매핑을 통해 참조가능하게함
    -> Department(일) 쪽에서는 여러명의 Employee가 들어올 수 있으므로 List 형태로 매핑
package com.spring.jpastudy.chap04_relation.entity;


import lombok.*;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Setter
@Getter
@ToString(exclude = "employees")
@EqualsAndHashCode(of = "id")
@NoArgsConstructor
@AllArgsConstructor
@Builder

@Entity
@Table(name = "tbl_dept")
public class Department { // 1. One

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "dept_id")
    private Long id; // 부서 번호

    @Column(name = "dept_name", nullable = false)
    private String name;


    /*
        - 양방향 매핑은 데이터베이스와 달리 객체지향 시스템에서 가능한 방법으로
        1대N관계에서 1쪽에 N데이터를 포함시킬 수 있는 방법이다.

        - 양방향 매핑에서 1쪽은 상대방 엔터티 갱신에 관여 할 수 없고
           (리스트에서 사원을 지운다고 실제 디비에서 사원이 삭제되지는 않는다는 말)
           단순히 읽기전용 (조회전용)으로만 사용하는 것이다.
        - mappedBy에는 상대방 엔터티에 @ManyToOne에 대응되는 필드명을 꼭 적어야 함
     */

    @OneToMany(mappedBy = "department", fetch = FetchType.LAZY) // 상대방클래스에서 정의한 내 클래스 명
    private List<Employee> employees = new ArrayList<>(); // 다. Many, NullPointerException 방지로 초기화.

}

양방향에서 수정 문제

한 클래스에서만 업데이트를 하면 제대로 이뤄지지 않는다.

두 엔터티클래스 모두 수정을 해줘야 제대로 이뤄진다.

  • DepartmentTest
 @Test
    @DisplayName("양방향 연관관계에서 연관데이터 수정")
    void changeTest() {
        //given
        
        // 3번 사원의 부서를 2번부서에서 1번부서로 수정
        
        // 3번 사원 정보 조회
        Employee employee = employeeRepository.findById(3L).orElseThrow();
        
        // 1번 부서 정보 조회
        Department department = departmentRepository.findById(2L).orElseThrow();

        //when


        /*
            사원정보가 Employee엔터티에서 수정되어도
            반대편 엔터티인 Department에서는 리스트에 바로 반영되지 않는다.

            해결방안은 데이터 수정시에 반대편 엔터티에도 같이 수정을 해줘야 한다.
         */

        // 사원정보 수정
        employee.setDepartment(department);

        // 핵심코드 ↓ 
        // 양방향에서는 수정시 반대편도 같이 수정
        department.getEmployees().add(employee);
        
        employeeRepository.save(employee);

        //then
        // 바뀐부서의 사원목록 조회
        List<Employee> employees = department.getEmployees();
        System.out.println("\n\n\n\n");
        employees.forEach(System.out::println);
        System.out.println("\n\n\n\n");
    }
    

양방향 업데이트 예제

package com.spring.jpastudy.chap04_relation.entity;

@Setter
@Getter
@ToString(exclude = "employees")
@EqualsAndHashCode(of = "id")
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "tbl_dept")
public class Department {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "dept_id")
    private Long id; // 부서번호


			...
            

    @OneToMany(mappedBy = "department", orphanRemoval = true, cascade = CascadeType.ALL)
    private List<Employee> employees = new ArrayList<>();


// 양방향에서 업데이트 해주는 메서드
	// 삭제
    public void removeEmployee(Employee employee) {
        this.employees.remove(employee);
        employee.setDepartment(null);
    }
	// 추가
    public void addEmployee(Employee employee) {
        this.employees.add(employee);
        employee.setDepartment(this);
    }

}
  • test
  • 이래야(양방향 수정, 삭제) 제대로 이루어진다
 @Test
    @DisplayName("고아 객체 삭제하기")
    void orphanRemovalTest() {
        //given

        // 1번 부서 조회
        Department department = departmentRepository.findById(1L).orElseThrow();

        // 1번 부서 사원 목록 가져오기
        List<Employee> employeeList = department.getEmployees();

        // 2번 사원 조회
        Employee employee = employeeList.get(1);
        //when

        // 부서 목록에서 사원 삭제 (부모가 자식 버림)
        // ↓ 반대편에서도 고아로 만들어준다. (자식도 부모 버림) (양방향 주의)
        department.removeEmployee(employee);

        // 갱신 반영
        departmentRepository.save(department);
        //then
    }



    @Test
    @DisplayName("양방향 관계에서 리스트에 데이터를 추가하면 DB에도 INSERT 된다")
    void cascadePersistTest() {
        //given

        // 2번 부서 조회
        Department department = departmentRepository.findById(2L).orElseThrow();

        Employee employee = Employee.builder()
                .name("뽀로로")
                .build();

        //when
        department.addEmployee(employee);

        //then
    }

양방향 매핑 정리

1. @ToString(exclude = "department") 처럼연관관계 필드는 무조건 제외
2. 다대일에서 일 입장에서 다를 매핑할 때는 List로 매핑
3. 양방향에서 갱신 (삽입, 수정, 삭제)시 에는 무조건 양방향에서 이루어져야함
4. JPA 연관관계 loading은 웬만하면 Lazy
5. mappedBy에는 상대방클래스에서 정의한 내 클래스 명을 작성

profile
백엔드 개발자
post-custom-banner

0개의 댓글