중급자를 위해 준비한
[웹 개발, 백엔드] 강의입니다.
JPA(Java Persistence API)를 보다 쉽게 사용할 수 있도록 여러 기능을 제공하는 스프링 데이터 JPA에 대해 학습합니다.
✍️
이런 걸
배워요!
ORM에 대한 이해
JPA 프로그래밍
Bean 생성 방법
스프링 JPA가 어렵게 느껴졌다면?
개념과 원리, 실제까지 확실하게 학습해 보세요.
제대로 배우는
백기선의 스프링 데이터 JPA
JPA(Java Persistence API)를 보다 쉽게 사용할 수 있도록 여러 기능을 제공하는 스프링 데이터 JPA에 대해 학습합니다.
강의 :
https://www.inflearn.com/course/스프링-데이터-jpa#reviews
강의자료 :
https://docs.google.com/document/d/1IjSKwMEsLdNXhRLvFk576VTR03AKTED_3jMsk0bHANg/edit
소스 코드
https://github.com/braverokmc79/springdatajpa
https://github.com/braverokmc79/demojpa3
- 강좌 소개 
Application -> 스프링 데이터 JPA (-> JPA -> JDBC) -> Database
- 강사 소개 
백기선
마이크로소프트(2+) <- 아마존(1) <- 네이버(4.5) <- SLT(2.5) ...
강좌
- 스프링 프레임워크 입문 (Udemy) 
- 백기선의 스프링 부트 (인프런) 
특징
- 스프링 프레임워크 중독자 
- JPA 하이버네이트 애호가 
- 유튜브 / 백기선 
[2부: 스프링 데이터 JPA 활용]
31.스프링 데이터 JPA 1. JpaRepository
강의 :
https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13774&tab=curriculum
@EnableJpaRepositories
- 스프링 부트 사용할 때는 사용하지 않아도 자동 설정 됨. 
- 스프링 부트 사용하지 않을 때는 @Configuration과 같이 사용. 
@Repository 애노테이션을 붙여야 하나 말아야 하나...
- 안붙여도 됩니다. 
- 이미 붙어 있어요. 또 붙인다고 별일이 생기는건 아니지만 중복일 뿐입니다. 
스프링 @Repository
- SQLExcpetion 또는 JPA 관련 예외를 스프링의 DataAccessException으로 변환 해준다. 
package com.example.demojap3.post;
import org.assertj.core.api.Assertions;
import org.checkerframework.checker.units.qual.A;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
@Rollback(value = false)
class PostRepositoryTest {
    @Autowired
    private PostRepository postRepository;
    @Test
    public void crud(){
        Post post =new Post();
        post.setTitle("jpa");
        postRepository.save(post);
       // List<Post> all=postRepository.findAll();
       // Assertions.assertThat(all.size()).isEqualTo(1);
    }
}
32.스프링 데이터 JPA 2. JpaRepository.save() 메소드
강의 :
https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13775&tab=curriculum
JpaRepository의 save()는 단순히 새 엔티티를 추가하는 메소드가 아닙니다.
- Transient 상태의 객체라면 EntityManager.persist() 
- Detached 상태의 객체라면 EntityManager.merge() 
Transient인지 Detached 인지 어떻게 판단 하는가?
- 엔티티의 @Id 프로퍼티를 찾는다. 해당 프로퍼티가 null이면 Transient 상태로 판단하고 id가 null이 아니면 Detached 상태로 판단한다. 
- 엔티티가 Persistable 인터페이스를 구현하고 있다면 isNew() 메소드에 위임한다. 
- JpaRepositoryFactory를 상속받는 클래스를 만들고 getEntityInfomration()을 오버라이딩해서 자신이 원하는 판단 로직을 구현할 수도 있습니다. 
EntityManager.persist()
- https://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#persist(java.lang.Object) 
- Persist() 메소드에 넘긴 그 엔티티 객체를 Persistent 상태로 변경합니다. 
EntityManager.merge()
- https://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#merge(java.lang.Object) 
- Merge() 메소드에 넘긴 그 엔티티의 복사본을 만들고, 그 복사본을 다시 Persistent 상태로 변경하고 그 복사본을 반환합니다. 
영속성이 적용된 항상 반환된 값을 이용 해라
Post savedPost
savedPost 값 사용
updatePost 값 사용
    @Test
    public void save(){
        Post post=new Post();
        post.setId(1l);
        post.setTitle("jpa");
        Post savedPost=postRepository.save(post); //persist
        Assertions.assertThat(entityManager.contains(post)).isFalse();
        Assertions.assertThat(entityManager.contains(savedPost)).isTrue();
        Assertions.assertThat(savedPost==post);
        Post postUpdate=new Post();
        postUpdate.setId(1l);
        postUpdate.setTitle("hibernate");
        Post updatePost= postRepository.save(postUpdate); //update
        Assertions.assertThat(entityManager.contains(updatePost)).isTrue();
        Assertions.assertThat(entityManager.contains(postUpdate)).isFalse();
        Assertions.assertThat(updatePost==postUpdate);
        List<Post> all=postRepository.findAll();
        Assertions.assertThat(all.size()).isEqualTo(1);
    }
