Entity 관계 매핑
Fetch 전략
: 부모클래스를 조회할 때 자식클래스를 계속 로딩 할 것이냐, 아니면 실제로 사용할 때만 로딩 할 것이냐. FetchType은 부모클래스에 지정하면 된다.
1. Eager Fetching (즉시 로딩)
- 사용자를 조회할 때 주문한 내역을 반드시 가져와야 한다면 Eager Fatching을 지정하여 무조건 가져올 수 있도록 한다.
- 단점: 불필요한 데이터 조회때문에 성능히 저하될 수 있다.
import javax.persistence.*;
import java.util.List;
@Entity
@Getter
@Setter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
@OneToMany(fetch = FetchType.EAGER, mappedBy = "user")
private List<Order> orders;
}
@Entity
@Getter
@Setter
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String product;
private int quantity;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
2. Lazy Fetching(지연 로딩)
- 사용자의 주문내역이 필요할 때만 조회하게 된다.
- 엔티티를 조회할 때 로딩하지 않고 실제로 사용할 때 로딩한다.
- 단점: 추가 쿼리가 발생할 수 있다.
import javax.persistence.*;
import java.util.List;
@Entity
@Getter
@Setter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
private List<Order> orders;
}
@Entity
@Getter
@Setter
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String product;
private int quantity;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
그렇다면 실제로 사용한다는 것은 언제를 말하는 것일까?
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Order> orders; // 사용자의 주문 목록
// Constructor, Getter, Setter 생략
public List<Order> getOrders() {
return orders; // orders 리스트를 반환
}
}
public class UserDto {
private Long id;
private String name;
private int orderCount; // 주문 수를 포함하는 필드
public UserDto(Long id, String name, int orderCount) {
this.id = id;
this.name = name;
this.orderCount = orderCount; // 주문 수 초기화
}
// Getter 및 Setter 생략
}
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUserById(@PathVariable Long id) {
User user = userService.findById(id);
if (user != null) {
// user 객체를 DTO로 변환하여 반환
UserDto userDto = new UserDto(user.getId(), user.getName(), user.getOrders().size());
return ResponseEntity.ok(userDto);
} else {
return ResponseEntity.notFound().build();
}
}
}
- Controller:
- 사용자가 특정 User를 조회하기 위해 /users/{id} 엔드포인트에 GET 요청을 보냅니다.
- Controller는 UserService의 findById 메서드를 호출하여 User 객체를 가져옵니다.
- 이 시점에서 User 객체는 로드되지만, orders 리스트는 아직 로드되지 않습니다.
- Service:
- findById 메서드는 userRepository를 통해 User를 조회합니다. 이때 orders는 Lazy Loading으로 설정되어 있기 때문에 메모리에 로드되지 않습니다.
- DTO 변환:
- User 객체에서 이름과 ID를 가져오고, orders의 크기를 구하여 UserDto 객체를 생성합니다. 이때 getOrders().size()를 호출하는 순간 orders가 로드됩니다.
- 최종적으로 UserDto를 ResponseEntity로 반환하여 클라이언트에게 전달합니다.
Cascade 유형
: 부모클래스와 자식클래스의 사이를 정해놓은 것이다. 부모클래스를 저장, 삭제, 병합, 새로고침, 분리할 때 자식클래스를 어떻게 할 것인가를 지정하는 것이다. CascadeType은 부모클래스에 지정하면 된다.
- CascadeType.PERSIST: 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장
- CascadeType.REMOVE: 부모 엔티티를 삭제할 때 자식 엔티티도 함께 삭제
- CascadeType.MERGE: 부모 엔티티를 병합할 때 자식 엔티티도 함께 병합
- CascadeType.REFRESH: 부모 엔티티를 새로 고칠 때 자식 엔티티도 함께 새로고침
- CascadeType.DETACH: 부모 엔티티를 영속성 컨텍스트에서 분리할 때 자식 엔티티도 함께 분리
- CascadeType.ALL: 모든 Cascade 작업을 전파
근데 이것을 보고 그냥 CascadeType.ALL로 설정하면 편하지 않나.. 라는 생각이 들었다. 그러나 만약 부모객체인 User의 정보를 삭제하는데, 자식객체인 Order의 주문기록은 필요 한 경우가 있다. 이럴 경우 CascadeType.ALL을 설정해버리면 모든 정보가 사라지기 때문에 곤란하게 된다. 이럴 경우는 아래와 같이 삭제를 제외한 나머지 기능을 명시하면 된다.
@Entity
public class ParentEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
private List<ChildEntity> childEntities;
// Getters and Setters
}
@Entity
public class ChildEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Getters and Setters
}
JPA Query Method
JPA Query Method는 쉽게 말해 메서드 이름을 입력하면 쿼리문을 자동으로 작성해 주는 개꿀 기능이다. 모든 기능이 되는 것은 아니고 생성, 조회, 업데이트, 삭제(CRUD) 기능만 가능하다.
기본 쿼리 Method
: 기본 Method는 별도 Repository 단에서 설정해 줄 필요가 없다. 그 이유는 JpaRepository 인터페이스 상속을 3번 올라가다 보면 CrudRepository가 나오는데 그 인터페이스에서 메소드를 지정해줬기 때문이다.
- save(S entity): 엔티티를 저장하거나 업데이트합니다.
- saveAll(Iterable<S> entities): 여러 엔티티를 저장합니다.
- findById(ID id): 주어진 ID로 엔티티를 조회합니다.
- findAll(): 모든 엔티티를 조회합니다.
- findAllById(Iterable<ID> ids): 주어진 ID 목록에 해당하는 모든 엔티티를 조회합니다.
- deleteById(ID id): 주어진 ID의 엔티티를 삭제합니다.
- delete(S entity): 주어진 엔티티를 삭제합니다.
- deleteAll(Iterable<? extends S> entities): 여러 엔티티를 삭제합니다.
- deleteAll(): 모든 엔티티를 삭제합니다.
- count(): 엔티티의 총 개수를 반환합니다.
- existsById(ID id): 주어진 ID의 엔티티가 존재하는지 확인합니다.
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
}
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
// 기본 CRUD 메서드는 JpaRepository에서 자동으로 제공됨
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// 사용자 저장 또는 업데이트
public User saveUser(User user) {
return userRepository.save(user);
}
// ID로 사용자 조회
public Optional<User> getUserById(Long id) {
return userRepository.findById(id);
}
// 모든 사용자 조회
public List<User> getAllUsers() {
return userRepository.findAll();
}
// ID로 사용자 삭제
public void deleteUserById(Long id) {
userRepository.deleteById(id);
}
// 사용자 수 카운트
public long countUsers() {
return userRepository.count();
}
// ID로 사용자 존재 여부 확인
public boolean userExists(Long id) {
return userRepository.existsById(id);
}
}
쿼리 생성 전략
: 여기에 count, find, delete, exist 메서드에는 뒤에 By를 붙여서 특정 조건의 레코드를 조회할 수 있다. 또한 쿼리 키워드를 조합하여 복수도 조회할 수 있다. 아래의 예시의 경우 findByUsernameEmail의 메소드를 Controller 에서 작성하고 apiService에서 And와 Or를 바꿔가며 String과 Email을 활용하여 조회할 수 있도록 설정하였다.
@RestController
@RequestMapping("/apis")
@RequiredArgsConstructor
public class ApiController {
private final MemberService memberService;
private final ApiService apiService;
@GetMapping("/member-username-email")
public ResponseEntity<?> findByUsernameEmail(@RequestParam("Username") String username,
@RequestParam("Email") String email){
ResponseDto<Member> responseDto = new ResponseDto<>();
try{
// findByUsernameAndEmail 혹은 findByUsernameOrEmail
List<Member> memberList = apiService.findByUsernameOrEmail(username, email);
responseDto.setStatusCode(200);
responseDto.setStatusMessage("OK");
responseDto.setDataList(memberList);
return ResponseEntity.ok(responseDto);
} catch(Exception e){
responseDto.setStatusCode(500);
responseDto.setStatusMessage(e.getMessage());
return ResponseEntity.internalServerError().body(responseDto);
}
}
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndEmail(String username, String email);
List<Member> findByUsernameOrEmail(String username, String email);
}
조건 연산자
: 문자열을 포함했냐, 패턴이 일치했냐, 문자열로 끝났냐 등 특정한 조건을 걸어서 조회하는 방법이다.
- 이름 관련
- findByName(String name) : 특정 이름을 가진 사용자 조회
- findByNameEquals(String name) : 특정 이름을 가진 사용자 조회 (equals 사용). Equals는 Default 값이다.
- findByNameNot(String name) : 특정 이름이 아닌 사용자 조회
- findByNameStartingWith(String prefix) : 이름이 특정 문자열로 시작하는 사용자 조회
- findByNameContaining(String substring) : 이름에 특정 문자열을 포함하는 사용자 조회
- 이메일 관련
- findByEmailLike(String emailPattern) : 이메일이 특정 패턴과 일치하는 사용자 조회
- findByEmailEndingWith(String suffix) : 이메일이 특정 문자열로 끝나는 사용자 조회
- findByEmailIsNull() : 이메일이 null인 사용자 조회
- findByEmailIsNotNull() : 이메일이 null이 아닌 사용자 조회
- 나이 관련
- findByAgeGreaterThan(int age) : 나이가 특정 값보다 큰 사용자 조회
- findByAgeLessThan(int age) : 나이가 특정 값보다 작은 사용자 조회
- findByAgeBetween(int startAge, int endAge) : 나이가 특정 두 값 사이에 있는 사용자 조회
- ID 관련
- findByIdIn(List<Long> ids) : 주어진 ID 목록에 포함된 사용자 조회
- 활성 상태 관련
- findByActiveTrue() : 특정 boolean 값이 true인 사용자 조회
- findByActiveFalse() : 특정 boolean 값이 false인 사용자 조회
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
// User 엔티티에 대한 리포지토리 인터페이스
public interface UserRepository extends JpaRepository<User, Long> {
// 특정 이름을 가진 사용자 조회. 기본적으로 Equals가 들어가 있음
List<User> findByName(String name);
List<User> findByNameEquals(String name);
// 특정 이름이 아닌 사용자 조회
List<User> findByNameNot(String name);
// 이메일이 특정 패턴과 일치하는 사용자 조회
List<User> findByEmailLike(String emailPattern);
// 이름이 특정 문자열로 시작하는 사용자 조회
List<User> findByNameStartingWith(String prefix);
// 이메일이 특정 문자열로 끝나는 사용자 조회
List<User> findByEmailEndingWith(String suffix);
// 이름에 특정 문자열을 포함하는 사용자 조회
List<User> findByNameContaining(String substring);
// 나이가 특정 값보다 큰 사용자 조회
List<User> findByAgeGreaterThan(int age);
// 나이가 특정 값보다 작은 사용자 조회
List<User> findByAgeLessThan(int age);
// 나이가 특정 두 값 사이에 있는 사용자 조회
List<User> findByAgeBetween(int startAge, int endAge);
// 주어진 ID 목록에 포함된 사용자 조회
List<User> findByIdIn(List<Long> ids);
// 이메일이 null인 사용자 조회
List<User> findByEmailIsNull();
// 이메일이 null이 아닌 사용자 조회
List<User> findByEmailIsNotNull();
// 특정 boolean 값이 true인 사용자 조회
List<User> findByActiveTrue();
// 특정 boolean 값이 false인 사용자 조회
List<User> findByActiveFalse();
}
정렬 및 페이징 처리
: List 를 활용해 정렬하거나 페이징 처리 자료를 정렬하여 받아볼 수 있도록 지정한다. 페이징 처리를 할 때에는 pageable 객체를 항상 넘겨줘야 한다.
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
// Member 엔티티에 대한 리포지토리 인터페이스
public interface MemberRepository extends JpaRepository<Member, Long> {
// 주어진 사용자 이름으로 정렬된 사용자 목록 조회 (생성 날짜 내림차순)
Page<Member> findByUsernameOrderByCreatedDateDesc(String username, Pageable pageable);
// 활성 상태인 사용자 목록을 페이징 처리하여 조회
Page<Member> findByActiveTrue(Pageable pageable);
// 특정 나이에 해당하는 사용자 목록 조회 (정렬 포함)
List<Member> findByAge(int age, Sort sort);
// 활성 상태인 사용자 목록 조회 (정렬 포함)
List<Member> findByActiveTrue(Sort sort);
}
- 이 때 페이징 처리는 어떻게 진행되는지 예시로 설명해주겠다. page와 pageable import는 여러개가 나오는데 domain을 해야함을 명심해야 한다.
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long> {
Page<Member> findALLByOrderByIdDesc(Pageable pageable);
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
@Service
public class MemberService {
@Autowired
private MemberRepository memberRepository;
@Override
public Page<Member> findAll(Pageable pageable) {
return memberRepository.findALLByOrderByIdDesc(pageable);
}
}
@GetMapping("/members")
public ResponseEntity<?> findAll(@PageableDefault(page = 0, size = 5) Pageable pageable) {
ResponseDto<Member> responseDto = new ResponseDto<>();
try {
Page<Member> memberList = apiService.findAll(pageable);
responseDto.setStatusCode(HttpStatus.OK.value());
responseDto.setStatusMessage("OK");
responseDto.setDataPaging(memberList);
return ResponseEntity.ok(responseDto);
} catch(Exception e) {
responseDto.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
responseDto.setStatusMessage(e.getMessage());
return ResponseEntity.internalServerError().body(responseDto);
}
}
이제 swagger에서 page를 0, size를 1로 지정해 보자.
현재 나는 게시물이 3개가 있었다. page size를 1로 지정했기 때문에 totalPages는 3으로 나오고, content는 내림차순으로 id가 3번부터 나온다. 게시물이 1개만 나오는 이유는 page size를 1로 지정했기 때문이다.
관계형 쿼리 메소드
: Entity 간의 관계를 Entity 관계 매핑으로 지정해 주었다(@OneToMany, @ManyToOne). 이 관계를 이용하여 자료를 조회할 때 해당하는 Entity를 조회 후 필드를 가져올 수 있도록 지정해주었다. 아래 예시는 Member Entity가 @OneToMany, FreeBoard Entity가 @ManyToOne으로 지정되어 있음을 명시하고 읽어보기 바란다.
package com.bit.springboard.repository;
import com.bit.springboard.entity.FreeBoard;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface FreeboardRepository extends JpaRepository<FreeBoard, Long> {
List<FreeBoard> findByMemberUsername(String username);
}
package com.bit.springboard.service.impl;
@Service
@RequiredArgsConstructor
public class ApiServiceImpl implements ApiService {
private final FreeboardRepository freeboardRepository;
@Override
public List<FreeBoard> findByMemberUsername(String username) {
return freeboardRepository.findByMemberUsername(username);
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class OrderController {
@Autowired
private ApiService apiService;
@GetMapping("/boards-username")
public ResponseEntity<?> findByMemberUsername(@RequestParam("username") String username){
ResponseDto<FreeBoard> responseDto = new ResponseDto<>();
try{
List<FreeBoard> freeBoardList = apiService.findByMemberUsername(username);
responseDto.setStatusCode(200);
responseDto.setStatusMessage("OK");
responseDto.setDataList(freeBoardList);
return ResponseEntity.ok(responseDto);
}catch (Exception e){
responseDto.setStatusCode(500);
responseDto.setStatusMessage(e.getMessage());
return ResponseEntity.internalServerError().body(responseDto);
}
}
Member 클래스의 Username이 String과 연결된 FreeBoard의 자료를 찾도록 지시하였다.
결과값으로 member username이 id가 1인데, 이에 해당하는 freeboard의 id는 2가 해당되어 해당 자료를 갖고오게 되었다.
console 창을 확인해보면 freeboard의 자료를 가져오고, 조건으로 member의 board_id를 가져오도록 지시하고 있다. 앞서 말한것처럼 자동으로 join을 하고 있는 것이다.
'백엔드 > Spring Boot' 카테고리의 다른 글
Spring Boot / 이메일 인증 (아이디, 비밀번호 찾기) (1) | 2024.10.04 |
---|---|
Spring Boot / Swagger 홈페이지 활용 (403에러) (0) | 2024.10.04 |
JSON 형태 웹 페이지(Chrome) 에서 깔끔하게 확인하기 (0) | 2024.08.17 |
스프링 부트 설정(Intelli J) (0) | 2024.08.17 |