[스프링 데이터 JPA] 중요 요약본 2

sonnng·2023년 11월 21일
0

Spring

목록 보기
32/41
post-thumbnail

벌크성 수정, 삭제 연산을 쓸 때 JpaRepository사용시 @Modifying 필수!

이 어노테이션 없이 추상 메서드를 작성하게 된다면 레포지토리의 일반적인 getResultList()getSingleResult()로 인식해버리며 오류가 발생하게 된다.

순수 JPA 레포지토리로 벌크연산(수정)을 작성한 것을 spring data jpa로 바꾸는 과정을 살펴보면 다음과 같다.

public int bulkAgePlus(int age){
        return em.createQuery(
                "update Member m set m.age = m.age+1"
                + " where m.age>= :age"
        ).setParameter("age", age)
                .executeUpdate();
    }

이 코드를 spring data jpa로 바꿔보자. 파라미터가 들어가있으니 @Param으로 받아주면 되고 @Query로 쉽게 해당 쿼리를 그대로 작성할 수 있다.

 @Modifying
 @Query("update Member m set m.age = m.age+1 where m.age>= :age")
 int bulkAgePlus(@Param("age")int age);

이렇게 작성한 벌크연산은 JPA의 영속성 컨텍스트를 거치지 않고 바로 DB에 접근해 연산을 수행한다. 그렇기 때문에 동일하지 않게 되는 문제가 발생할 수 있다. 따라서 @PersistenceContext를 사용해서 엔티티 매니저를 호출해오고 em.clear()로 영속성컨텍스트 내부를 깨끗하게 비워주면(초기화) 해결된다.

@Test
    public void bulkUpdate(){
        memberRepository.save(new Member("member1", 10));
        memberRepository.save(new Member("member2", 19));
        memberRepository.save(new Member("member3", 20));
        memberRepository.save(new Member("member4", 21));
        memberRepository.save(new Member("member5", 40));

        int resultCount = memberRepository.bulkAgePlus(20);
        entityManager.clear();

        assertThat(resultCount).isEqualTo(3);
    }


EntityGraph

연관된 엔티티들을 SQL로 한번에 조회하는 방법이다. 이건 spring data jpa를 사용할 때 간단하게 fetch join을 하고 싶을 때 JPQL로 fetch join 대신에 어노테이션으로 쉽게 처리하는 방법이다.

먼저 연관된 엔티티들을 한번에 조회하려면 JPQL로 페치조인을 해야한다.

@Query("select m from Member m left join fetch m.team") 
List<Member> findMemberFetchJoin();

이렇게 쓴걸 JPA에서 제공하는 엔티티 그래프 기능으로 빠꿔주면 다음과 같다. jpql 없이 페치조인이 가능하다. 복잡하게 페치조인을 해야하는 경우만 jpql로 작성해주고 간단한 경우만 엔티티 그래프 기능을 사용하는걸 권장한다.

//공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"}) 
List<Member> findAll();

//JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
//메서드 이름으로 쿼리에서 특히 편리하다.

@EntityGraph(attributePaths = {"team"}) 
List<Member> findByUsername(String username)

사용자 정의 리포지토리 구현(확장 기능)

스프링 데이터 jpa가 제공하는 인터페이스를 직접 구현하면 구현해야하는 기능이 너무 많을 때, 다양한 이유로 인터페이스의 메서드를 직접 구현하고 싶은 경우 다음의 방법들이 있다. 이중에서 골라서 사용하면 된다.

  • JPA 직접 사용(EntityManager)
  • 스프링 jdbc template 사용
  • mybatis 사용
  • 데이터베이스 커넥션 직접 사용 등
  • Querydsl 사용

우선 사용자 정의 리포지토리를 구현하기 위해서 다음의 절차를 따르면 된다.

1. 사용자 정의 인터페이스 생성

public interface MemberRepositoryCustom { 
    List<Member> findMemberCustom();
}

2. 사용자 정의 인터페이스 구현 클래스

@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom { 
	private final EntityManager em;
	@Override
	public List<Member> findMemberCustom() {
		return em.createQuery("select m from Member m").getResultList();
	} 
}

구현 클래스를 작성할때 주의할 규칙은 리포지토리 인터페이스 이름 + Impl 를 작성해야한다는 점이다. 스프링 데이터 JPA가 인식해서 스프링 빈으로 등록하면 된다.

3. 사용자 정의 인터페이스 상속(JpaRepository를 상속하는 곳에서)

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
...
}

4. 사용자 정의 메서드 호출 코드 작성

List<Member> result = memberRepository.findMemberCustom();

이렇게 작성하면 된다.



참고로, 항상 사용자 정의 리포지토리가 필요한 것은 아니다.