33.스프링 데이터 JPA 3. JPA 쿼리 메소드
강의 :
쿼리 생성하기
- https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation 
- And, Or 
- Is, Equals 
- LessThan, LessThanEqual, GreaterThan, GreaterThanEqual 
- After, Before 
- IsNull, IsNotNull, NotNull 
- Like, NotLike 
- StartingWith, EndingWith, Containing 
- OrderBy 
- Not, In, NotIn 
- True, False 
- IgnoreCase 
쿼리 찾아쓰기
- 엔티티에 정의한 쿼리 찾아 사용하기 JPA Named 쿼리 - @NamedQuery 
- @NamedNativeQuery 
 
- 리포지토리 메소드에 정의한 쿼리 사용하기 - @Query 
- @Query(nativeQuery=true) 
 
PostRepository
package com.example.demojap3.post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface PostRepository extends JpaRepository<Post, Long> {
     List<Post> findByTitleStartsWith(String title);
     @Query("SELECT p  FROM Post AS p WHERE p.title =:title")
     List<Post> findByTitle(@Param("title") String title);
}
Post
@NamedQuery 쿼리 사용시
@Entity
@Data
@ToString(of={"title", "content"})
@NoArgsConstructor(access = AccessLevel.PUBLIC)
//@NamedQuery(name="Post.findByTitle", query="SELECT p  FROM Post AS p WHERE p.title =:title")
public class Post {
~
PostRepositoryTest
    @Test
    public void findByTitleStartWidth(){
        savePost();
        List<Post> all =postRepository.findByTitleStartsWith("Spring");
        Assertions.assertThat(all.size()).isEqualTo(1);
        all.forEach(p-> System.out.println("p :getTitle : " +p.getTitle()));
    }
    private void savePost() {
        Post post =new Post();
        post.setTitle("Spring Data Jpa");
        postRepository.save(post);  //persist
    }
    @Test
    public void findByTitleTest(){
        savePost();
        List<Post> all =postRepository.findByTitle("Spring");
        Assertions.assertThat(all.size()).isEqualTo(1);
        all.forEach(p-> System.out.println("findByTitleTest => : " +p.getTitle()));
    }
34.Sort
강의 :
이전과 마찬가지로 Pageable이나 Sort를 매개변수로 사용할 수 있는데, @Query와 같이 사용할 때 제약 사항이 하나 있습니다.
Order by 절에서 함수를 호출하는 경우에는 Sort를 사용하지 못합니다. 그 경우에는 JpaSort.unsafe()를 사용 해야 합니다.
- Sort는 그 안에서 사용한 프로퍼티 또는 alias가 엔티티에 없는 경우에는 예외가 발생합니다. 
- JpaSort.unsafe()를 사용하면 함수 호출을 할 수 있습니다. - JpaSort.unsafe(“LENGTH(firstname)”); 
 
package com.example.demojap3.post;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface PostRepository extends JpaRepository<Post, Long> {
     List<Post> findByTitleStartsWith(String title);
     @Query("SELECT p  FROM Post AS p WHERE p.title =:title")
     List<Post> findByTitle(@Param("title") String title, Sort sort);
}
  @Test
    public void findByTitleTest(){
        savePost();
        List<Post> all =postRepository.findByTitle("Spring" , Sort.by("title"));
        System.out.println("all.size() = " + all.size());
        all.forEach(p-> System.out.println("findByTitleTest => : " +p.getTitle()));
    }
    /**
     * 함수를 이용한 길이 정렬
     */
    @Test
    public void findByTitleTest2(){
        savePost();
        List<Post> all =postRepository.findByTitle("Spring" , JpaSort.unsafe("LENGTH(title)"));
        System.out.println("2.all.size() = " + all.size());
        all.forEach(p-> System.out.println("2.findByTitleTest => : " +p.getTitle()));
    }
35.Named Parameter와 SpEL
강의 :
Named Parameter
- @Query에서 참조하는 매개변수를 ?1, ?2 이렇게 채번으로 참조하는게 아니라 이름으로 :title 이렇게 참조하는 방법은 다음과 같습니다. 
@Query("SELECT p FROM Post AS p WHERE p.title = :title")
List<Post> findByTitle(@Param("title") String title, Sort sort);
SpEL
- 스프링 표현 언어 
- https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions 
- @Query에서 엔티티 이름을 #{#entityName} 으로 표현할 수 있습니다. 
@Query("SELECT p FROM #{#entityName} AS p WHERE p.title = :title")
List<Post> findByTitle(@Param("title") String title, Sort sort);
36.Update 쿼리
강의 :
쿼리 생성하기
- find... 
- count... 
- delete... 
- 흠.. update는 어떻게 하지? 
Update 또는 Delete 쿼리 직접 정의하기
- @Modifying @Query 
- 추천하진 않습니다. 
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE Post p SET p.title = ?2 WHERE p.id = ?1")
int updateTitle(Long id, String title);
     @Modifying(clearAutomatically = true)
     @Query("UPDATE Post p  Set p.title =:title WHERE p.id =:id")
     int updateTitle(@Param("title") String title, @Param("id") Long id);
테스트
    private Post savePost() {
        Post post =new Post();
        post.setTitle("Spring Data Jpa");
        return postRepository.save(post);  //persist
    }
    @Test
    public void updateTitle(){
        Post spring  =savePost();
        String hibernate="hibernate";
        int update=postRepository.updateTitle("hibernate", spring.getId());
        Assertions.assertThat(update).isEqualTo(update);
        Optional<Post> byId = postRepository.findById(spring.getId());
        Assertions.assertThat(byId.get().getTitle()).isEqualTo(hibernate);
    }
    /**
     *
     * ==>
     * 업데이트 는 다음과 같이 더티 체킹
     */
    @Test
    public void updateTitle2(){
        Post spring  =savePost();
        spring.setTitle("hibernate");
        List<Post> all=postRepository.findAll();
        Assertions.assertThat(all.get(0).getTitle()).isEqualTo("hibernate");
    }
37.EntityGraph
강의 :
쿼리 메소드 마다 연관 관계의 Fetch 모드를 설정 할 수 있습니다.
@NamedEntityGraph
- @Entity에서 재사용할 여러 엔티티 그룹을 정의할 때 사용. 
@EntityGraph
- @NamedEntityGraph에 정의되어 있는 엔티티 그룹을 사용 함. 
- 그래프 타입 설정 가능 - (기본값) FETCH: 설정한 엔티티 애트리뷰트는 EAGER 패치 나머지는 LAZY 패치. 
- LOAD: 설정한 엔티티 애트리뷰트는 EAGER 패치 나머지는 기본 패치 전략 따름. 
 
=> EntityGraph 는 다음을 참조해서 볼것
https://macaronics.net/index.php/m01/spring/view/2079
강의 :
https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-JPA-실전&unitId=28019&tab=curriculum
#spring.jpa.properties.hibernate.default_batch_fetch_size=100
batch_fetch 를 사용하면 지연로딩에서 entityGraph 인 페치 조인을 사용안하면 in 으로 데이터를 한번에 가져온다.
앞 강의 확인 할것.
연관된 엔티티들을 SQL 한번에 조회하는 방법
member team은 지연로딩 관계이다. 따라서 다음과 같이 team의 데이터를 조회할 때 마다 쿼리가
실행된다. (N+1 문제 발생)
@Test
public void findMemberLazy() throws Exception {
    //given
    //member1 -> teamA
    //member2 -> teamB
    Team teamA = new Team("teamA");
    Team teamB = new Team("teamB");
    teamRepository.save(teamA);
    teamRepository.save(teamB);
    memberRepository.save(new Member("member1", 10, teamA));
    memberRepository.save(new Member("member2", 20, teamB));
    em.flush();
    em.clear();
    //when
    List<Member> members = memberRepository.findAll();
    //then
    for (Member member : members) {
 
        System.out.println("시작============================================================");
        member.getTeam().getName();
        // 다음과 같이 지연 로딩 여부를 확인할 수 있다
        //Hibernate 기능으로 확인
        Hibernate.initialize(member.getTeam());
 
        //JPA 표준 방법으로 확인
        PersistenceUnitUtil util =  em.getEntityManagerFactory().getPersistenceUnitUtil();
        System.out.println( "isLoaded : " + util.isLoaded(member.getTeam()) );
        System.out.println("끝============================================================");
    }
}
참고: 다음과 같이 지연 로딩 여부를 확인할 수 있다.
//Hibernate 기능으로 확인
Hibernate.isInitialized(member.getTeam())
//JPA 표준 방법으로 확인
PersistenceUnitUtil util =
    em.getEntityManagerFactory().getPersistenceUnitUtil();
util.isLoaded(member.getTeam());
연관된 엔티티를 한번에 조회하려면 페치 조인이 필요하다.
JPQL 페치 조인
@Query("select m from Member m left join fetch m.team")
List <Member> findMemberFetchJoin();
스프링 데이터 JPA는 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> findEntityGraphByUsername(String username);
}
EntityGraph 정리 사실상 페치 조인(FETCH JOIN)의 간편 버전 LEFT OUTER JOIN 사용
NamedEntityGraph 사용 방법
@EntityGraph Member.all 은 @NamedEntityGraph 호출 하며 이것은 FETCH JOIN 으로 설정되어 진다.
@NamedEntityGraph(name = "Member.all", attributeNodes =
    @NamedAttributeNode("team"))
