/*
- 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 = ?
*/
// 도시명으로 학생 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);
@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());
}
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);
}
@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);
}
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Post> posts = new ArrayList<>();
}
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private List<Post> posts = new ArrayList<>();
}
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;
}
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)
}
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 방지로 초기화.
}
@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
@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에는 상대방클래스에서 정의한 내 클래스 명을 작성