중급자를 위해 준비한
[웹 개발, 백엔드] 강의입니다.
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)
댓글 남기기