@Entity
public class Member {}
@EntityGraph("Member.all")
@Query("select m from Member m")
List <Member> findMemberEntityGraph();
★★★ 38.스프링 데이터 JPA: Projection ★★★
강의 :
엔티티의 일부 데이터만 가져오기.
인터페이스 기반 프로젝션
- Nested 프로젝션 가능. 
- Closed 프로젝션 - 쿼리를 최적화 할 수 있다. 가져오려는 애트리뷰트가 뭔지 알고 있으니까. 
- Java 8의 디폴트 메소드를 사용해서 연산을 할 수 있다. 
 
- Open 프로젝션 - @Value(SpEL)을 사용해서 연산을 할 수 있다. 스프링 빈의 메소드도 호출 가능. 
- 쿼리 최적화를 할 수 없다. SpEL을 엔티티 대상으로 사용하기 때문에. 
 
클래스 기반 프로젝션
- DTO 
- 롬복 @Value로 코드 줄일 수 있음 
다이나믹 프로젝션
- 프로젝션 용 메소드 하나만 정의하고 실제 프로젝션 타입은 타입 인자로 전달하기. 
<T> List<T> findByPost_Id(Long id, Class<T> type);

Post
package com.example.demojap3.post;
import com.example.demojap3.comment.Comment;
import jakarta.persistence.*;
import lombok.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Entity
@Data
@ToString(of={"title", "content"})
@NoArgsConstructor(access = AccessLevel.PUBLIC)
//@NamedQuery(name="Post.findByTitle", query="SELECT p  FROM Post AS p WHERE p.title =:title")
public class Post {
    @Id
    @GeneratedValue
    @Column(name = "post_id")
    private Long id;
    private String title;
    @Lob
    private String content;
    @Temporal(TemporalType.TIMESTAMP)
    private Date created;
    public Post(String content) {
        this.content = content;
    }
    @OneToMany(mappedBy = "post")
    public List<Comment> commentList=new ArrayList<>();
}
Comment
package com.example.demojap3.comment;
import com.example.demojap3.post.Post;
import jakarta.persistence.*;
import lombok.Data;
import lombok.Getter;
@Entity
@Data
//@NamedEntityGraph(name = "Comment.post",
//    attributeNodes = @NamedAttributeNode("post")
//)
public class Comment {
    @Id
    @GeneratedValue
    @Column(name = "comment_id")
    private Long id;
    private String comment;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;
    private int up;
    private int down;
    private boolean best;
    
}
CommentRepository
 
