본문 바로가기

Spring

양방향 연관관계와 영속성 전이 (Cascade) / Fetch / 페이징 정렬 / Dynamic Insert,Update

Cascade (영속성 전이)

  • 다대일에서 일쪽에 즉, 부모에 걸어야 함(게시물과 댓글 중 게시물에)
  • 양쪽 엔티티의 라이프사이클이 동일하거나 비슷할 때 거는 것
  • 영속성 전이는 현재 엔티티에서만 전이되야 함 → 댓글을 게시물이 아닌 다른 곳에서 하면 안 됨
  • 옵션 종류
    • ALL : 전체 상태 전이
    • PERSIST : 저장 상태 전이
    • REMOVE : 삭제 상태 전이
    • MERGE : 업데이트 상태 전이
    • REFERESH : 갱신 상태 전이
    • DETACH : 비영속성 상태 전이

예를 들어 Cascade.REMOVE를 하면 게시물이 삭제됐을 때 댓글도 삭제됨

@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
    private List<Child> children = new ArrayList<>();
    
    public void addChild(Child child) {
        children.add(child);
        child.setParent(this);
    }
}

@Entity
public class Child {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

// 사용 예
Parent parent = new Parent();
parent.setName("부모 엔티티");

Child child1 = new Child();
child1.setName("자식1");
Child child2 = new Child();
child2.setName("자식2");

parent.addChild(child1);
parent.addChild(child2);

 

@Service
@RequiredArgsConstructor
public class PostService {
    private final PostRepository postRepository;
    private final CommentRepository commentRepository;

    @Transactional
    public void addComment(Long postId, String content) {
        // 게시글 조회
        Post post = postRepository.findById(postId)
                .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다."));

        // 댓글 생성
        Comment comment = new Comment(content);
        post.addComment(comment); // Post 엔티티의 addComment 메서드 호출

        // 저장 (CascadeType.ALL 덕분에 post 저장 시 comment도 저장됨)
        postRepository.save(post);
    }
}

-> Parent를 DB에 저장하면, List<Child>에 있는 Child들도 DB에 저장

 

 

orphanRemoval (고아 객체 제거)

  • 다대일에서 일쪽에 즉, 부모에 걸어야 함(게시물과 댓글 중 게시물에)
  • Cascade.REMOVE 와 비슷한 용도로 삭제를 전파하는데 쓰임
  • 부모객체가 삭제됐을 때 자식이 삭제되고, 부모 객체의 리스트에서 해당 자식객체의 요소를 삭제한 경우에도 자식이 삭제됨
  • 리스트 요소로써의 영속성 전이도 해준다는 뜻
  • 옵션
    • true
    • false

parent1.getChildList().remove(0); // delete 쿼리가 나감

 

영속성 전이 최강 조합 : orphanRemoval=true + Cascade.ALL

자식 엔티티의 라이프 사이클이 부모 엔티티와 동일해지며, 직접 자식 엔티티의 생명주기를 관리할 수 있음

 

Fetch (조회시점)

  • @ManyToMany, @OneToMany, @ManyToOne, @OneToOne에 사용
  • 기본 LAZY(부모 조회 시 자식은 필요할 때 조회) 를 설정한 뒤에 필요할때만 fetch Join 을 수행
  • 같이 쓰이는 연관관계 일 경우만 EAGER(부모 조회 시 자식도 같이 조회) 를 설정

 

JpaRepository 효율적으로 사용하는 방법

Optional 제거하기 : 비지니스 로직에서 Optional 처리를 위한 추가적인 작업을 방지

public interface UserRepository extends JpaRepository<User, Long> {
// Default 메소드를 사용하여 findById의 Optional을 내부적으로 처리
default User findUserById(Long id) {
        return findById(id).orElseThrow(() -> new DataNotFoundException("User not found with id: " + id));
    }
}

 

메서드명 간소화하기 : default 메서드를 활용하면 긴 메서드명을 간결하고 명확하게 표현

public interface ProductRepository extends JpaRepository<Product, Long> {
// 기존의 긴 쿼리 메소드
List<Product> findAllByCategoryAndPriceGreaterThanEqualAndPriceLessThanEqualOrderByPriceAsc(String category, BigDecimal minPrice, BigDecimal maxPrice);

// Default 메소드를 사용하여 간결한 메소드명 제공
default List<Product> findProductsByCategoryAndPriceRange(String category, BigDecimal minPrice, BigDecimal maxPrice) {
        return findAllByCategoryAndPriceGreaterThanEqualAndPriceLessThanEqualOrderByPriceAsc(category, minPrice, maxPrice);
    }
}

 

 

테이블 객체로 페이지 조회하기

페이징 처리 프로세스

  1. PageRequest 를 사용하여 Pageable에 페이징 정보를 담아 객체화
  2. Pageable을 JpaRepository가 상속된 인터페이스의 메서드에 T(Entity)와 함꼐 파라미터로 전달
  3. 2번의 메서드의 return 으로 Page<T>가 응답
  4. 응답된 Page<T>에 담겨진 Page 정보를 바탕으로 로직을 처리

페이징 정렬

Sort 클래스

Sort sort1 = Sort.by("name").descending();     // 내림차순
Sort sort2 = Sort.by("password").ascending();  // 오름차순
Sort sortAll = sort1.and(sort2);      // 2개이상 다중정렬도 가능하다
Pageable pageable = PageRequest.of(0, 10, sortAll);  // pageable 생성시 추가

 

@Query 사용시 Alias(쿼리에서 as 로 지정한 문구) 를 기준으로 정렬

// 아래와 같이 AS user_password 로 Alias(AS) 를 걸어주면
@Query("SELECT u.user_name, u.password AS user_password FROM user u WHERE u.username = ?1")
List<User> findByUsername(String username, Sort sort);
// 이렇게 해당 user_password 를 기준으로 정렬할 수 있다.
List<User> users = findByUsername("user", Sort.by("user_password"));

 

JPA 속도 향상하는 방법 : 필요한 부분만 갱신하기

@DynamicInsert

Insert 쿼리를 날릴 때 null 인 값은 제외하고 쿼리문이 만들어짐

Hibernate: 
    insert 
    into
        users
        (username, id) 
    values
        (?, ?)  // 133ms 소요

 

@DynamicUpdate

Update 쿼리를 날릴 때 null인 값은 제외하고 쿼리문이 만들어짐

Hibernate: 
    update
        users 
    set
        password=? 
    where
        id=?  // 134ms

 

-> 필드가 많아질수록 둘 다 속도가 향상된다!