JPA Entity를 대상으로 먼가 지지고 볶는 것.
- 1. 프로젝트 환경설정
- IntelliJ Settings
- BuildTools > Gradle > change Build+Test : IntelliJ
- Compiler > Annotation Processors > Enable
- 프로젝트 생성
- start.spring.io + Gradle + Java + ADD web, jpa, h2, lombok
- 라이브러리 살펴보기
- dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
} - Transparent Dependancy : 추가적인 연관 dependancy
- HikariCP : spring boot 2.x 대 부터 DB Connection pool 을 Hikari로 사용
- 매우 빠름. 실무에서 성능 극한을 다루려면 메뉴얼 봐야 함.
- AssertJ : 기존 대비 chainning 방식으로 짤 수 있게 개선된 라이브러리.
- 버전 명기가 안 된 것 : spring boot가 맞는 버전을 사전에 세팅 해놔서 생략한 것.
- gradle download 된 것 확인하여, 관련 클라이언트 설치하면 됨(h2...)
- 핵심 라이브러리
- 스프링 MVC
- 스프링 ORM
- JPA, 하이버네이트
- 스프링 데이터 JPA
- 기타 라이브러리
- H2 데이터베이스 클라이언트
- 커넥션 풀: 부트 기본은 HikariCP
- 로깅 SLF4J & LogBack
- 테스트
- dependencies {
- H2 데이터베이스 설치
- https://www.h2database.com
- 파일로 접근하기 : jdbc:h2:~/datajpa (첫 한번만, 파일로 접근하면 lock 생겨서 곤란해짐)
- 원격접근 : jdbc:h2:tcp://localhost/~/datajpa
- 스프링 데이터 JPA와 DB 설정, 동작확인
- profile 파일 변경하기
- application.properties(삭제) -> application.yml 생성
- 아래처럼 spring, jpa, logging 세가지 설정 넣어준다.
- ddl-auto: create
- application loading시점에 table 모두 drop 후 새로 생성하는 옵션
- application 내려가도 table 남아있음
- 공부할 때 좋은 옵션.
- 잘못적으면 에러남 : https://www.inflearn.com/questions/192360/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%98%A4%EB%A5%98
- hibernate:
- show_sql = 콘솔에 찍기
- format_sql = 쿼리를 이쁘게 틀 잡아서 출력
- logging level : debug = log 파일로 sql 남기기
- org.hibernate.type: trace = binding 된 파라미터 까지 볼 수 있는 옵션
- Entity 생성(JPA 필수)
- @Entity, @Getter
- field 추가
- Repository 생성
- @Repository
- @PersistenceContext 추가(EntityManager)
- CRUD 메소드 추가
- Spring Data Repository 생성을 위해선 extends JpaRepository 선언
- Test 생성 : cmd(ctrl) + shift + t
- JPA는 entity의 default 생성자(parameter 없는)가 필요하다.
- protected level로 : JPA proxy 방법 접근을 위해
- protected void Member(){}
- 상단에 @NoArgsConstructor(access = AccessLevel.PROTECTED) 선언으로 대체 할 수 있다.
- assertThat().isEqualTo : == 연산과 동일
- 동일 transaction 안에선 동일성 보장된다.(== 1차 caching)
- 공부를 위해 query parameter 보는 설정(현업에서 쓰지 말 것)
- gradle dependancy에 implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.7' 추가
- protected level로 : JPA proxy 방법 접근을 위해
- JPA는 entity의 default 생성자(parameter 없는)가 필요하다.
- profile 파일 변경하기
- IntelliJ Settings
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/datajpa
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
# show_sql: true
format_sql: true
logging.level:
org.hibernate.SQL: debug
# org.hibernate.type: trace
- 2. 예제 도메인 모델(Member : Team = n:1)
- 예제 도메인 모델과 동작확인
- DB에서는 foreign key가 1:다 에서 다에 들어가야 한다.
- @Column(name = "member_id")
- 실무style : 관리상 "{table명}_id" 로 쓴다. join할 때 편하다.
- 연관 column field 만들기 전에 해당 Entity class가 먼저 생성되어 있어야 한다(편함)
- 관계선언
- @ManyToOne
@JoinColumn(name = "team_id")
private Team team; //foreign key 설정. 단일 객체임을 주의깊게 보자.- ManyToOne의 fetch 기본 타입이 EAGER다. LAZY로 직접 선언해주자
- EAGER는 성능 최적화 어렵다.
- member조회 끝 -> team 조회 에서 member+team 조회 iteration이 된다.
- LAZY : 해당 Entity 호출할 그 시점에 쿼리
- ManyToOne의 fetch 기본 타입이 EAGER다. LAZY로 직접 선언해주자
- @OneToMany
private List<Member> members = new ArrayList<>(); //List임을 주의 - 이렇게 양쪽 설정하는 건, mapped by 설정 필요함. FK 없는 쪽에 거는 것이 좋다!
- @ManyToOne
- @ToString(of = {"id", "username", "age"}) : 객체 바로 찍을 때 출력 되는 것. 선언한 field 타고가서 출력을 해주며, 연관관계 field는 무한loop 유발 할 수 있으므로 적지 말 것.
- 연관관계 세팅 메소드(Member Class에)
- public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}- 팀이 변경 되면, team의 members도 같이 수정. 이걸로 생성자에서 team 세팅을 한다?
- public void changeTeam(Team team) {
- EntityManger.flush() : 강제로 DB에 모아놓은 쿼리 날림
- EntityManager.clear() : JPA persistence context에 있는 cache clear
- 예제 도메인 모델과 동작확인
- 3. 공통 인터페이스 기능
- 순수 JPA 기반 리포지토리 만들기
- JPQL : select m from Member m, 객체를 대상으로 하는 쿼리(Table X)
- JPQL -> SQL 번역 -> DB에서 반환
- JPA는 update가 없다.
- collection과 동일한 형태로 다룸. entity 수정하고 commit하면 자동으로 인지해서 update query날림
- = dirty checking
- findAll()등 CRUD 메소드 반복 => 해결해보자.. generic... -> Interface로 해결(오픈소스) -> Spring Data JPA로 발전.
- JPQL : select m from Member m, 객체를 대상으로 하는 쿼리(Table X)
- 공통 인터페이스(JpaRepository) 설정
- implements가 없는데 어떻게 사용 되나
- Injection 주입 받는 것 print 찍어보면, com.sun.proxy.$XXX : Spring Data JPA가 implements class를 알아서 만들어 주입.
- @Repository annotation이 없다?
- JpaRepository Extends 한 건 없어도 된다.
- extends JpaRepository<{EntityType},{id type}> 형식으로 표기한다.
- 여기서 id란 model의 id 이다.(the type of the id of the entity the repository manages)
- implements가 없는데 어떻게 사용 되나
- 공통 인터페이스(JpaRepository) 적용
- TestCase를 하나 만들어서, @Autowired로 interface Repository에 주입을 시키고, 돌려본다.
- 잘돌아가면, 어디선가 implements가 잘 만들어졌다는 뜻이고, Spring Data JPA가 잘 넣어 준 것이다.
- TestCase를 하나 만들어서, @Autowired로 interface Repository에 주입을 시키고, 돌려본다.
- 공통 인터페이스 분석(개발자가 상상할 수 있는 모든 기능 제공)
- spring-data-jpa
- JpaRepository
- org.springframework.data.jpa.repository, spring-data-jpa
- Package 경로를 보면, Spring data의 일부로, jpa 특화의 느낌을 알 수 있다.
- JpaRepository
- spring-data-commons : Mongo, Redis, JPA 등 공통으로 다 쓰는 것.
- PagingAndSortingRepository
- org.springframework.data.repository, spring-data-commons
- CrudRepository
- org.springframework.data.repository, spring-data-commons
- Repository : marker interface. classpath scanning for easy Spring bean creation. bean 스캔용.
- PagingAndSortingRepository
- T findOne(Id) -> Optional<T> findById(Id) 로 변경됨 주의
- T = 엔티티, Id = 엔티티의 식별자 타입, S = 엔티티와 그 자식 타입
- 주요메서드
- Save(S) : save & merge.
- delete(T) : delete 1 entity. EntityManager.remove()
- findById(ID) : get 1 entity. EntityManager.find()
- getOne(ID) : entity를 proxy로 조회 -> reference 반환(가짜 프록시 개체). EntityManager.getReference()
- proxy를 touch해서 실제로 값을 꺼낼 때, query가 날라가서 값을 가지고 옮(프록시가 초기화 된다?)
- findAll(_) : Sort, Paging 조건을 제공. 쿼리나갈때 sort 해주고, page 쿼리도 나가고.
- 도메인 특화 기능 개발해야한다면??
- findByUserName? => implements 구현 필요없다
- 쿼리메소드 기능 사용
- findByUserName? => implements 구현 필요없다
- spring-data-jpa
- 순수 JPA 기반 리포지토리 만들기
- IntelliJ Tip) 이전 것 왔다갔다 하기 : Cmd+E+Enter
- 4. 쿼리 메소드 기능
- 쿼리를 어떤식으로 해결할지 제공하는 강력한 3가지 기능
- 메소드 이름으로 쿼리 생성
- 관례를 가지고 String분석하여 생성
- 이름, 즉 문법이 중요!. 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경
- ex) List<Member> findByUsernameAndAgeGreaterThan(String username, int age)
- UsernameAndAge :Username=Username, And 조건, AgeGreaterThan= Age> ?
- Spring Data JPA - Reference Documentation
- Spring Data JPA - Reference Documentation
- Parameter 최대 2개정도가 적당하며, 짤막한 쿼리 필요할 때 아주 유용하다.
- 관례를 가지고 String분석하여 생성
- JPA NamedQuery
- 선언 : Entity에 @NamedQuery(name="Entity.methodName", query="{JPQL}") 추가
- 쿼리에 이름을 부여하고 호출하는 기능
- repository에 em.createQuery("Entity.methodName", Member.class)..... 혹은
- @Query(name = "Entity.methodName") List<Member> methodName(@Param() String "");
- 혹은 @Query도 생략 가능 => Entity.methodName으로 작성하면 알아서 찾음.
- 우선순위 Named query > Method query
- 실무에서 거의 안 씀. 장점 : app loading 시점에 파싱해서 문법 오류 알려줌
- @Query, 리포지토리 메소드에 쿼리 정의하기** : 실무 활용형태
- @ Query("select m from Member m where m.username = : ~!@#!)
- 인터페이스 메소드위에 바로 annotation으로 선언
- 복잡한 쿼리 핸들링
- app loading 시점에 파싱해서 문법 오류 알려줌(정적쿼리)
- 동적쿼리는 queryDSL 권장(깔끔 + 유지보수성)
- 메소드 이름으로 쿼리 생성
- @Query, 값, DTO로 조회하기 : 단순히 값 하나를 조회
- 값 조회 : 상단에 @Query 및 method 선언
- DTO 조회
- @Data는 Getter Setter 포함이라 Entity에는 웬만하면 선언 하지 말 것
- @Query("select new study.datajpa.dto.MemberDto{m.id, m.username, t.name} from Member m join m.team t")
- 생성자에 매칭하여 new Package경로{column} 적는 것이 포인트 == 생성 후 반환해주는 개념
- new operation이라고 함.
- 쿼리를 어떤식으로 해결할지 제공하는 강력한 3가지 기능
- IntelliJ Tip) 코드블럭 위아래 옮기기 사용 용도 : 코드 선행관계 조정할 때
- 파라미터 바인딩(이름기반)
- 위치기반
- select m from Member m where m.username = ?0 //위치 기반. 거의 안씀
- 이름기반
- select m from Member m where m.username = :name //이름 기반(권장)
- 컬렉션 파라미터 바인딩 : 인자를 이쁘게 만들어 줌. 괄호 쉼표 등등...
- @Query("select m from Member m where m.username in :names")
- in :names
- -> from member where username in(?,?) 형태로 변환
- 위치기반
- 반환 타입 : 알아서 잘 반환. 유연하게 사용가능
- List<Member> findByUsername(String name); //컬렉션
Member findByUsername(String name); //단건
Optional<Member> findByUsername(String name); //단건 Optional - collection
- 없다면 empty collection return
- 단 건
- 메서드를 호출하면
- 순수 JPA : 내부에서 JPQL의 Query.getSingleResult() 메서드를 호출.조회 결과가 없으면 javax.persis tence.NoResultException 예외가 발생
- 스프링 Data JPA : 단건을 조회할 때 NoResultException 발생하면 무시하고 null 을 반환한다.
- JPA와 동작 방식이 다른 사례.
- 옵셔널로 받을 것.
- 메서드를 호출하면
- 그 외, 공식 문서 참고 : Spring Data JPA - Reference Documentation
- List<Member> findByUsername(String name); //컬렉션
- 순수 JPA 페이징과 정렬
- 페이징 : DB에 있는 데이터를 적절한 size로 끊어서 끌어오는 것. ex) 100만건 load 불가...
- 페이징으로 끊어서 화면,API에 전달.
- SQL로 페이징 하면 너무 어려움 ex) row num을 3번넘게 넣어서 select 막 감싸고..
- int offset, int limit : 페이징 쿼리 condition. 몇번째 부터 시작해서 몇개를 가져와라 명령을 위한 변수
- .setFirstResult(offset) : 어디서 부터 가져올 것이냐 = 페이징 쿼리로 설정
- .setMaxResults(limit) : 가져올 갯 수 설정
- Total Count query 추가 : 몇번쨰 페이지인지 알아야 함.
- 페이지는 0부터 시작한다.
- db가 바뀌어도 JPA가 알아서 쿼리를 날린다 : dialect(방언)
- 페이징 : DB에 있는 데이터를 적절한 size로 끊어서 끌어오는 것. ex) 100만건 load 불가...
-
- 스프링 데이터 JPA 페이징과 정렬
- interface 2개로 페이징 표준화,공통화(감동적..)
- 모든 DB는 페이징을 제공하는데, 이것을 공통화 처리 해버렸다.
- org.springframework.data.domain.Sort : 정렬 기능
org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort 포함)
- 작성조건
- Parameter에 Pageable : 인터페이스, 쿼리에 대한 조건, 쿼리 리미트를 걸기 위해 : 현재 1페이지야..
- 반환타입 Page/Slice/List 선언 : total count 등을 결정
- 반환타입(Page/Slice)
- 반환 타입 Page : TotalCount 쿼리 같이 날림. 필요하니까
- org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징
- Contents + totalCount query 같이나감
- Page<Member> findByAge(int age, Pageable pageable) 형태로 쓴다.
- 사용
- PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
- Page<Member> members = memberRepository.findByAge(age, (Pageable) pageRequest); //PageRequest -> AbstractPageRequest -> Pageable
- page.getContent() : page 내부 실제 데이터 가져오기(contents)
- page.getNumber() : 해당 page의 번호
- page.getTotalElements(): 전체 element 갯수
- page.getTotalPages() : 총 page 갯수
- page.isFirst() : 첫페이지인지 아닌지 리턴
- page.hasNext() : 다음페이지 있는지 리턴
- PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
- 사용
- TotalCount 불필요 : slice (더보기 버튼 style.. 표시를 숨겨놓기)
- org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능
- 다음페이지 확인 원리 : limit + 1 조회하여 contents 갖고 옴.
- 기준 : 다음페이지가 있냐/없냐 정도로 푸는 것..?
- 전체 count 가져오지 않음
- Contents query만 나감
- 따라서 아래 두개는 존재하지 않는다.
- slice.getTotalElements(): 전체 element 갯수
- slice.getTotalPages() : 총 page 갯수
- slice.getContent() : page 내부 실제 데이터 가져오기(contents)
- slice.getNumber() : 해당 page의 번호
- slice.isFirst() : 첫페이지인지 아닌지 리턴
- slice.hasNext() : 다음페이지 있는지 리턴
- 다음페이지 확인 원리 : limit + 1 조회하여 contents 갖고 옴.
- Slice extends Streamable
- org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능
- cf) List (자바 컬렉션): 추가 count 쿼리 없이 결과(List)만 반환. 정확히 몇개만 끊어받기
- 반환 타입 Page : TotalCount 쿼리 같이 날림. 필요하니까
- Paging 안 쓰려고 하는 이유 : total count가 전체조회라, 성능을 잡아먹기 때문
- total count query를 잘 짜야 함.(ex) left outer join 같은 경우 count에서 똑같이 join 쓸 필요가 없다..)
- 그래서 count query 보통 분리한다
- 분리하는 법 : 쿼리 정의시 분리해서 쓸 수 있도록 되어 있음.
- @Query(value = "select m from Member m left join m.team t",countQuery = "select count(m.username) from Member m")
- 분리하는 법 : 쿼리 정의시 분리해서 쓸 수 있도록 되어 있음.
- 그래서 count query 보통 분리한다
- total count query를 잘 짜야 함.(ex) left outer join 같은 경우 count에서 똑같이 join 쓸 필요가 없다..)
- pagable 등이 등장하면서, JPQL을 직접 짜더라도, totalcount 같은 핵심비즈니스 외 관심사,잡무에서 벗어날 수 있게 되었다.
- API, Controller에서 Page<Entity> 그대로 반환하면 큰일 난다.
- Entity는 외부에 절대 노출하면 안됨. 무조건 DTO로 변환해서 노출해야 함.
- Entity 바꾸면 API spec이 바뀌고 API들 다 장애가 난다.
- Page<Entity> 를 Page<DTO>로 변환하기 : map
- page.map(member -> new MemberDto(member.getId(),....);
- Page를 API로 반환해도 좋은 이유
- 데이터가 모두 JSON으로 반환, content도 JSON으로 전송, response body JSON으로 보내면 깔끔 -> front에서 알아서
- Entity는 외부에 절대 노출하면 안됨. 무조건 DTO로 변환해서 노출해야 함.
- interface 2개로 페이징 표준화,공통화(감동적..)
- 벌크성 수정 쿼리
- JPA dirty checking은 단건이다, 한번에 수정할 필요가 있다 = 벌크성
- entity 중심이라 query와 다르게 분리되어 있다.
- em.createQuery().executeUpdate() : 응답 값의 갯수를 반환
- @Modifying : modifying이 있어야 select가 아니라 executeUpdate를 실행함
- JPA 벌크성 업데이트 주의사항(transaction에 bulk 연산만 있다면 무관)
- PersistenceContext에서 Entity가 관리가 되어야하는데, 무시하고 DB에 강제 Update하는 것
- PersistenceContext (1차 캐시)에 있는 엔티티의 상태와 DB에 엔티티 상태가 다를 수 있다.
- DB 반영 타이밍 전일 수가 있다.
- 권장하는 방안
- 1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행.
- 2. 영속성 컨텍스트에 엔티티가 있으면, 벌크 연산 직후 영속성 컨텍스트를 초기화
- 같은 transaction이면 같은 EntityManager가 올라와서 동작
- em.clear(); 추가 or @Modifying(clearAutomatically = true) 로 설정
- 깔끔한상태에서 DB에서 다시 조회해 가져 옴.
- MyBatis, JDBC Template등 연동할 때도 동일 문제 발생 가능하다.
- PersistenceContext (1차 캐시)에 있는 엔티티의 상태와 DB에 엔티티 상태가 다를 수 있다.
- PersistenceContext에서 Entity가 관리가 되어야하는데, 무시하고 DB에 강제 Update하는 것
- cf) JPQL 수행 전에, 항상 영속성 컨텍스트의 Data를 DB에 보내는 작업이 있다(flush)
- @EntityGraph : fetch join 편하게 쓰는 문법. 연관된 엔티티들을 SQL 한번에 조회하는 방법
- fetch join : 지연로딩과 발생하는 연관 문제를 먼저 이해할 것
- member1 -> teamA, member2 -> teamB
member - team은 지연로딩 관계이다. select member만 했을 때는 가짜 개체(proxy)만 가져온다.- getClass()로 중간에 print 해보면 확인 가능
- team의 세부정보를 알려고 할 때, read해 놓은 데이터가 없으므로 가져올 때(proxy 초기화)마다 쿼리가 실행된다. (N+1 문제 발생)
- N+1
- 1 : 원래 select 한 쿼리
- N : 1에 연계된 n개의 추가 쿼리
- N+1
- 결국 fetch join은 N+1 문제 해결에 사용
- @Query("select m from Member m left join fetch m.team")
- 조회 할 때 연관된 것을 같이 조회해버린다(or 다 받아버린다, fetch)는 뜻.
- proxy가 아니므로 proxy 초기화 할 필요가 없어진다.
- 연관관계가 있는 것 = 객체그래프라고 함
- select 할 때 data를 다 넣고 join 함(기본적으로 left outer join)
- "한번의 쿼리로 끝내!"
- member1 -> teamA, member2 -> teamB
- Spring Data JPA는 fetch join 때문에 JPQL을 사용할 수 밖에 없는 상황(네임 쿼리 등 불가..)
- @EntityGraph를 도입하여 처리하도록 개발 됨(JPQL 짜기 싫어서)
- 메소드 위에 @EntityGraph(attributePaths ={"team"}) 으로 선언하면 됨.
- fetch join을 annotation으로 바꾼것.
- JPQL 없이도 객체그래프를 엮어, 성능최적화 해서 한번에 가져오는게 된다
- JPQL 짰는데 fetch join 추가 하고 싶을 때도, @EntityGraph(attributePaths ={"team"}) 사용 가능
- 쿼리문 안에 join 필요 없음.
- 적절한사용 : 연관된 것이 매우 잦은 빈도로 같이 사용할 때.
- @NamedEntityGraph 사용방법
- Entity에 선언 : @NamedEntityGraph(name = "Member.all", attributeNodes = @NamedAttributeNode("team"))
- Repository에 선언 : @EntityGraph("Member.all")
- Entity에 name tag를 달고, 연관 node 명시하여 같이 조회하도록 하는 형태인 듯.
- 보통 안 씀
- fetch join : 지연로딩과 발생하는 연관 문제를 먼저 이해할 것
- JPA Hint & Lock
- JPA Hint : SQL Hint 아님, Hibernate(or 다른 구현체..)에게 알려주는 힌트
- Interface 이상으로 Hibernate가 뭔가 따로 쓸 때 필요함
- 대표적으로 ReadOnly에 쓴다.
- flush() 까지는 정보가 영속성 context에 남아있다.
- update시 dirtychecking을 위해선 원본이 있어야 한다.(메모리 먹는다)
- dirtychecking 때문에 원본을 따로 만드는 로직이 있다
- 변경을 안하는 상황에선 매우 불필요
- Hibernate에서 이 상황 해결 기능 제공(최적화 기능, 구멍을 만들어 놨다)
- 메소드 위에 @QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true")) 선언하면 됨
- readonly = true 세팅 시 원본/변경본의 snapshot을 안 만든다. dirty checking 안 함
- Lock
- select for update : DB에 lock 거는 것. JPA도 지원 함.
- 메소드 상단에 @Lock(LockModeType.PESSIMISTIC_WRITE) 선언
- 쿼리끝에 for update 가 추가 됨.
- 공식문서 참고 할 것.
- 실시간 많은 서비스에서는 사용금지. 특히 PESSIMISTIC Lock
- 차라리 Optimistic Lock이 나음(versioning 방식)
- 금융 관련, 원장 맞추는 스타일은 사용 하긴 함.
- JPA Hint : SQL Hint 아님, Hibernate(or 다른 구현체..)에게 알려주는 힌트
- 파라미터 바인딩(이름기반)
- 5. 확장 기능
- 사용자 정의 리포지토리 구현(쿼리 커스텀 필요할 때 사용 ex)query dsl, 기타 동적쿼리.. )
- 내 Custom기능 몇 몇개만 넣고 싶을 때, 인터페이스 메소드 직접 구현할 때 사용
- extends interface를 추가하는 원리.
- JAVA가 해주는 것은 아니고 Spring Data JPA가 해주는 것이다.
- JPA 직접 사용(Entity Manager), JDBC Template 사용, Querydsl 사용, Mybatis 사용....
- Spring Data JPA를 활용 위해 상속을 받아야 하는데, super class의 모든 것을 구현하는 건 현실적이지 않다.
- 순서 : Custom Interface 생성 > target Repository Impl 생성{EntityManger} -> target repository의 extends에 추가
- Impl 구현
- 1. @PersistenceContext 선언(생성자 1개면 생략가능) : EntityManager 주입
- 2. @RequiredArgsConstructor : 생성자 대체 가능
- 3. interface 함수 override -> em.createQuery()
- JDBC로 DB 직접 native query 날리고 싶다면 connection 얻는 형태로 가면 됨.
- 4. target repository의 extends에 추가
- Impl 구현
- 주의 규칙 : impl은 꼭 Target Repository로 세팅해야 한다. ex)MemberRepositoryImpl
- Impl이 싫다면, 아래 설정(근데... 관례 그냥 따르자)
- xml설정의 경우 : <repositories repository-impl-postfix="Impl" /> 변경
- javaConfig 설정의 경우 : @EnabeJpaRepositories( repositoryImplementationPostfix = "")
- Impl이 싫다면, 아래 설정(근데... 관례 그냥 따르자)
- 핵심 비즈니스 로직 & 화면에 맞춘 쿼리(Dto..)를 분리할 필요가 있음.
- Repository를 그냥 분리해라! custom해서 쓰지 말고, Class를 따로 만들어서 쿼리 날리자
- 분리할 때 고민할 점 : 핵심 비즈니스 & 그외 lifecycle 관점에서, command & query 분리 ...
- Repository를 그냥 분리해라! custom해서 쓰지 말고, Class를 따로 만들어서 쿼리 날리자
- 내 Custom기능 몇 몇개만 넣고 싶을 때, 인터페이스 메소드 직접 구현할 때 사용
- Auditing
- entity 이력 추적 관리를 편하게 하는 방법
- 테이블 생성시, 등록일, 수정일, 등록자&수정자(세션정보 기반) 필드 꼭 포함.
- 순수 JPA 사용
- Java : LocalDateTime
- @Column(updatable = false) 추가 : 수정불가 선언. created Date 필드
- @PrePersist 추가 : 이벤트 어노테이션. persist(저장) 전에 이벤트 발생. 메소드에 추가
- this. 생략 이유 : 요즘은 id를 다 사용하므로. 강조의 용도 외에는 표기 안 함.
- LocalDateTime.now()로 데이터 미리 세팅하는 이유 : 미리 해놔야 쿼리 날릴 때 편하다.
- null이 있으면 쿼리 지저분 하다.
- 등록일 수정일 미리 맞춰두기. 최초 등록된 데이터구나~
- @PreUpdate 추가 : 이벤트 어노테이션. update 전에 이벤트 발생. 메소드에 추가
- updateDate 갱신
- 대상 Entity에 윗 단계에서 생성한 class를 extends 처리
- 날짜가 없을 때
- JPA 진짜 상속 : 따로 있음. 추후 추가
- 속성만 상속 : @MappedSuperClass, 데이터만 공유하는 것
- 쿼리 날릴 때 같이 추가 되는 것 확인 가능
- 날짜가 없을 때
- 장점 : 여기저기 extends로 받으면 등록일,수정일 자동화(공통관심사)
- Spring DATA JPA 활용
- 스프링 부트 설정 클래스(Application main class)에
- @EnableJpaAuditing(AuditingEntityListener.class) 설정 추가
- 변경 마다 값 채우는것 null 로 두고 싶은 경우(ModifyOnCreate = false) 로 설정 추가
- CurrentAuditor 추가 : @Bean public AuditorAware<String> autditorProvider()
- SpringSecurity 등등 경우 : Session정보 가져와서 User Id 꺼냄
- 등록자/수정자에 넣음
- SpringSecurity 등등 경우 : Session정보 가져와서 User Id 꺼냄
- @EnableJpaAuditing(AuditingEntityListener.class) 설정 추가
- Entity에 @EntityListeners(AuditingEntityListener.class) 추가
- Event 기반으로 동작하도록 설정
- @CreatedDate 사용 : PrePersist 사라짐
- @LastModifiedDate 사용 : PreUpdate 사라짐
- @CreatedBy/@LastModifiedBy :등록자. 수정자
- 이벤트 감지시, AuditorAware 호출해서, 그때마다 결과물 꺼내서 채움
- 등록자/수정자는 안쓰는 경우도 많아서, 상위 클래스로 등록일,수정일 만든 후 상속 받아 추가하는 형태로 구현한다.
- Entity Listener 관련 설정(@EnableJpaAuditing, @EntityListeners..)생략하고 싶은 경우
- META-INF/orm.xml에 <entity-listeners> 설정 등록.
- 스프링 부트 설정 클래스(Application main class)에
- Web 확장 - 도메인 클래스 컨버터
- PK일 때, domain class converter 사용 가능. Spring Data JPA가 repository query통해서 return을 위한 parameter 변환 과정을 해 줌. 권장하진 않는 기능. 단순 조회 용만 쓸 수 있다. 트랜잭션 없이 조회하여, Entity 변경시 DB반영이 안 됨.
- AsIs : public String findMember(@PathVariable("id") Long id){ repository~~ > return }
- ToBe : public String findMember(@PathVariable("id") Member member) {return }
- AsIs : public String findMember(@PathVariable("id") Long id){ repository~~ > return }
- @RestController 추가
- @PathVariable 설정
- @PostConstruct 설정 : Spring Application 올라올 때 실행.
- PK일 때, domain class converter 사용 가능. Spring Data JPA가 repository query통해서 return을 위한 parameter 변환 과정을 해 줌. 권장하진 않는 기능. 단순 조회 용만 쓸 수 있다. 트랜잭션 없이 조회하여, Entity 변경시 DB반영이 안 됨.
- Web 확장 - 페이징과 정렬
- 페이징이 웹에서 바로 바인딩 되록 함.
- Controller > repository call(parameter pageable) -> PagingAndSortRepository
- page & size 지원
- "/members" mapping시, http://localhost:8080/members?page=1&size=3&sort=id,desc&sort=username,desc 의 형태 사용가능
- Parameter에 Pageable이 있으면,
- PageRequest 객체를 생성해서, 값을 채워서,
- Pageable에 inject 해 줌
- Sort
- 계속 url뒤에 추가 가능
- 요청파라미터 : page/size/sort
- 기본값 변경 원할 때
- 글로벌 설정 : application.yml 설정에 추가. Spring Boot 매뉴얼 확인하기
- ex) data: web: pageable: default-page-size: 10 max-page-size:2000
- 특별한 설정 : 메소드 파라미터 넣을 때 annotation 추가. 최우선으로 적용됨
- AsIs : (Pageable pageable)
- ToBe : (@PageableDefault(size=5, sort="username") Pageable pageable)
- 글로벌 설정 : application.yml 설정에 추가. Spring Boot 매뉴얼 확인하기
- 기본값 변경 원할 때
- Postman 툴: http return 값 이쁘게 보기 위해 사용(ex) JSON return)
- Page반환시는 항상 total Count query가 나간다.
- 접두사 : 한 api에 페이징 정보 둘 이상시 접두사로 구분
- @Qualifier 사용. ex) @Qualifier("member) , @ Qualifier("order) ...
- 결과 : /members?/member_page=0&order_page=1
- @Qualifier 사용. ex) @Qualifier("member) , @ Qualifier("order) ...
- Page를 DTO 변환
- DTO 반환해야 하는 이유
- Entity는 외부에 노출하면 절대 안된다.
- MVC 나눈 의미가 사라짐.
- Entity 손대는 순간 API spec이 변경 됨
- map을 통해서 내부 변경
- page.map(member -> new MemberDto(param1,param2...))
- 옛날엔 map이 없어서 직접 loop돌리면서 객체 새로 생성해서 담았었음..
- DTO는 Entity 봐도 됨
- public MemberDTO(Member member){ this.id = member.getId(), this.username...) 이렇게 변경 가능하고
- (member -> new MemberDto(param1,param2...)) 이것이 MemberDto::new 로 변경가능
- = 메소드 레퍼런스
- :: = package java.util.function;
- Page 1부터 시작하기
- Pageable, PageRequest 직접 구현 : PageRequest.of(page:1, size:2)
- spring.data.web.pageable.one-indexed--parameters = true로 설정
- web에서 파라미터를 -1로 바꿔서 처리하는 형태.
- pageable 객체의 data는 요청과 안맞는 문제 발생!
- web에서 파라미터를 -1로 바꿔서 처리하는 형태.
- DTO 반환해야 하는 이유
- 페이징이 웹에서 바로 바인딩 되록 함.
- 사용자 정의 리포지토리 구현(쿼리 커스텀 필요할 때 사용 ex)query dsl, 기타 동적쿼리.. )
- 6. 스프링 데이터 JPA 분석
- 스프링 데이터 JPA 구현체 분석(code level)
- JPA 공통 인터페이스 구현체 : SimpleJpaRepository
- findbyId,findAll... : 결국 EntityManager 통해서 다 작업. JPA 내부기능 활용
- Query String을 미리 들고 있는 것을 볼 수 있다.
- @Repository가 붙어있다
- Spring Bean의 scan 대상
- Exception을 Spring에 정의된 Exception 사용가능(매핑표 있음)
- 하부 구현기술을 JDBC -> JPA로 바꿔도, exception 처리 매커니즘 동일(스프링 강점)
- @Transational
- @Transcational은 별다른 옵션 없으면 Transaction 이어 받아 동작한다.
- Spring Data JPA의 모든 기능은 일단 Transaction 걸고 시작한다.
- 즉, Transcation 표기 없어도, 알아서 Transaction 시작 한다.
- 서비스 계증에서 시작 까먹어도 시작한다.
- 모든 JPA 데이터 변경은 Transaction 안에서 일어나야 한다.
- Spring Data JPA 사용 시 자동으로 됨!
- Transcation 안에서 변경 안하면 예외 던짐
- 즉, Transcation 표기 없어도, 알아서 Transaction 시작 한다.
- DatabaseConnection(set autocommit = false)로 넘기는 과정 포함 됨
- readOnly = true 설정 : flush를 안하도록 하는 설정 -> 성능 개선 효과
- read only니까 db 변경 필요없어서(dirty checking 등..) flush 안해도 됨.
- save 메서드
- isNew -> Persist 한다.
- else(=DB에 이미 있던 것) -> merge 한다.
- DB에서 read -> 교체 -> 반영
- 위 형태 때문에, DB Select 쿼리를 한번 하는 단점이 있다.
- update시 save의 merge는 가급적 하지 말고, entity 값만 바꾸는 것을 사용해야 한다.
- merge는 persist 상태를 벗어난 entity가 다시 persist 상태가 되어야 할 때 쓰는 것.
- JPA 공통 인터페이스 구현체 : SimpleJpaRepository
- 새로운 엔티티를 구별하는 방법 : @Id @GeneratedValue
- 전략
- @Id가 객체 일 때 : null로 봄
- @Id가 자바 primitive일 때 : 0으로 봄(객체가 아니기 때문)
- @GeneratedValue
- JPA에서 em.persist() 할 때 생성 함. JPA서 read해서 만들어 주입.
- GeneratedValue를 안 쓴다면 : merge가 실행된다(select 문 + insert문 실행)
- 데이터 저장은 꼭, persist로 해야한다. merge는 entity detached 상태에 쓴다.
- Id를 직접 넣어 줄 것이다.
- PK 값을 미리 세팅되어 있다.
- persist 호출이 안 된다. null이어야 새걸로 판단하는데 pk가 null이 아니다.
- 따라서, merge(DB에 값이 있다는 것이 가정된 메소드) 동작을 호출한다.
select문을 먼저 호출해서 read시도를 하고, 없으면 없다는 판단을하여
insert문을 추가로 호출.
- 따라서, merge(DB에 값이 있다는 것이 가정된 메소드) 동작을 호출한다.
- persist 호출이 안 된다. null이어야 새걸로 판단하는데 pk가 null이 아니다.
- PK 값을 미리 세팅되어 있다.
- 아래처럼 persist할 때, isNew판단 시(entityInformation.isNew(entity) = false)
ex) public void save(){
Item item = new Item("A");
itemRepository.save(item);
- Id를 직접 생성하고 싶은데... " : entity에 implements Persistable<String>을 사용하면 됨
- @Override isNew(){} 해서 직접 로직 정의
- step1. @EntityListener(AuditingEntityListener.class) 선언
- step2. @CreatedDate 사용해서 new 판단 : CreatedDate는 persist전에 호출 됨
@CreatedDate
private LocalDataTime createdDate; - step3. @Override boolean isNew() {
return createdDate == null;
}
- @Override isNew(){} 해서 직접 로직 정의
- 전략
- cf) JPA는 기본생성자가 꼭 있어야 한다.
- data JPA 나머지 기능 : JOIN일부 구현 불가하여..네이티브 쿼리 빼고, 실무에서 안 씀...
- => Query dsl로 다 처리 가능.
- Specifications (명세)
- 도메인주도설계에서 소개 된 Specification 개념
- where문 등 조건을 조립해서 쓸 수 있도록 하는 추상화 개념
- JPA Criteria 활용해서 사용 할 수 있도록 지원
- JPA에서 가장 잘못 설계되었다고 생각함..(영한's 의견)
- 가독성 떨어지고, 복잡해 지면 이해불가. 사용하면 분명 후회할 것
- predicate(술어)
- 참,거짓으로 평가. AND OR 등 연산자로 검색조건 쉽게 생성(composite 패턴)
- org.springframework.data.jpa.domain.Specification 클래스로 정의
- 사용방법 : extends JpaSpecificatonExecutor<Entity> : findAll, findOne...
- 파라미터에 Specification을 넣음
- Query By Example : springframework.data.commons
- . eEntity(도메인객체)가 검색 조건이 됨.
- NoSQL변경해도, 코드 변경 필요 없음
- 사용방법
- Example<Member> example = Example.of(member);
repository.findAll(example); - 연관관계 설정한 것도 조건으로 걸림.
- Example<Member> example = Example.of(member);
- Probe : 실제 도메인 객체
- ExampleMatcher : 특정 필드를 일치시키는 상세정보. 구체적인 검색조건
- 하지만, 결국 equal 조건 선에서만 찾을 수 있다.
- Example : 쿼리 생성에 사용
- INNER JOIN만 가능하고 LEFT JOIN안 됨
- . eEntity(도메인객체)가 검색 조건이 됨.
- Projections
- 특정 필드, 특정 데이터만 조회하고 싶을 때 쓰면 좋음.
- Interface기반 : AOP Proxy 이용 Spring Data JPA가 Implements등 구현하여 반환 해 줌.
- Close Projection
- Interface로 반환 타입 생성
- repository 메소드에 반환타입 입력
List<Inerface Model> findProjectionsByUserName("somename"); - AOP Proxy 이용 Spring Data JPA가 Implements등 구현하여 반환 해 줌.
- Open Projection
- username + age + team.name 조회
- @Value("#{target.username + ' ' + target.age + ' ' + target.team.name}") : spEL문법이라고 함
- targetEntity를 전부 가져 와서, 원하는 데이터만 표출.(최적화 X)
- Close Projection
- Class 기반 Projection
- 생성자의 parameter 이름으로 matching 시켜 projection
- class이므로 proxy 안 씀.
- 동적 Projection
- 반환 타입에 <T>로 Generic 선언하고 Service에서 구체적으로 Class 타입 명시하면 됨
- 중첩구조(연관관계, Interface 안에 Interface 선언)
- public interface NestedClosedProjection {
String getUsername();
TeamInfo getTeam();
interface TeamInfo {
String getName();
}
} - root 모델은 명시한 것만 가져오나, 내부의 연관모델은 전부 다 가져온다.
- left join 사용
- 비효율 임
- public interface NestedClosedProjection {
- 네이티브 쿼리
- sql 직접 짜는 것. JPA 사용시 가급적 쓰지 말고, 최후의 수단으로 사용 할 것
- @Query(value = "select * from member where username = ?", nativeQuery =
true) - 반환타입이 정해져있고, 엔티티 일부만 보고 싶을 경우 난감해 진다.
- 반환타입 : Object[], Tuple, DTO
- Sort 정렬이 정상 동작하지 않을 수 있음(직접 처리 할 것)
- app 로딩시점에 문법 확인 불가(JPQL은 로딩시 파싱과정 통해서 알 수 있는데..)
- 동적쿼리 불가.
- 동적 네이티브 쿼리 : 하이버네이트(외부 라이브러리 쓰는 것 권장)
- String sql = "select m.username as username from member m";
List<MemberDto> result = em.createNativeQuery(sql)
.setFirstResult(0)
.setMaxResults(10)
.unwrap(NativeQuery.class)
.addScalar("username")
.setResultTransformer(Transformers.aliasToBean(MemberDto.class))
.getResultList();
}
- 보통 Join등을 해서 DTO로 가져오고 싶을 때 쓰는 경향이 있는데, JdbcTemplate, myBatis 사용 권장
- Projections 활용 할 때는 쓸 만 하다.
- @Query(value = "SELECT m.member_id as id, m.username, t.name as teamName " +
"FROM member m left join team t ON m.team_id = t.team_id",
countQuery = "SELECT count(*) from member",
nativeQuery = true)
Page<MemberProjection> findByNativeProjection(Pageable pageable);
- @Query(value = "SELECT m.member_id as id, m.username, t.name as teamName " +
- count 쿼리 별도로 짜야 한다.
- 수백줄 쿼리는 쿼리 3개로 나눠서 수십줄로 처리하는 방법 권장
- 스프링 데이터 JPA 구현체 분석(code level)
'Back > JPA' 카테고리의 다른 글
[김영한, inflearn] 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 (0) | 2023.01.09 |
---|