package com.example.demojap3.comment;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
public interface CommentRepository extends JpaRepository<Comment, Long> {
//    @EntityGraph(value = "Comment", type = EntityGraph.EntityGraphType.LOAD)
//    Optional<Comment> loadCommentById(Long id);
     List<CommentSummary2> findByPost_Id(Long id);
     /**
      * ==> 다음과 같이 제네릭으로 변경
      */
     <T>List<T> findByPost_Id(Long id, Class<T> type);
}
CommentSummary (인터페이스 방식 )
package com.example.demojap3.comment;
public interface CommentSummary {
    String getComment();
    int getUp();
    int getDown();
    /**
     * Open Projection 방법
     * @return
     @Value("#{target.up + ' ' +target.down}")
     String getVotes();
     Hibernate:
     select
         c1_0.comment_id,
         c1_0.best,
         c1_0.comment,
         c1_0.down,
         c1_0.post_id,
         c1_0.up
     from
         comment c1_0
     where
        c1_0.post_id=?
     호출시
     ====================================
        10 1
     ====================================
     =====> 아래 같은 메서드 방법으로 커스텀화 되어 최적화된 쿼리로 출력 가능
     */
    default String getVotes(){
        return getUp() + " " +getDown();
    }
    /** ★ ★ ★최적화 되어 쿼리 원하는 것만 출력
     *     select
     *         c1_0.comment,
     *         c1_0.up,
     *         c1_0.down
     *     from
     *         comment c1_0
     *     where
     *         c1_0.post_id=?
     */
}
CommentSummary2 (클래스 방식)
package com.example.demojap3.comment;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
 * 클래스방식 커스텀
 */