이렇게 사용자 정의 리포지토리를 작성해놓으면 기존 스프링 데이터 jpa에 추가적으로 해당 메서드를 사용할 수 있게 된다.

@Test
public void callCustom(){
	List<Member> memberCustom = memberRepository.findMemberCustom();

}


Auditing

엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적하고 싶을 때 방법을 알아보면 순수 JPA를 사용하는 방법이랑 spring data jpa를 사용하는 방법 두 가지가 있다.

  • 등록일/수정일/등록자/수정자를 저장하고 싶다.

1) 순수 JPA를 사용하는 방법

@MappedSuperclass
@Getter
public class JpaBaseEntity {
	@Column(updatable = false) 
	private LocalDateTime createdDate; 
	private LocalDateTime updatedDate;
	
    @PrePersist
	public void prePersist() {
        LocalDateTime now = LocalDateTime.now(); 
        createdDate = now;
        updatedDate = now;
	}
    
    @PreUpdate
	public void preUpdate() {
        updatedDate = LocalDateTime.now();
	} 
}
public class Member extends JpaBaseEntity {}

JpaBaseEntity에서 @MappedSuperclass를 사용하는 이유는, 상속받는 클래스(member)에서 createdDate, updatedDate 등의 필드를 그대로 사용할 수 있도록 하기 위해서이다.

테스트 코드로 등록일과 수정일을 비교해본 결과

와 같이 확인됨을 알 수 있다.



2) 스프링 데이터 JPA를 사용하는 방법

(1) 설정
@EnableJpaAuditing :: 스프링부트설정 클래스에적용해야함
@EntityListeners(AuditingEntityListener.class) :: 엔티티에적용

(2) 사용 어노테이션
@CreatedDate, @LastModifiedDate , @CreatedBy, @LastModifiedBy

(3) 코드 작성

등록일, 수정일과 등록자, 수정자까지 넣는 방법

@EntityListeners(AuditingEntityListener.class) 
@MappedSuperclass
public class BaseEntity {
	@CreatedDate
	@Column(updatable = false) 
	private LocalDateTime createdDate; 

	@LastModifiedDate
	private LocalDateTime lastModifiedDate; 

	@CreatedBy
	@Column(updatable = false) 
	private String createdBy; 
    
    @LastModifiedBy
	private String lastModifiedBy;
}

+++ 등록자, 수정자를 처리해주는AuditorAware 스프링 빈으로 등록해야 한다.

@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
	public static void main(String[] args) {
          SpringApplication.run(DataJpaApplication.class, args);
	}
	@Bean
	public AuditorAware<String> auditorProvider() {
		return () -> Optional.of(UUID.randomUUID().toString()); 
	}
}

실무에서는 세션정보나 스프링 시큐리티 로그인 정보에서 ID정보를 받아서 사용하면 된다.



Web 확장 - 도메인 클래스 컨버터

Http 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩할 수 있다.

(1) 도메인 클래스 컨버터 사용 전 모습

@RestController
@RequiredArgsConstructor 
public class MemberController {
	private final MemberRepository memberRepository; 
	@GetMapping("/members/{id}")
	public String findMember(@PathVariable("id") Long id) { 
        Member member = memberRepository.findById(id).get();
		return member.getUsername(); 
	}
}

(2) 도메인 클래스 컨버터 사용 후 모습

@RestController
@RequiredArgsConstructor 
public class MemberController {
	private final MemberRepository memberRepository; 
	@GetMapping("/members/{id}")
    public String findMember(@PathVariable("id") Member member) {
		return member.getUsername(); 
	}
}

HTTP 요청은 회원 id 를받지만 도메인 클래스컨버터가 중간에 동작해서, 회원엔티티 객체를 반환하게 된다. 리파지토리를 사용해서 엔티티를 찾을 수 있기 때문이다.

➡️ 주의할 점 :: 도메인 클래스 컨버터로 엔티티를 파라미터로 받게 되면, 단순 조회용으로만 사용해야 한다.



병합 merge는 업데이트할때 사용하는 것이 아니다.

spring data jpa에서 save()메서드는 새로운 엔티티면 저장(persist)해주고 그렇지 않은 경우 병합(merge)해준다. 하지만 새로운 엔티티가 아닌 경우라고 해서 업데이트해줘야할 대상으로 보는건 옳지 않다고 한다. 엔티티의 값이 변경되는 경우는 더티체킹으로 자동으로 알아서 JPA에서 update쿼리를 날려주기 때문에 병합merge를 사용하는 건 옳지 않다. 병합은 영속화 된 것이 비영속상태가 된 후 다시 영속화할때 사용하는 메서드다.

0개의 댓글