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
테스트
H2 데이터베이스 설치
스프링 데이터 JPA와 DB 설정, 동작확인
profile 파일 변경하기
application.properties(삭제) -> application.yml 생성
아래처럼 spring, jpa, logging 세가지 설정 넣어준다.
ddl-auto: create
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' 추가
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 호출할 그 시점에 쿼리
@OneToMany private List<Member> members = new ArrayList<>(); //List임을 주의
이렇게 양쪽 설정하는 건, mapped by 설정 필요함. FK 없는 쪽에 거는 것이 좋다!
@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 세팅을 한다?
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로 발전.
공통 인터페이스(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)
공통 인터페이스(JpaRepository) 적용
TestCase를 하나 만들어서, @Autowired로 interface Repository에 주입을 시키고, 돌려본다.
잘돌아가면, 어디선가 implements가 잘 만들어졌다는 뜻이고, Spring Data JPA가 잘 넣어 준 것이다.
공통 인터페이스 분석(개발자가 상상할 수 있는 모든 기능 제공)
spring-data-jpa
JpaRepository
org.springframework.data.jpa.repository, spring-data-jpa
Package 경로를 보면, Spring data의 일부로, jpa 특화의 느낌을 알 수 있다.
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 스캔용.
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 구현 필요없다
IntelliJ Tip) 이전 것 왔다갔다 하기 : Cmd+E+Enter
4. 쿼리 메소드 기능
쿼리를 어떤식으로 해결할지 제공하는 강력한 3가지 기능
메소드 이름으로 쿼리 생성
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이라고 함.
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
순수 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(방언)
스프링 데이터 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() : 다음페이지 있는지 리턴
TotalCount 불필요 : slice (더보기 버튼 style.. 표시를 숨겨놓기)
org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능
다음페이지 확인 원리 : limit + 1 조회하여 contents 갖고 옴.
기준 : 다음페이지가 있냐/없냐 정도로 푸는 것..?
전체 count 가져오지 않음
따라서 아래 두개는 존재하지 않는다.
slice.getTotalElements(): 전체 element 갯수
slice.getTotalPages() : 총 page 갯수
slice.getContent() : page 내부 실제 데이터 가져오기(contents)
slice.getNumber() : 해당 page의 번호
slice.isFirst() : 첫페이지인지 아닌지 리턴
slice.hasNext() : 다음페이지 있는지 리턴
Slice extends Streamable
cf) List (자바 컬렉션): 추가 count 쿼리 없이 결과(List)만 반환. 정확히 몇개만 끊어받기
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")
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에서 알아서
벌크성 수정 쿼리
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등 연동할 때도 동일 문제 발생 가능하다.
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개의 추가 쿼리
결국 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)
"한번의 쿼리로 끝내!"
Spring Data JPA는 fetch join 때문에 JPQL을 사용할 수 밖에 없는 상황(네임 쿼리 등 불가..)
@EntityGraph를 도입하여 처리하도록 개발 됨(JPQL 짜기 싫어서)
메소드 위에 @EntityGraph(attributePaths ={"team"}) 으로 선언하면 됨.
fetch join을 annotation으로 바꾼것.
JPQL 없이도 객체그래프를 엮어, 성능최적화 해서 한번에 가져오는게 된다
JPQL 짰는데 fetch join 추가 하고 싶을 때도, @EntityGraph(attributePaths ={"team"}) 사용 가능
적절한사용 : 연관된 것이 매우 잦은 빈도로 같이 사용할 때.
@NamedEntityGraph 사용방법
Entity에 선언 : @NamedEntityGraph(name = "Member.all", attributeNodes = @NamedAttributeNode("team"))
Repository에 선언 : @EntityGraph("Member.all")
Entity에 name tag를 달고, 연관 node 명시하여 같이 조회하도록 하는 형태인 듯.
보통 안 씀
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 방식)
금융 관련, 원장 맞추는 스타일은 사용 하긴 함.
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은 꼭 Target Repository로 세팅해야 한다. ex)MemberRepositoryImpl
Impl이 싫다면, 아래 설정(근데... 관례 그냥 따르자)
xml설정의 경우 : <repositories repository-impl-postfix="Impl" /> 변경
javaConfig 설정의 경우 : @EnabeJpaRepositories( repositoryImplementationPostfix = "")
핵심 비즈니스 로직 & 화면에 맞춘 쿼리(Dto..)를 분리할 필요가 있음.
Repository를 그냥 분리해라! custom해서 쓰지 말고, Class를 따로 만들어서 쿼리 날리자
분리할 때 고민할 점 : 핵심 비즈니스 & 그외 lifecycle 관점에서, command & query 분리 ...
Auditing
entity 이력 추적 관리를 편하게 하는 방법
테이블 생성시, 등록일, 수정일, 등록자&수정자(세션정보 기반) 필드 꼭 포함.
순수 JPA 사용
Java : LocalDateTime
@Column(updatable = false) 추가 : 수정불가 선언. created Date 필드
@PrePersist 추가 : 이벤트 어노테이션. persist(저장) 전에 이벤트 발생. 메소드에 추가
this. 생략 이유 : 요즘은 id를 다 사용하므로. 강조의 용도 외에는 표기 안 함.
LocalDateTime.now()로 데이터 미리 세팅하는 이유 : 미리 해놔야 쿼리 날릴 때 편하다.
null이 있으면 쿼리 지저분 하다.
등록일 수정일 미리 맞춰두기. 최초 등록된 데이터구나~
@PreUpdate 추가 : 이벤트 어노테이션. update 전에 이벤트 발생. 메소드에 추가
대상 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 꺼냄
Entity에 @EntityListeners(AuditingEntityListener.class) 추가
@CreatedDate 사용 : PrePersist 사라짐
@LastModifiedDate 사용 : PreUpdate 사라짐
@CreatedBy/@LastModifiedBy :등록자. 수정자
이벤트 감지시, AuditorAware 호출해서, 그때마다 결과물 꺼내서 채움
등록자/수정자는 안쓰는 경우도 많아서, 상위 클래스로 등록일,수정일 만든 후 상속 받아 추가하는 형태로 구현한다.
Entity Listener 관련 설정(@EnableJpaAuditing, @EntityListeners..)생략하고 싶은 경우
META-INF/orm.xml에 <entity-listeners> 설정 등록.
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 }
@RestController 추가
@PathVariable 설정
@PostConstruct 설정 : Spring Application 올라올 때 실행.
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
요청파라미터 : 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)
Postman 툴: http return 값 이쁘게 보기 위해 사용(ex) JSON return)
Page반환시는 항상 total Count query가 나간다.
접두사 : 한 api에 페이징 정보 둘 이상시 접두사로 구분
@Qualifier 사용. ex) @Qualifier("member) , @ Qualifier("order) ...
결과 : /members?/member_page=0&order_page=1
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는 요청과 안맞는 문제 발생!
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 안에서 변경 안하면 예외 던짐
DatabaseConnection(set autocommit = false)로 넘기는 과정 포함 됨
readOnly = true 설정 : flush를 안하도록 하는 설정 -> 성능 개선 효과
read only니까 db 변경 필요없어서(dirty checking 등..) flush 안해도 됨.
save 메서드
isNew -> Persist 한다.
else(=DB에 이미 있던 것) -> merge 한다.
위 형태 때문에, DB Select 쿼리를 한번 하는 단점이 있다.
update시 save의 merge는 가급적 하지 말고, entity 값만 바꾸는 것을 사용해야 한다.
merge는 persist 상태를 벗어난 entity가 다시 persist 상태가 되어야 할 때 쓰는 것.
새로운 엔티티를 구별하는 방법 : @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문을 추가로 호출.
아래처럼 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; }
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...
Query By Example : springframework.data.commons
. eEntity(도메인객체)가 검색 조건이 됨.
사용방법
Example<Member> example = Example.of(member); repository.findAll(example);
연관관계 설정한 것도 조건으로 걸림.
Probe : 실제 도메인 객체
ExampleMatcher : 특정 필드를 일치시키는 상세정보. 구체적인 검색조건
하지만, 결국 equal 조건 선에서만 찾을 수 있다.
Example : 쿼리 생성에 사용
INNER JOIN만 가능하고 LEFT JOIN안 됨
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)
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 모델은 명시한 것만 가져오나, 내부의 연관모델은 전부 다 가져온다.
네이티브 쿼리
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);
count 쿼리 별도로 짜야 한다.
수백줄 쿼리는 쿼리 3개로 나눠서 수십줄로 처리하는 방법 권장