@Data
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class CommentSummary2 {
    private  String comment;
    private int up;
    private int down;
    public String getVotes(){
        return getUp() + " : " +getDown();
    }
}
CommentOnly (클래스 방식)
package com.example.demojap3.comment;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class CommentOnly {
    private String comment;
}
테스트
package com.example.demojap3.comment;
import com.example.demojap3.post.Post;
import com.example.demojap3.post.PostRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@SpringBootTest
@Transactional
class CommentRepositoryTest {
    @Autowired
    CommentRepository commentRepository;
    @Autowired
    PostRepository postRepository;
    @Test
    public void getCommentTest1(){
        savePost();
        Optional<Comment> byId = commentRepository.findById(1l);
        System.out.println("제목 = ? " +byId.get().getPost().getTitle());
    }
    private Comment savePost() {
        Post post = new Post();
        post.setTitle("jpa");
        Post savePost=postRepository.save(post);
        Comment comment=new Comment();
        comment.setComment("comment- hello ");
        comment.setPost(savePost);
        comment.setUp(10);
        comment.setDown(1);
        return commentRepository.save(comment);
    }
    @Test
    public void getCommentTest2(){
        savePost();
        commentRepository.findByPost_Id(1l).forEach(c->{
            System.out.println("====================================");
              System.out.println(c.getVotes());
            System.out.println("====================================");
        });
    }
    @Test
    public void getCommentOnelyTest(){
        savePost();
        commentRepository.findByPost_Id(1l, CommentOnly.class).forEach(c->{
            System.out.println("====================================");
            System.out.println(c.getComment());
            System.out.println("====================================");
        });
    }
}
Hibernate: 
    select
        c1_0.comment_id,
        c1_0.best,
        c1_0.comment,
        c1_0.down,
        c1_0.post_id,
        c1_0.up 
    from
        comment c1_0 
    where
        c1_0.post_id=?
