ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링 JPA (스프링 데이터 JPA - 7)
    Programming/Spring Data JPA 2021. 1. 1. 15:48
    728x90

     

    - @EnableJpaRepositories

    스프링 부트를 사용하지 않을 때는 @Configuration과 같이 사용

     

     

    1. 엔티티 저장하기

     

    - save()호출되면 현재 상태에 따라 persist, merge가 호출됨

    Transient상태이면 persist - (create)

    Detached상태이면 merge - (update, 없으면 create)

     

    - Transient인지 Detached인지 어떻게 판단?

    엔티티의 @Id프로퍼티를 찾아서 null이면 Transient 상태로 판단함

     

    - EntityManager.persist()

    메소드에 넘긴 객체를 Persistent 상태로 변경

     

    - EntityManager.merge()

    메소드에 넘긴 객체의 복사본을 만들고, 그 복사본을 다시 Persistent 상태로 변경하고 복사본을 반환

     

    *반드시 save를 호출해 얻은 객체를 사용하기!! - 해당 객체는 영속성 컨텍스트에 등록되어 관리되므로,

    변경되면 save하지 않아도 반영될 수 있음!!*

     

     

    2. 쿼리 메소드

      

    1) 메소드로 구현 (스프링 데이터 common 에서 한 방식과 동일)

     

    2) 쿼리 찾아쓰기

     

    - @NamedQuery (@NamedNativeQuery)

    @Entity
    @NamedQuery(name = "Post.findByTitle", query = "SELECT p FROM Post AS p WHERE p.title = ?1")
    public class Post {
    
        @Id @GeneratedValue
        private Long id;
    
        private String title;
    }
    
    public interface PostRepository extends JpaRepository<Post, Long> {
    
        List<Post> findByTitle(String title);
    
    }
    

    -> 도메인 엔티티 클래스가 더러워짐, SQL 코드가 많아짐

    따라서 이 방식보다는 Repository 메소드 위에 @Query 를 붙이는게 더 나음

     

     

    3. Sort

    - Pageable이나 Sort를 매개변수로 사용 가능

     

    아래와 같이 @Query와 같이 사용할 때 제약사항 존재

    - 프로퍼티, alias 만 사용 가능

    - 함수 호출해야 하는 경우에는 JpaSort.unsafe() 사용

     

    public interface PostRepository extends JpaRepository<Post, Long> {
    
        @Query("SELECT p FROM Post AS p WHERE p.title = ?1")
        List<Post> findByTitle(String title, Sort sort);
    
    }
    
    @RunWith(SpringRunner.class)
    @DataJpaTest
    public class PostRepositoryTest {
    
        @Autowired
        PostRepository postRepository;
    
        @Test
        public void findTitle() {
            Post post = new Post();
            post.setTitle("hongchan jiyun");
            postRepository.save(post);
    
    //        List<Post> found = postRepository.findByTitle("hongchan jiyun", Sort.by("title"));
            List<Post> found = postRepository.findByTitle("hongchan jiyun", JpaSort.unsafe("LENGTH(title)"));
            assertThat(found.size()).isEqualTo(1);
        }
    
    }

     

     

    - Named Parameter 와 SpEL

     

    다음과 같이 표현 가능

    public interface PostRepository extends JpaRepository<Post, Long> {
    
        @Query("SELECT p FROM #{#entityName} AS p WHERE p.title = :title")
        List<Post> findByTitle(@Param("title") String title, Sort sort);
    
    }
    

     

     

    4. EntityGraph

    - 쿼리 메소드 마다 연관 관계의 Fetch 모드를 설정 할 수 있다.

     

    @NamedEntityGraph

    - @Entity에서 재사용할 여러 엔티티 그룹을 정의

    @NamedEntityGraph(name = "Comment.post",
            attributeNodes = @NamedAttributeNode("post"))
    @Entity
    public class Comment {
    
        @Id @GeneratedValue
        private Long id;
    
        private String comment;
    
        @ManyToOne(fetch = FetchType.LAZY)
        private Post post;
    }
    

     

    @EntityGraph

    - 의의 정의된 엔티티 그룹을 사용함

    - 두 가지 타입 존재

    FETCH (기본 값) : 설정된 엔티티 애트리뷰트는 EAGER 패치를, 나머지는 LAZY 패치를 함 (*여기서 나머지란 연관관계의 것들을 말함)

    LOAD   : 설정된 엔티티 애트리뷰트는 EAGER 패치를, 나머지는 기본 패치 전략을 따름

    public interface CommentRepository extends JpaRepository<Comment, Long> {
    
        @EntityGraph(value = "Comment.post")
        Optional<Comment> getById(Long id);
    }
    

     

    // getById 호출
    
    Hibernate: 
        select
            comment0_.id as id1_0_0_,
            post1_.id as id1_1_1_,
            comment0_.comment as comment2_0_0_,
            comment0_.post_id as post_id3_0_0_,
            post1_.title as title2_1_1_ 
        from
            comment comment0_ 
        left outer join
            post post1_ 
                on comment0_.post_id=post1_.id 
        where
            comment0_.id=?
    
    ========================================================================
    // findById 호출
    
    Hibernate: 
        select
            comment0_.id as id1_0_0_,
            comment0_.comment as comment2_0_0_,
            comment0_.post_id as post_id3_0_0_ 
        from
            comment comment0_ 
        where
            comment0_.id=?

     

    *재사용하지 않을 시 아래와 같이 그룹을 설정하지 않고 바로 사용 가능

    public interface CommentRepository extends JpaRepository<Comment, Long> {
    
    //    @EntityGraph(value = "Comment.post")
        @EntityGraph(attributePaths = "post")
        Optional<Comment> getById(Long id);
    }
    

     

     

    5. Projection

    - 엔티티의 일부 데이터만 가져오기

     

    1) 인터페이스 기반 프로젝션

    - Closed 프로젝션

    쿼리를 최적화할 수 있음.

     

    public interface CommentSummary {
    
        String getComment();
    
        int getUp();
    
        int getDown();
    
    }
    
    public interface CommentRepository extends JpaRepository<Comment, Long> {
    
        Optional<CommentSummary> findByPost_Id(Long id);
    }
    

    SELECT comment, up, down ~~ SQL 문과 같은 효과 -> 쿼리 최적화

     

     

    - Open 프로젝션

    @Value(SpEL)을 사용해서 연산이 가능

    쿼리 최적화를 할 수 없음 (SELECT *)

    public interface CommentSummary {
    
        String getComment();
    
        int getUp();
    
        int getDown();
    
        @Value("#{target.up + ' ' + target.down}")
        String getVotes();
    
    }
    

     

    아래와 같이 default 메소드를 사용하면 특정 값만 가져오므로 쿼리 최적화 가능

    public interface CommentSummary {
    
        String getComment();
    
        int getUp();
    
        int getDown();
    
        default String getVotes() {
            return getUp() + " " + getDown();
        }
        
    }
    

     

    2) 클래스 기반 프로젝션

    클래스 코드이다보니 코드가 길어짐

     

    3) 다이나믹 프로젝션

    - 프로젝션용 메소드 하나만 정의하고 실제 프로젝션 타입은 타입 인자로 전달하기

    public interface CommentRepository extends JpaRepository<Comment, Long> {
    
        <T> Optional<T> findByPost_Id(Long id, Class<T> type);
    //    Optional<CommentSummary> findByPost_Id(Long id);
    }
    

     

     

    6. Specification

    -  QueryDSL의 Predicate와 비슷함

    - 보통 간단한 쿼리는 쿼리 메소드로 만들지만, 복잡한 쿼리는 이해하기 어려움. 따라서 보통 jOOQ와 QueryDSL을 같이 사용하거나 반복적인 조건이 생긴다면 Specification으로 만들어서 재사용함.

     

    1) 의존성, 플러그인 설정 

    2) IDE에 애노테이션 처리기 설정

    2-1) 프로젝트 빌드

    3) 코딩 시작

     

    public class CommentSpecs {
    
        public static Specification<Comment> isBest() {
            return (Specification<Comment>)
                    (root, query, builder) ->
                            builder.isTrue(root.get(Comment_.best));
        }
    
        public static Specification<Comment> isGood() {
            return (Specification<Comment>)
                    (root, query, builder) ->
                            builder.greaterThanOrEqualTo(root.get(Comment_.up), 10);
        }
    }
    
    public interface CommentRepository extends JpaRepository<Comment, Long>, JpaSpecificationExecutor<Comment> {
    
    }
    

     

    @Test
        public void specs() {
            commentRepository.findAll(CommentSpecs.isBest().or(CommentSpecs.isGood()));
        }

     

     

    7. 트랜잭션

    스프링 데이터 JPA가 제공하는 Repository의 모든 메소드에는 기본적으로 @Transactional이 적용되어 있다.

     

    - Isolation

    여러 개의 트랜잭션을 어떻게 처리할지에 대한 전략

     

    - Propagation

    네스티드 트랜잭션의 경우, 기존에 트랜잭션을 사용할지? 새로운 트랜잭션으로 할지?

     

    - readOnly

    Flush 모드를 NEVER로 설정하여, Dirty checking을 하지 않아도 됨.

    Flush란? 디비에 반영

     

     

    보통 Service단의 메소드에서 하나의 트랜잭션으로 처리하고자 많이 사용함!

    Repository에서도 직접 불러오는 경우가 있으니, 이를 붙이는 거는 좋은 습관임. (+readOnly)

     

     

    8. Auditing

    - 엔티티의 변경 시점에 언제, 누가 변경했는지에 대한 정보를 기록하는 기능

     

    자동설정을 지원하지 않아 다음과 같이 설정해야 함

    @SpringBootApplication
    @EnableJpaAuditing(auditorAwareRef = "accountAuditAware")
    public class SpringDataJpaApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringDataJpaApplication.class, args);
        }
    
    }
    
    @Service
    public class AccountAuditAware implements AuditorAware<Account> {
        @Override
        public Optional<Account> getCurrentAuditor() {
            System.out.println("Loocking for current User at spring security");
            
            // 스프링 시큐리티로부터 유저 정보 가져옴
            
            return Optional.empty();
        }
    }
    
    @Entity
    @EntityListeners(AuditingEntityListener.class)
    public class Comment {
    
        @Id @GeneratedValue
        private Long id;
    
    	...
    
        @CreatedBy
        @ManyToOne
        private Account createBy;
    
        @LastModifiedDate
        private Date updated;
    
        @LastModifiedBy
        @ManyToOne
        private Account updatedBy;
    
    }
    

     

    JPA의 라이프 사이클 이벤트

    - @PrePersist

    - @PreUpdate

    등을 사용하여 Auditing 기능을 구현할 수 있다.

     

     

    인프런 백기선님 '스프링 데이터 JPA’ 강의를 듣고 정리한 내용입니다.
    728x90

    댓글

Designed by Tistory.