스프링

 

 

중급자를 위해 준비한
[웹 개발, 백엔드] 강의입니다.

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

 

 

 

  1. 강좌 소개

 

Application -> 스프링 데이터 JPA (-> JPA -> JDBC) -> Database

 

 

 

  1. 강사 소개

 

백기선

 

마이크로소프트(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()

EntityManager.merge()

 

 

영속성이 적용된  항상 반환된 값을 이용 해라

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://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13776&category=questionDetail&tab=curriculum

 

 

 

쿼리 생성하기

 

쿼리 찾아쓰기

  • 엔티티에 정의한 쿼리 찾아 사용하기 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

 

 

강의 :

https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13777&category=questionDetail&tab=curriculum

 

이전과 마찬가지로 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

 

 

강의 :

https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13778&category=questionDetail&tab=curriculum

 

 

 

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

 

    @Query("SELECT p FROM #{#entityName} AS p WHERE p.title = :title")

    List<Post> findByTitle(@Param("title") String title, Sort sort);

 

 

 

 

 

 

 

 

 

 

36.Update 쿼리

 

 

강의 :

https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13779&category=questionDetail&tab=curriculum

 

 

 

 

쿼리 생성하기

  • 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

 

 

강의 :

https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13780&category=questionDetail&tab=curriculum

 

 

 

쿼리 메소드 마다 연관 관계의 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  ★★★

 

 

강의 :

https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13781&category=questionDetail&tab=curriculum

 

 

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

 

인터페이스 기반 프로젝션

  • 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와 비슷합니다.

 

설정 하는 방법

 

 

    <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

 

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;

 

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

 

아쉽지만 이 기능은 스프링 부트가 자동 설정 해주지 않습니다.

  1. 메인 애플리케이션 위에 @EnableJpaAuditing 추가

  2. 엔티티 클래스 위에 @EntityListeners(AuditingEntityListener.class) 추가

  3. AuditorAware 구현체 만들기

  4. @EnableJpaAuditing에 AuditorAware 빈 이름 설정하기.

 

JPA의 라이프 사이클 이벤트

 

 

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를 사용하고 학습하시는데 도움이 되었길 바랍니다.

감사합니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

Don't judge of a man by his looks. (외양으로 사람을 판단하지 말라.)

댓글 ( 4)

댓글 남기기

작성