★인터페이스 페이스 값으로 가져옴
★ 원하는 컬럼만 가져오며 query 가 최적화 됨
    select
        c1_0.comment,
        c1_0.up,
        c1_0.down 
    from
        comment c1_0 
    where
        c1_0.post_id=?
====================================
10 1
====================================
결론
1) CommentSummary2 와 같은 클래스 방식에
2) 아래처럼 제네릭 메소드 추천
     /**
      * ==> 다음과 같이 제네릭으로 변경
      */
     <T>List<T> findByPost_Id(Long id, Class<T> type);
★ 반드시 참조해서 볼것 : =>
링크클릭
★32.동적 쿼리와 성능 최적화 조회 - Where절 파라미터 사용
예)
package study.querydsl.dto;
 
import com.querydsl.core.annotations.QueryProjection;
import lombok.Data;
 
@Data
public class MemberTeamDto {
 
    private Long memberId;
    private String username;
    private int age;
    private Long teamId;
    private String teamName;
 
    @QueryProjection
    public MemberTeamDto(Long memberId, String username, int age, Long teamId, String teamName) {
        this.memberId = memberId;
        this.username = username;
        this.age = age;
        this.teamId = teamId;
        this.teamName = teamName;
    }
 
}
public List<MemberTeamDto> search(MemberSearchCondition condition){
    return queryFactory
            .select(new QMemberTeamDto(
                    member.id.as("memberId"),
                    member.username,
                    member.age,
                    team.id.as("teamId"),
                    team.name.as("teamName")
            ))
            .from(member)
            .leftJoin(member.team, team)
            .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoeEq(condition.getAgeGoe()),
                    ageLoeEq(condition.getAgeLoe())
            )
            .fetch();
}
 
private BooleanExpression usernameEq(String username) {
    return hasText(username) ?  member.username.eq(username) :null;
}
 
private BooleanExpression teamNameEq(String teamName) {
    return hasText(teamName) ? team.name.eq(teamName) :null;
}
 
private BooleanExpression ageGoeEq(Integer ageGoe) {
    return ageGoe!=null? member.age.goe(ageGoe) : null;
}
 
private BooleanExpression ageLoeEq(Integer ageLoe) {
    return ageLoe!=null? member.age.loe(ageLoe) : nu
39.Specifications (99%사용안함)
강의 :
https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13782&tab=curriculum
에릭 에반스의 책 DDD에서 언급하는 Specification 개념을 차용 한 것으로 QueryDSL의 Predicate와 비슷합니다.
설정 하는 방법
- https://docs.jboss.org/hibernate/stable/jpamodelgen/reference/en-US/html_single/ 
- 의존성 설정 
- 플러그인 설정 
- IDE에 애노테이션 처리기 설정 
- 코딩 시작 
    <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-jpamodelgen</artifactId>
        </dependency>
    <plugin>
                <groupId>org.bsc.maven</groupId>
                <artifactId>maven-processor-plugin</artifactId>
                <version>2.0.5</version>
                <executions>
                    <execution>
                        <id>process</id>
                        <goals>
                            <goal>process</goal>
                        </goals>
                        <phase>generate-sources</phase>
                        <configuration>
                            <processors>
                                <processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor>
                            </processors>
                        </configuration>
                    </execution>
                </executions>
                <dependencies>
                    <dependency>
                        <groupId>org.hibernate</groupId>
                        <artifactId>hibernate-jpamodelgen</artifactId>
                        <version>${hibernate.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor
public interface CommentRepository extends JpaRepository<Comment, Long>, JpaSpecificationExecutor<Comment> {
}
참조 :
https://macaronics.net/index.php/m01/spring/view/2082
Specifications (명세) 는
1) 코드를 해독하는데 있어서 어려움이 많아 쿼리가 조금이라도 복잡해지면 사용하기가 어렵다.
2) 생성해야 하는 Predicate 가 많아진다면 관리하기 어려워지고 직관적으로 이해하기 힘들다는 단점이 발생.
3) JPA Specification 와 Criteria 는 99% 사용 안하며, 대신에 QueryDSL을 사용한다.
40.Query by Example(99%사용안함)
강의 :
https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13783&tab=curriculum
QBE는 필드 이름을 작성할 필요 없이(뻥) 단순한 인터페이스를 통해 동적으로 쿼리를 만드는 기능을 제공하는 사용자 친화적인 쿼리 기술입니다. (감이 1도 안잡히는거 이해합니다.. 코드를 봐야 이해하실꺼에요.)
Example = Probe + ExampleMatcher
- Probe는 필드에 어떤 값들을 가지고 있는 도메인 객체. 
- ExampleMatcher는 Prove에 들어있는 그 필드의 값들을 어떻게 쿼리할 데이터와 비교할지 정의한 것. 
- Example은 그 둘을 하나로 합친 것. 이걸로 쿼리를 함. 
장점
- 별다른 코드 생성기나 애노테이션 처리기 필요 없음. 
- 도메인 객체 리팩토링 해도 기존 쿼리가 깨질 걱정하지 않아도 됨.(뻥) 
- 데이터 기술에 독립적인 API 
단점
- nested 또는 프로퍼티 그룹 제약 조건을 못 만든다. 
- 조건이 제한적이다. 문자열은 starts/contains/ends/regex 가 가능하고 그밖에 property는 값이 정확히 일치해야 한다. 
QueryByExampleExecutor
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example
41.트랜잭션
강의 :
https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13784&tab=curriculum
스프링 데이터 JPA가 제공하는 Repository의 모든 메소드에는 기본적으로 @Transaction이 적용되어 있습니다.
스프링 @Transactional
- 클래스, 인터페이스, 메소드에 사용할 수 있으며, 메소드에 가장 가까운 애노테이션이 우선 순위가 높다. 
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html (반드시 읽어볼 것, 그래야 뭘 설정해서 쓸 수 있는지 알죠..) 
JPA 구현체로 Hibernate를 사용할 때 트랜잭션을 readOnly를 사용하면 좋은 점
- Flush 모드를 NEVER로 설정하여, Dirty checking을 하지 않도록 한다. 
42.Auditing
강의 :
https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13785&tab=curriculum
스프링 데이터 JPA의 Auditing
 @CreatedDate
    private Date created;
    @LastModifiedDate
    private Date updated;
    @CreatedBy
    @ManyToOne
    private Account createdBy;
    @LastModifiedBy
    @ManyToOne
    private Account updatedBy;
엔티티의 변경 시점에 언제, 누가 변경했는지에 대한 정보를 기록하는 기능.
아쉽지만 이 기능은 스프링 부트가 자동 설정 해주지 않습니다.
- 메인 애플리케이션 위에 @EnableJpaAuditing 추가 
- 엔티티 클래스 위에 @EntityListeners(AuditingEntityListener.class) 추가 
- AuditorAware 구현체 만들기 
- @EnableJpaAuditing에 AuditorAware 빈 이름 설정하기. 
JPA의 라이프 사이클 이벤트
- https://docs.jboss.org/hibernate/orm/4.0/hem/en-US/html/listeners.html 
- @PrePersist 
- @PreUpdate 
- ... 
Account
package com.example.demojap3.account;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Data;
@Entity
@Data
public class Account {
    @Id
    @GeneratedValue
    private Long id;
    private String username;
    private String firstName;
    private String lastName;
}
AccountAuditAware
package com.example.demojap3.account;
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class AccountAuditAware implements AuditorAware<Account> {
    @Override
    public Optional<Account> getCurrentAuditor() {
        System.out.println("\n\n\n********** looking for current user\n\n\n");
        return Optional.empty();
    }
}
Comment
package com.example.demojap3.comment;
import com.example.demojap3.account.Account;
import com.example.demojap3.post.Post;
import jakarta.persistence.*;
import jakarta.persistence.Id;
import lombok.Data;
import lombok.Getter;
import org.springframework.data.annotation.*;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Entity
@Data
//@NamedEntityGraph(name = "Comment.post",
//    attributeNodes = @NamedAttributeNode("post")
//)
@EntityListeners(AuditingEntityListener.class)
public class Comment {
    @Id
    @GeneratedValue
    @Column(name = "comment_id")
    private Long id;
    private String comment;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;
    private int up;
    private int down;
    private boolean best;
    @CreatedBy
    @ManyToOne(fetch = FetchType.LAZY)
    private Account createBy;
    @LastModifiedBy
    @ManyToOne(fetch = FetchType.LAZY)
    private Account updateBy;
    @CreatedDate
    private LocalDateTime created;
    @LastModifiedDate
    private LocalDateTime updated;
    @PrePersist
    public void prePersist(){
        System.out.println("Pre Persist is called");
    }
}
Application
package com.example.demojap3;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication
@EnableJpaRepositories(repositoryBaseClass =SimpleMyRepository.class)
@EnableJpaAuditing(auditorAwareRef = "accountAuditAware")//빈 이름으로 설정해야 한다.
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}
* Auditing 사용하려면
1.Application 에 @EnableJpaAuditing 추가 *
2.Auditing 을 사용할 Entity 에서도 @EntityListeners(AuditingEntityListener.class) 추가
3.@EnableJpaAuditing(auditorAwareRef = "accountAuditAware")//빈 이름으로 설정해야 한다.
테스트
  /**
     * Auditing 사용하려면
     * 1.Application  에 @EnableJpaAuditing  추가
     * 2.Auditing 을 사용할 Entity 에서도 @EntityListeners(AuditingEntityListener.class) 추가
     * 3.@EnableJpaAuditing(auditorAwareRef = "accountAuditAware")//빈 이름으로 설정해야 한다.
     */
    @Test
    //@Rollback(value = false)
    public void auditingTest(){
        savePost();
    }
다음을 참조할것
https://macaronics.net/index.php/m01/spring/view/2081
Spring Boot + JPA + Audit Listener
Spring Boot는 이미 이 솔루션을 제공하고 있습니다.
0단계 — pom.xml 에 JPA가 있는지 확인
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
1단계 - Auditor.java 생성
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Optional;
public class Auditor implements AuditorAware<String> {
    @Override
    public Optional<String> getCurrentAuditor() {
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();
        return authentication != null ? Optional.of((String) authentication.getPrincipal()) : Optional.of("0");
    }
}
사용하는 Principal 개체 유형에 따라 다릅니다.
이 예에서는 문자열을 사용하여 사용자 ID를 Auditor 으로 나타냅니다.
2단계 - Spring Boot 애플리케이션 클래스에서 @EnableJpaAuditing을 추가하고 Audit Bean을 정의합니다 .
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "auditor")
public class MyAwesomeApplication {
    ...
    @Bean
    public AuditorAware<String> auditor() {
        return new Auditor();
    }
    ...
}
3단계 — 기본 엔터티에 @EntityListeners 및 @MappedSuperclass 추가
import lombok.Data;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.time.Instant;
@Data
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity {
    ...
    @CreatedBy
    @Column(length = 36)
    private String createdBy;
    @CreatedDate
    private Instant created;
    @LastModifiedBy
    @Column(length = 36)
    private String updatedBy;
    @LastModifiedDate
    private Instant updated;
    ...
}
4단계 - BaseEntity 확장
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
@EqualsAndHashCode(callSuper = true)
@Data
@Entity(name = "m_user")
public class User extends BaseEntity {
    @Column(length = 60, nullable = false, unique = true)
    private String username;
}
43.JPA: 마무리
강의 :
https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13786&tab=curriculum
이번 강좌가 여러분이 앞으로 스프링 데이터 JPA를 사용하고 학습하시는데 도움이 되었길 바랍니다.
감사합니다.
 
									
















 
댓글 ( 4)  
댓글 남기기