이 글은 내용을 정리하고 개인적인 사견을 첨가한 2차 창작물이며, 강의 및 강의자료(코드 등)에 대한 저작권은 원본 및 원작자에게 있고 공유하지 않는다.
- H2 Database
- JPA 의 정확한 매핑
- JPA 내부 동작 방식 이해
- (수업자료 다운로드)
- 개발할때는 객체지향 방법론을 사용, 데이터베이스는 관계형 DB 를 사용 → CRUD SQL 을 작성하는 방법론들이 지루하게 반복되고 지루함 (개발자가 SQL 매퍼 역할)
- 객체와 관계형 데이터베이스는 비슷한것같지만 패러다임 자체가 다름
- 객체는 참조를 사용하고, 테이블은 왜래키를 사용한다 (
getTeam()vsJOIN) - 객체다운 모델링(참조변수)은 SQL 을 통한 INSERT 문을 작성하기 까다로움
- 객체는 자유롭게 객체 그래프를 참조할 수 있어야하는데, SQL 은 조회되지 않으면(JOIN 하지않으면) 탐색할 수 없다
- 계층형 아키텍쳐 에서는 넘어온 데이터를 믿고 쓸 수 있어야 하는데, 탐색이 안되면 해당 엔티티에 대한 신뢰가 없어짐
- 객체지향적으로 설계할수록 코드만 지저분해짐 → 자바 컬렉션처럼 DB를 사용할 수 없을까? 하는 고민들 → JPA 가 나옴
- JPA → Java Persistence API (자바진영의 ORM 표준)
- ORM → Object Relational Mapping (객체와 관계형을 매핑)
- 자바 컬렉션을 사용하듯 객체를 사용하면 JPA 가 여러 문제점들(매핑, 패러다임불일치) 해결해준다
- 예전에 너무 막장이었던 Enterprise JavaBeans; EJB 를 잘 만들기위해 고민하던 개발자 게빈킹이 하이버네이트를 만들어버림
- 우리는? → “JPA 표준 인터페이스에서 하이버네이트 구현체를 쓰는 중”
- Entity = JPA 가 관리하는 객체
- JPA 의 성능 최적화
- 1차 캐시와 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연
- 지연로딩
- JPA 는 ORM 이고 ORM 은 객체와 관계형DB의 매핑을 도와주는것이기 때문에 결국 잘 사용하려면 객체지향과 RDB를 다 잘 다루어야 함
- (H2 데이터베이스 설치)
- 최초 접속시엔
jdbc:h2:~/test로 접속, 이후에는jdbc:h2:tcp://localhost/~/test로 사용
- 최초 접속시엔
- 실무에선 거의 Gradle 을 사용하지만, 실습환경에선 Maven 을 사용
- 라이브러리에 사용할 hibernate 버전 확인 → https://hibernate.org/orm/
- 내게 맞는 버전 찾는법: https://spring.io/projects/spring-boot#learn → Reference Doc. → Dependency Versions
hibernate.dialect설정을 통해 JPA 는 각각의 데이터베이스만의 특정한 “방언”을 맞춰줄 수 있음
- (만든 Maven 프로젝트에서 JPA를 사용해보는
JpaMain클래스 예제)
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin(); // 트랜잭션 시작
try {
Member findMamber = em.find(Member.class, 1L);
findMember.setName("HelloJPA");
tx.commit(); // 트랜잭션 커밋
} catch (Exception e) { // 에러시
tx.rollback(); // 트랜잭션 롤백
} finally { em.close(); }
emf.close();
}
}- JPA 의 모든 변경은 트랜잭션 안에서 실행해야 한다.
- 엔티티 매니저는 쓰레드간에 공유 X (사용하고 버려야함)
- Java Persistence Query Language (JPQL) is the query language defined by JPA. → 객체지향 SQL
- JPQL 은 테이블이 아닌 객체(Entity)를 대상으로 쿼리를 실행함
- 실제 테이블에 쿼리를 날려버리면 JPA 의 구조 자체가 무너지므로, 복잡한 find 쿼리같은건 객체 지향 쿼리인 JPQL 을 많이 사용
- JPA 에서 가장 중요한 2가지를 뽑으라면?
- 객체와 관계형 데이터베이스 매핑 (설계)
- 영속성 컨텍스트: 실제 데이터베이스가 내부적으로 어떻게 동작하는지, “엔티티를 영구 저장하는 환경”
- 영속성 컨텍스트
- 영속성 컨텍스트는 논리적인 개념임 (물리x)
- 눈에 보이지 않으며, EntityManager 를 통해 영속성컨텍스트를 접근
- 엔티티의 생명주기
- 비영속(new/transient): 객체를 생성만 한 상태
- 영속(managed): em(EntityManager) 를 가져와서
em.persist( .. );를 해서 영속화 한 상태. 영속상태가 된다고 쿼리가 날아가는게 아님 - 준영속
- 삭제
- 영속성 컨텍스트는 내부에 1차 캐시를 들고있음
- 영속 엔티티의 동일성을 보장해줌
- 트랜잭션을 지원하는 쓰기 지연 → 모았다가 한번에 쿼리할 수 있는 기능 (배치가 아니고 실시간에선 크게 이점은 없지만 유리함)
- 엔티티 변경 감지 (더티체킹) → 커밋을 하면 flush 를 호출하는데, 이때 기존의 1차캐시 데이터와 스냅샷을 비교하고 다르면 update query를 작성해줌
- 플러시: 영속성 컨텍스트의 변경내용을 데이터베이스에 반영 (동기화) 하는 작업
- 실제 코드에서 커밋전에 강제로 플러시를 호출하려면
em.flush();를 호출하면 됨 - JPQL 을 사용시 JPQL 은 쿼리를 직접 생성해서 호출하기때문에, 쿼리가 나가기전 자동으로 플러시를 발생한다
- 영속상태가 되기위해선
em.persist나em.find를 호출하면 된다 (호출하면 1차캐시에 올라가기때문) - 영속상태의 객체를
em.detach( .. )를 사용하면 준영속 상태로 영속상태가 해제된다
- 영속성 컨텍스트에 대해 알아봄
- JPA 에서 매핑과 영속성컨텍스트가 매우 중요
- 엔티티에는 생명 주기가 있음
- 플러시는 영속성컨텍스트를 비우지않음 (그냥 동기화임)
- 트랜잭션이라는 작업 단위가 매우 중요
- 객체와 테이블매핑, 스키마 자동생성, 필드와 컬럼, 기본키, 연관관계 매핑에 대해 배움
- 객체와 테이블 매핑 -
@Entity- 이 어노테이션이 붙으면 JPA 가 관리하는 엔티티임
- 기본 생성자 필수
- 데이터베이스의 매핑정보만 보면 어떤 쿼리를 만들어야 할지 알 수 있으므로, 로딩시점에 테이블 쿼리를 만들어줌
- 데이터베이스 방언을 활용하여 특정 DB엔진에 맞는 DDL 문을 설정해줌
- 이렇게 생성된 DDL 은 반드시 개발 환경에서만 사용
- 필드의
@Column속성중unique값은 랜덤값으로 배정되기때문에 보통@Table의uniqueConstraints를 많이 사용 @Enumerated를 사용할때 타입은 반드시ORDINAL이 아닌,STRING을 사용 → 순서에 의존적이게 된다- 사실상
@Temporal은 옛날 자바의Date,Time클래스를 사용할때만 필요함
- 간단히 기본키 매핑은
@Id어노테이션을 사용함 → 직접 ID 를 셋팅 - 또한,
@GeneratedValue로 자동생성 할 수 있음IDENTITY: 보통 MySQL 에서 사용(MySQL 의 mysql auto_increment)디비에게 자동생성을 완전 위임SEQUENCE: 보통 오라클DB 에서 많이 사용하며, 시퀀스테이블을 사용하여 넘버링
- 권장하는 식별자 전략:
- 키본키 제약 조건: null이면 안됨, 유일해야함, 변하면 안됨
- 권장: Long 형 + 대체키(UUID 같은) + 키 생성전략 사용
- IDNTITY 전략의 특징
- 데이터베이스에 실제로 나가봐야 키값을 알 수 있기때문에, 영속성컨텍스트의 1차캐시에 저장하기 위해
em.persist를 호출하면 즉시insert쿼리가 나감 - 따라서 모아서 (버퍼링해서) 한번에 쿼리가 나가는 전략을 사용할 수 없다 → 김영한님 피셜: 근데 뭐 큰 의미는 없음
- 데이터베이스에 실제로 나가봐야 키값을 알 수 있기때문에, 영속성컨텍스트의 1차캐시에 저장하기 위해
- SEQUENCE 전략의 특징
- 시퀀스 전략이면 id 값을 가져오기 위해
em.persist를 호출하기 위해 다음 시퀀스를 위한 SELECT(call next value) 가 호출되고 영속성 컨텍스트에 저장됨 - 이럴때 성능 최적화를 위해
allocationSize를 사용해서 미리 시퀀스값을 미리 가져와서 사용하는 방법 → 동시성 문제도 없음
- 시퀀스 전략이면 id 값을 가져오기 위해
- (조금더 복잡한 예제를 통해 어떻게 설계하는지 알아보자)
- (라이브코딩으로 엔티티 설계대로 간단한 주문 Entity 들 설계)
- 데이터 중심으로 설계하면
Member를 직접 호출하지 않고memberId를 가지고 찾아야하기 때문에 객체지향 스럽지 않다 → 다음시간에 연관관계 매핑을 배워보자
- 단순히 id 를 가지고 호출하는것이 아닌, 객체 자체를 매핑해서 가져오는 방법
- (연관관계의 주인 개념이 어려움)
- 객체를 테이블에 맞춰서
team_id를 직접 맵핑했을때의 문제점- 팀을 저장하고 회원을 저장할 때,
teamId를 가지고 팀을 가지고 오는게 객체지향스럽지않음 team_id만 가지고 팀을 알 수 없으니, Member 에서 Team 을 사용하려면 계속em.find를 호출해야 함- 테이블은 외래키로 조인 vs 객체는 참조를 사용 → 모델링의 협력 관계를 만들 수 없음
- 팀을 저장하고 회원을 저장할 때,
- 단방향 연관관계는
@ManyToOne과@JoinColumn을 사용하여 간단하게 가능
- (JPA 계의 포인터;;) 영속성컨텍스트의 메커니즘과 양방향연관관계와 연관관계 주인이 제일 어려움
- 테이블의 연관관계는 사실상 Foreign Key 하나만 가지고 양방향을 다 다룰 수 있음 but 객체는 아님
@OneToMany에서는mappedBy를 사용하여 어디에 매핑이 되어있는지 기재해준다- 연관관계의 주인과 mappedBy
- 객체에서의 양방향 연관관계는 사실상 단방향 연관관계가 2개 있는거임
- 객체의 양방향에서는 어느 단방향쪽에서 업데이트를 해줘야 할지 정해야 한다 (테이블은 FK 하나로 되지만) → 둘중 하나로 주인을 정해서 관리를 해야함 = 연관관계 주인
- 연관관계 주인에서 주인은 CRUD 가 가능, 주인이 아니면 읽기만 가능, 주인은
mappedBy를 사용하지 않음, 주인이 아니면mappedBy로 주인이 누군지 써야함 - 누구를 주인으로 해야하지? → mapped ‘By’ 니까
mappedBy가 없는게 주인임,mappedBy는 읽기만 가능하고 값을 넣어봐야 의미없음 - (강사 추천) 외래키가 있는곳을 주인으로 정하는게 좋음
- 이걸 반대로 하면 Team 을 업데이트 했는데 Member 쿼리가 나갈 수 있음
- 성능이슈도 있음 → 뒤에서 설명
- 1:N 에서 N쪽이 무조껀 연관관계 주인이 되는게 제일 깔끔함
mappedBy가 붙어있으면 읽기전용이라 JPA 가 insert 할때 아무런 동작을 안씀- 양방향 매핑시에 연관관계 주인에 반드시 값을 넣어주지 않으면
null이 들어가므로 주의 - 그냥 양방향 매핑을 할때는 양쪽에 다 값을 넣어주는게 맞음
- JPA 를 통해 불러오기 전에
em.flush(), em.clear()를 해주면 문제가 안되지만, 그전에 호출해버리면 DB 에서 select 가 안나가서 데이터가 없다고 잘못 나옴 - JPA 입장에서 보면 주인에만 값을 넣어주면 되지만, 객체지향적으로 생각하면 둘다 데이터가 있는게 맞기때문에
- JPA 를 통해 불러오기 전에
- 이럴때를 위해 연관관계 편의 메서드를 작성해주는게 좋다. 더 자세한 편의메서드 작성법은 책을 참고
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}- member 를 기준으로 team 을 넣을지, team 을 기준으로 member 를 넣을지 편의메서드를 작성할곳을 정해주면 됨 (한쪽에만 작성해주는게 좋음) → 어플리케이션 작성하면서 어디 있으면 되겠다 싶은곳에 넣으면됨
- 양방향 매핑시에 무한루프를 주의 (toString, lombok, JSON 생성 라이브러리 사용시)
- Lombok 에서 toString 은 지양해라
- Entity를 JSON 생성할때 쭉 뽑으면서 에러가 남 (Entity를 컨트롤러에서 직접 반환하는 경우) → 컨트롤러에는 그냥 Entity 를 쓰지말고, DTO 로 변환해서 반환해라
- 정리: 단방향 매핑만으로도 이미 연관관계는 매핑이 완료된거임
- 객체입장에서 “양방향매핑”은 단점이 많아짐
- 처음에 설계할 때, 먼저 단방향 매핑으로 싹다 설계를 완료해야함
- (양방향매핑은 그냥 읽기 권한만 추가되는거라서) 필요시에만 양방향을 추가함 → 양방으로 바꾸는건 그냥 List 만 하나 추가해주면 되기때문
- 중요) 연관관계의 주인은 외래키의 위치를 기준으로 정해야함
- (그전엔 DB 설계대로 만들었는데, 이제 객체지향 스럽게 연관관계를 설계해보자)
- (섹션4 마지막에 했던 실전예제 1을 객체연관관계 매핑으로 변경하는 내용)
- 가급적이면 단방향 매핑이 좋은거임 → 실제 개발하다가 필요할때만 양방향 매핑
- “특정 회원의 전체 주문목록” 을 가져오는 기능을 구현하려면 “주문” 에서 특정 멤버의 정보를 가져와야지, 특정 멤버 → 멤버의 주문 으로 가는건 관심사를 끊어내지못하고 더 어렵게 설계한거라고 생각함 (주문이 필요하면 그냥 주문에서부터 시작하면됨)
- 실전에서는 JPQL 을 작성하는 경우가 꽤 많은데 이럴경우 양방향 연관관계가 필요한 경우가 많다
- 기본적인 연관관계 고려사항을 따져볼것이고, 여러 매핑시 고려사항을 알아봄
- 연관관계 매핑시 고려사항 3가지:
- 다중성 → n:1, 1:n, 1:1, n:m (다대다는 실무에선 지양하는게 좋음)
- 단(양)방향 → 테이블은 외래키 하나로 양쪽 조인으로 데이터를 가져오기때문에 “방향”이라는 개념이 없지만, 객체는 참조형 필드가 있어야해서 단/양방향이 있음. 그리고 사실 “양방향”은 아니고 단방향 2개임
- 연관관계의 주인
- 다대일 단방향: 당연히 테이블에는 다(N) 쪽에 외래키(FK)가 가야하고, 객체는 외래키 있는쪽에 참조객체를 넣으면 됨 (가장 많이 사용하는 연관관계)
- 여기서 양방향을 하려면 간단하게 반대쪽에 List 만 넣고
mappedBy해주면 해결
- 여기서 양방향을 하려면 간단하게 반대쪽에 List 만 넣고
- 여기선 1쪽에서 외래키를 관리 (강사님은 권장하지 않는 모델임, 스펙상 스프링이 지원만 할 뿐 실무에서 지양함)
@OneToMany를 사용한List<Mamber> members한곳에@JoinColumn( .. )을 선언해주면 실제로 사용 가능- 이렇게 매핑할 시 하이버네이트가 옆 테이블 (FK 가 있는 테이블) 의 업데이트 쿼리를 만들어줌
- 가장 큰 단점: 도메인 입장에서 Team Entity 만 수정했는데도, Member Table 의 update 가 나가기때문에 트래킹이 굉장히 힘들거나 사용이 난해해짐
- 이런 구조가 너무 필요하다면 그냥 원래대로 n:1 로 설계하고, 양방향 매핑으로 가져오는걸 권장
- 주의)
@JoinColumn( .. )을 안써버리면 테이블과 테이블 사이에 JPA 가 매핑테이블을 만들어서 관리함 → 테이블이 하나 더들어가니까 성능이슈도 있고, 운영에서 너무 복잡해짐 - 결론) 조금 객체지향적으로 참조를 하나 더 넣더라도 “다대일 양방향 매핑”을 사용하는것을 권장
- 일대다 양방향
- 이런 매핑은 공식적으론 없는거지만,
@ManyToOne와@JoinColumn(name="..", insertable=false, updatable=false를 사용하면 insert 와 update 를 사용하지 않는 읽기전용 (==mappedBy랑 비슷한) 역할을 할 수 있음
- 이런 매핑은 공식적으론 없는거지만,
- 일대일은 대칭관계이기때문에, 주테이블이나 대상테이블에 아무곳에나 외래키 선택이 가능
- 외래키 데이터베이스에 유니크제약조건을 추가해주는게 좋다
XXXToOne은 사용법이 비슷함- (위 그림대로 설계하면 굉장히 쉽고 편리함)
- 일대일인데 대상테이블(locker) 에 FK 가 존재하는 단방향은 JPA 에서 아예 지원하지 않음
- 일대일인데 대상테이블(locker) 에 FK 가 존재하는 양방향은 그냥 위 그림에서 locker 쪽에 외래키를 넣고, 반대쪽에 읽기전용 mappedBy 를 넣어주면됨
- 외래키는 Member 에 넣는게 좋을까 Locker 에 넣는게 좋을까? (일단 정답은 없지만)
- 만약에 1:1이 1:n 으로 바뀔 미래를 생각하면 → Locker 쪽에 FK 가 있다면 그냥 유니크 제약조건만 빼버리면됨
- 실제 ORM 을 사용해서 개발을할 개발자 입장에선 Member 에 Locker_Id 를 FK 로 가지고있으면 비즈니스 로직에서 멤버는 대부분 가져올때 이미 FK 값이 있으므로 JOIN 을 사용하여 쉽게 Locker 의 상태를 알 수 있음. 단, Locker 가 null 일수도있음
- 강사님은 그냥 Member 에서 Locker 를 가져가는걸 선호함 (위의 그림 그대로)
- Locker 에 Member_Id FK 가 걸려있으면 무조껀 양방향으로 걸려있어야됨 (1:1 단방향 반대쪽 FK는 지원을 안하니까)
- 주테이블에 외래키 → 주테이블만 조회해도 이미 Locker 정보를 알기 쉽지만, null이 들어갈 수 있음
- 대상 테이블에 외래키 → 나중에 Locker 가 N 으로 변경되어도 수정할게 적음, 단 Entity 구조 자체가 반드시 양방향으로 해야하고 지연로딩을 사용할 수 없음
- (강사님은 실무에서 쓰면 안된다고 생각)
- 관계형 데이터베이스는 정규화된 테이블2개로 다대다 관계를 표현할 수 없음 → 따라서 연결테이블(매핑테이블) 을 사용해서 1:n 두개로 만들어야함
- 근데 객체는 N:M 구조가 그냥 가능함 (컬렉션 2개 넣으면 되니까)
@ManyToMany를 사용하고 동일하게 단/양방향 모두 가능 - 실무 테이블에선 연결만 하고 끝나지 않고 복잡한데, 매핑테이블에 다른 필드(칼럼)들이 필요한데 이런것들 추가가 불가능하고, 중간 테이블이 숨겨져있어서 쿼리가 굉장히 복잡하게 나감
- n:m 이 필요한경우 중간에 수동으로 매핑테이블을 만들고 1:n 두개를 만들어서 연결
- 중간 매핑테이블에서 실제로 사용할땐 PK는 의미없는 값을 넣고, FK 로 그냥 매핑하는게 대부분 괜찮았다
- (1:1 관계와 N:N 관계를 추가하고 Entity 를 설계하는거 실습)
@ManyToMany지양하라고 했지만 보여줄려고 그냥 사용함 - (
@JoinColumn@ManyToOne등의 옵션들 한번 찾아볼 것)- 다:1 은
mappedBy자체가 없음 (연관관계 주인이 되어야해서)
- 다:1 은
- 객체는 상속관계가 있지만, DB 테이블은 상속개념이란개 (몇개 엔진 빼곤) 없음
- 슈퍼타입 - 서브타입 관계가 그나마 객체의 상속관계와 비슷함
- DB 는 이런 상속 구조를 3가지 전략으로 구현할 수 있음
- 조인 전략: 최상위에 item table 을 두고, PK 값과 DTYPE 으로 구분해서 가져옴 → 4개의 테이블
- 단일테이블 전략: 한개의 테이블에 때려박기 → 1개의 테이블 (단순하고 성능이 좋음(조인이 없으니까;))
- 개별테이블 전략: 테이블을 각각만들고 똑같은 필드를 그냥 중복시킴 → 3의 테이블
- 여기서 어떤 방법을 선택하던 JPA 로 구현 가능함
- 상속으로 해결할 시 JPA 의 기본전략은 “단일테이블전략” 임
@Entity
@Inheritance(strategy = InheritanceType.JOINED) // JOINED, SINGLE_TABLE, TABLE_PER_CLASS
@DiscriminatorColumn
public class Item { // TABLE_PER_CLASS 시에는 abstract class 로 선언해서 Entity 생성이 안되게
...
}
//
@Entity
//@DiscriminatorValue("B")
public class Book extends Item {
...
}
@Entity
public class Album extends Item {
...
}- 조인 전략 (사용법은
@Inheritance(strategy=InheritanceType.JOINED)로 선언하면 됨)@DiscriminatorColumn을 넣어주는게 좋음 → DTYPE 없이 상위테이블만 보면 얘가 뭐에 관련된 데이턴지 알 수 없기때문에- JPA 스펙상 조인전략도 DTYPE 이 필수인데, 하이버네이트 구현체는 필수는 아닌듯 (선언 안하면 안만들어짐)
- 단일테이블 전략
- 성능이 제일 잘나옴(단일 테이블이라서), join 없고, insert 도 쉽게 나감
- 단일테이블은 칼럼 구분을 할 수 없기때문에 DTYPE 이 필수로 들어가야함 (생략해버리면 JPA 가 강제로 넣어버림)
- 구현클래스마다 테이블 전략
- 상위 테이블은 추상 (abstract) 클래스로 만드는게 맞음(상속으로 사용할꺼니까)
- 테이블이 각자 다 다르고 연관된것도 없어서 DTYPE 을 선언해도 생성안됨
- 이 전략은 값을 넣을땐 편리한데, 부모클래스 타입으로 조회하면 UNION ALL 로 다찾아와야함 (어디에 뭐가있는지 모르니까)
- 각각의 장단점
- 조인 전략
- 테이블이 정규화가 잘 되어있음, 외래키 참조 무결성 제약조건을 지킴, 저장공간이 효율적임
- 조회할 때 뭘해도 JOIN 해야해서 성능이 저하되고, 쿼리가 복잡
- 이게 객체랑도 잘 맞고 정규화도 되고 설계도 깔끔해서 거의 정석적인 방법
- 단일 테이블 전략
- 조인이 필요없으므로 단순함, 조회도 단순함
- (치명적인 단점) 관계 없는 테이블은 싹다 null 값이 들어가야됨 → 데이터 무결성 제약조건을 만족하지않음
- 한테이블에 다들어가므로 테이블에 데이터가 비약적으로 많아질 수 있다
- 구현클래스마다 테이블 전략
- (일단 이건 쓰지마셈) DBA 와 객체설계 뭘로봐도 안좋음
- 서브타입을 완전히 구분해서 처리할 때 쓸만함
- 쿼리가 UNION 으로 나가야하고 아주 복잡함
- 시스템이 한번 만들어지면 변경하기 힘들어짐
- 조인 전략
- 강사님은 보통 조인전략을 사용하고, 애플리케이션이 아~주 단순하면 그냥 단일테이블로 함, “구현클래스마다 테이블전략 은 절때 쓰지마라.”
- 매핑정보만 받는 부모(슈퍼) 클래스
- 이건 엔티티도 아니고, 절때 상속관계 매핑이 아님 →
@MappedSuperclass는 직접 조회 (em.find) 가 안됨 - 직접 생성할일이 없으므로 추상클래스로 사용하시길 권장
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class BaseEntity {
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}- (기존의 실전예제 4에 Item 상속관계와 MappedSuperclass 로 생성일자 데이터 등을 추가)
- 애플리케이션이 너무 커지면 잘만든 상속관계에서도 복잡해서 다시 또 고민을 해야함 (정답은 없음)
- 일단 JPA 에서 프록시 개념이 쉽지가 않음
- 공부를 할때는 왜? 써야하는지를 꼭 알아야됨
- 연관관계가 걸려있다는 이유만으로 항상 모든 데이터를 한번에 (JOIN) 가져오면 비용만 늘어나고 비효율적임 → 이런 방법론을 지연로딩 & 프록시로 해결
- JPA 는
em.getReference()를 지원하고 이는 데이터베이스에 직접 조회하지 않는 프록시 객체를 던져줌 - 프록시는 Entity 의 본체를 상속받아서 만든 더미클래스이며, 실제 엔티티처럼 사용해도 이론상 상관이 없다
- 특징
- 프록시 객체는 참조(Target Entity) 를 보관
- 프록시 객체는
.getName을 호출하고 진짜 값이 없으면 영속성 컨텍스트를 건너서 데이터를 받아온 뒤 실제 Entity 의 위치를 저장하고, 상위 Entity 의 메서드를 실행한다 (이 동작 방식 자체는 JPA 스펙이 아니라서 구현체마다 다를 수 있음) - 프록시 객체는 처음 사용할 때 1번만 초기화 됨
- 프록시 객체를 초기화 하면 실제 엔티티로 변경되는게 아니고, 실제 엔티티에 접근해서 사용하는 것 임 → 따라서 “==” 비교를 하게되면 타입이 다를 수 있고, “instance of” 를 사용해야 함
- 영속성 컨텍스트에 엔티티가 이미 들어있으면
getReference를 호출해도 프록시가 아닌 실제 Entity 를 반환- 이미 영속성 컨텍스트에 올려놨는데 프록시를 던져봐야 의미가 없음
- JPA 는 같은 영속성 컨텍스트내에서 “==” 비교했을때 항상 같은 결과가 나오도록 지원해야 하기때문에 → 재밌는건 프록시를 비교해도 같은 프록시를 가져옴
- 더 재밌는건 프록시로 먼저 조회를 하고
em.find를 호출하면 find 를 해도 프록시 객체가 나온다 (==을 지원하려고)
- 준영속 상태일 때 프록시를 초기화하면 초기화되지 않았다는 에러가 발생
- 프록시용 유틸리티 메서드가 잇음
isLoaded: 프록시 인스턴스 초기화 여부 확인getClass.getName으로 프록시 클래스를 확인 할 수 있음Hibernate.initialize(ref)로 호출하지 않고 강제 초기화 할 수 있음 → JPA 표준스펙에 없어서 Hibernate 임
- 그럼
getReference많이 쓰나요? → 그렇지는 않음 잘 안씀 but 프록시개념을 알고있어야 잘 쓸 수 있음
- 팀와 멤버를 한번에 다가져오면 손해라서 JPA 는
@ManyToOne(fetch = FetchType.LAZY)로 지연로딩을 지원함 - 지연로딩 객체는 처음에 프록시 객체를 반환
getTeam까지가 아니고 실제 내부에 접근 (e.g.getTeam.getName) 을 하면 초기화함@ManyToOne(fetch = FetchType.EAGER)로 즉시로딩을 지원함 (한번에 가져오고 초기화까지 다 끝난상태)- EAGER 로 선언되어있으면 각자의 쿼리가 나가기 vs 조인으로 한번에 가져오기 선택이 가능한데, 대부분의 JPA 구현체들은 조인전략으로 구현함
- 강사님 피셜) 실무에선 가급적 지연로딩만 사용
- 즉시로딩을 적용하면 전~혀 예상하지 못한 SQL 이 막 나감 → JOIN 이라고 해서 막 느리진 않은데 테이블이 많아지면 성능이 처참해짐
- 즉시로딩은 JPQL 에서 N+1 문제를 일으킨다 → em.find 는 pk 로 가져오는것이기때문에 JPA 가 JOIN 최적화를 할 수 있지만, JPQL 은 쿼리를 그대로 실행하기때문에, 해당 객체의 select 가 나가고 그 행의 갯수만큼 또 select 문이 나간다
- 해결법 3가지: 페치조인을 사용 or 엔티티그래프 사용 or 배치사이즈를 조정해서 1:1 로 해결
- (실습은 그냥 이론이고 현업에선 모두 지연로딩을 사용하길 권장)
// 예시
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval =true)
private List<Child> childList = new ArrayList<>();- 영속성 전이 (CASCADE) 는 연관관계와 전~혀 상관이없음
- 부모를 저장할때 자식도 함께 저장하고 싶거나 할 때 (특정 엔티티를 영속상태로 만들때 연관 엔티티를 함께 영속상태로 만들기위해) 사용 (모두
em.persist가 나감) - 양방향 편의 메서드 만들때 기존 값이 있나 확인하고 빼주는게 나중에 코드 (복잡하지만 사용)
- 개발할 때 부모코드를 중심으로 할 때 자식클래스의
em.persist를 자동으로 하고싶을 때 사용 - 종류
- ALL: 모두 적용
- PERSIST: 영속화만 함께 적용
- REMOVE: 삭제
- 그래서 언제 써야하나? → 하나의 부모가 자식들을 오롯이 관리할 때 (게시판과 게시물 첨부파일 관계), 다른 Entity 에서 또 관리를 하면 사용했을때 난리날 수 있음 (소유자가 하나일때만 사용) + 라이프사이클이 동일할때
- 고아객체란 “부모엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능” (컬렉션에서 빠진애는 디비에도 없애버림)
orphanRemoval = true로 사용 가능
- 참조가 제거된 엔티티는 다른곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
- 참조하는 곳이 하나이고, 특정엔티티가 개인이 소유할 때 사용
- 개념적으로 부모 Entity 가 지워지면 자식은 자동으로 고아객체기때문에 다 삭제됨 → 이는 삭제가 전파되는
CascadeType.REMOVE처럼 동작한다 - 영속성전이 + 고아객체 = ? : 이 두옵션을 다 활성화하면 부모의 생명주기로 자식의 생명주기를 동일하게 관리할 수 있음
- DDD의 Aggregate Root 에서 “리포지토리는 Aggregate root 만 구현하고 나머지는 Repository 를 만들지 않는것이 낫다” 라는 조건을 구현 할 수 있음
- (모든 연관관계를 지연로딩으로 변경하는 실습)
- 임베디드타입과 값타입컬렉션이 중요
- JPA는 최상위 데이터타입을 “엔티티타입”과 “값타입” 으로 구분
- 엔티티타입:
@Entity로 정의하는 객체, 데이터가 변해도 식별자(pk) 로 추적 가능 - 값타입: int, Integer 처럼 단순히 자바 기본 타입이나 객체, 식별자가 없으므로 값 변경시 추적 불가
- 엔티티타입:
- 값타입에는 기본값타입(자바 기본타입, 래퍼클래스, String), 임베디드타입(e.g. x,y 로 만든 좌표클래스), 컬렉션 값타입(자바 컬렉션) 3개가 있음
- 기본값타입:
- 생명주기를 엔티티에 의존 → 엔티티를 삭제하면 이름, 나이도 같이 삭제됨
- 값 타입은 공유되지 않음 → 회원 이름을 바꿨다고 다른 회원 이름도 바뀌면 안되니까 → 자바의 기본 타입은 절때 공유되지않음 (자바 기본 지식)
int a = 10; b = a;로 해놓고a=20으로 변경해도 기존 값은 변경되지 않음 (자바의 기본타입에 대한 기본기임) → primitive type 은 공유되지 않음- but Integer 같은 래퍼 클래스나 String 같은 클래스들은 참조로 공유 가능하나, 변경 자체가 불가능하게 해서 사이드이펙트가 없음
- 기본값타입:
- (내장 타입 으로 번역되기도 함)
- JPA 는 embedded type 이라고 부름
- int 나 String 처럼 임베디드타입도 값타입으로 분류되기때문에 주의해야 한다
- 장점
- 재사용이 가능 (기간이나 주소 같은거)
- 응집도가 높다 (따로 관리하지않음)
Period.isWork()같은 객체지향적인 의미있는 메소드를 생성 가능- 값타입을 가지는 엔티티의 생명주기에 의존함 (엔티티가 사라지면 날짜들도 사라짐)
- 테이블은 테이블대로 생기는게 맞지만, 객체는 공통화하고 묶어주는게 (의미있는 메서드 만들기도 쉽고) 유리함
@Embeddable이나@Embedded는 둘중에 하나만 넣고 생략해도 되지만 강사님은 둘다 넣는걸 선호- 특징
- 임베디드 타입은 사용하기 전과 후에 매핑하는 테이블은 같다
- 객체와 테이블을 아주 세밀하게 매핑할 수 있다
- 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음
- 만약에 같은 Entity 에서 같은 값 타입을 여러개 사용하면? → 원래는 에러남 이때
AttributeOverride를 사용해서 사용하게 할 수 있음 (자주 사용하진 않음) - 당연한 말이지만, 임베디드타입이 null 이면 안에 있는 필드들도 다 null 임
- 값 타입은 복잡한 객체 세상을 조금더 단순화하려고 만든 개념이라서 값타입은 단순하고 안전하게 쓸 수 있어야함
- 단, 값타입 공유참조(하나의 임베디드 타입을 다른 엔티티에서 볼때) 상황에서 변경하면 문제가 생김
- 만약에 난 “이렇게 같이 변경되는것도 의도했어” 라고 하면 값타입 대신 새로운 Entity 로 사용해야 함
- 한계
- 항상 값을 복사해서 사용하면 공유참조 문제로 발생하는 부작용을 피할 수 있음
- 직접 정의한 값타입은 자바의 기본타입이 아니라 객체타입이라서 참조값을 대입하는 방법론을 (개발자가 입력하는것을) 막을 수 없다
- 객체의 공유참조는 피할수가 없다
- 객체타입의 한계: primitive type 은 값을 복사해서 대입해서 안전, 객체타입은 참조를 전달하기 때문에 안전하지않음
- 객체 타입을 “불변객체”로 설계하여 부작용을 원천차단 해버리는게 좋음 → 생성자로만 값을 설정하고 setter 를 다막아버림 → 자료들 찾아보면 방법론이 더 있음 찾아보는게 좋음
- 그럼 값을 바꾸고싶을땐? → 새로운 객체를 만들어서 new 로 아예 그 값타입 자체를 통째로 바꾼다 (부분만 바꾸지말고)
- 값 타입은 인스턴스가 달라도 그 안의 값이 같으면 같은 개념으로 봐야함
- 자바의 == 비교 자체가 참조값을 비교하기때문에 새로 new 로 생성한 객체가 같다고 나올리가 없음
- 동일성 비교: 인스턴스의 참조를 비교 (
==) - 동등성 비교: 인스턴스틔 값을 비교 (
equals()) → equals 의 기본값도 ==는 비교를 사용하기때문에, 오버라이드 해줘야함 (알겠지만 equals 를 구현해주면 hashCode 도 구현해줘야 함)
- 값타입을 컬렉션에 넣어서 사용하는걸 “값 타입 컬렉션” 이라고 한다
- 데이터베이스 테이블은 컬렉션을 지원하지 않기때문에 (최근엔 JSON으로 풀어서 하는 방법 등이 존재하지만 원래는 안되므로) 컬렉션을 별도의 테이블로 풀어서 저장해야 한다
- 값타입 컬렉션은 영속성전이 + 고아객체제거 기능을 필수로 가진다
- 값타입 컬렉션도 지연로딩을 사용한다
- 값 타입은 엔티티와 다르게 식별자 (PK) 가 없기때문에 변경하면 추적이 어려움
- 값 타입 컬렉션에 변경사항이 발생하면 그냥 모든데이터를 다 날리고 현재 필요한 값을 다시 넣는식으로 관리함 (wow..) → 결론부터 말하자면 그냥 값 타입 컬렉션 쓰지말고 1:N 으로 엔티티를 만들어서 관리하고 여기에 값 타입 (컬렉션x) 을 사용하고 영속성전이+고아객체제거 를 사용하는게 나음
- 값 타입 컬렉션 매핑은 모든 칼럼을 묶어서 기본키로 사용해야 함 (wow..)
- 그럼 언제써야하나? → 진~짜 단순해서 SelectBox 에 단순한 값 몇개 넣고 추적도 필요없고 수정도 필요없고 그럴때 사용
- (지금까지 배운 값타입들을 실전예제5 코드에 적용하는 실습)
- 임베디드타입의 equals 를 자동 구현할 때, 필드 직접접근보다 getter 를 사용하는게 좋다 → 프록시가 getter 를 사용하기때문
- JPA 는 다양한 쿼리 작성 방법들을 지원함 → JPQL, JPA Criteria, QueryDSL, Native SQL, JDBCTemplate, MyBatis ..
- JPQL
- 테이블은 매핑만 하는거지 우리는 엔티티 객체를 중심으로 개발을 해야하고, 검색을 할때도 테이블이 아닌 엔티티 객체를 대상으로 쿼리를 날릴 수 있어야함
- JPA 는 SQL 을 추상화한 JPQL 이라는 객체 지향 쿼리 언어를 제공하며, SQL 과 유사함
- JPQL 은 객체를 대상으로 쿼리를 하는것이고, SQL 은 테이블을 대상으로 쿼리를 한다는 점이 다름
- JPQL 도 단순히 그냥 문자열이라서 “동적쿼리” 작성하기가 너무 힘듦
- Criteria (자바 표준에서 지원)
- (실무에서 강사님이 써봤는데 너무 복잡해서 잘 안쓰게됨) → QueryDSL 을 추천
- 문자대신 자바 코드로 JPQL 을 만들 수 있고, JPA 공식임 → (JPQL 빌더 느낌)
- QueryDSL
- 딱 읽어보기만 해도 SQL 이랑 문법이 똑같음
- Criteria 처럼 JPQL 빌더 역할이며, 자바코드로 JPQL 작성가능
- QClass 를 사용하여 컴파일 시점에 문법오류를 찾을 수 있음
- (강사님 피셜 복잡한쿼리가 나가야하는 대부분의 상황에 그냥 QueryDSL 을 깔고감)
- 아무리 QueryDSL 을 쓰더라도 JPQL 을 알고 써야 잘 쓸 수 있음
- Native SQL
- 특정 데이터베이스만 종속적인 기능이나 hint 를 사용할 수 있도록 JPA 에서 지원
em.createNativeQuery( .. )로 작성 가능
- SpringJdbcTemplate 지원
- (강사님은 Native 를 직접쓰기보다 그냥 JDBC Template 를 사용)
- 주의) JPA 를 사용할 때는 flush 를 관리 해주지만, db connection 을 가져와서 executeQuery 할 때는 강제로
em.flush()를 사용해줘야함 (JPA 를 우회해서 SQL을 실행한다면 그 직전에 수동으로 플러쉬) - 플러시는 네이티브쿼리랑 commit 할때 자동으로 날아감 (기존 내용)
- JPQL 만 잘한다면 그냥 대부분 다 잘 할 수 있음 → 95% 정도는 JPQL 과 QueryDSL 로 대부분 해결 가능
- JPQL == Java Persistence Query Language
- (JPQL 기본적인 사용법 실습 내용)
- JPQL 작성시에 내용에 대소문자를 구분함 (객체의 내용과 똑같이)
- 반환 타입이 명확하면
TypeQuery, 명확하지 않으면Query를 지원한다 - 결과는
getResultList()를 쓰면 컬렉션(비면 빈 컬렉션이)이,getSingleResult()를 쓰면 그냥 그 객체(결과가 없거나 여러개면 에러남)가 나옴getSingleResult()는 나중에 Spring Data JPA 를 쓰면 에러가 안나고 그냥 null 이나 optional 이 던지도록 확장되어있음
- 파라메터 바인딩시에 “위치”와 “이름” 기준으로 할 수 있는데, “이름”으로 하는게 덜 헷갈리고 휴먼에러 발생 가능성도 적음
- 프로젝션 == select 절에 조회할 대상을 지정하는 것을 말함
- 원래 DB 는 스칼라 타입만 넣을 수 있는데 여기선 엔티티, 임베디드타입도 모두 사용 가능
- SELECT 에서 엔티티프로젝션(엔티티를 가져오면) 조회된 모든 엔티티가 영속성컨텍스트에서 관리됨
- (경로표현식에서 설명 하겠지만) JPQL 에선 자동으로 생성된 SQL JOIN 을 쓰기(묵시적으로 사용)보다 명시적으로 코드를 작성해주는게 좋다 (묵시적으로 하면 쿼리가 어떻게 나갈지 예측이 힘들고, 튜닝할때 추측으로 해야하니까)
- 임베디드값타입의 한계) 그냥 임베디드타입으로 호출이 불가능하고 (소속되어있기때문에) 소속을 알려줘야함
- 스칼라 타입 프로젝션은 그냥 필드를 적어주면 됨
SELECT DISTINCT ~로 중복을 제거할 수 있다- 여러 타입을 한번에 가져올 경우는?
- TypeQuery 말고 Query 로 받으면 됨 그럼 내부에
Object배열로 값이 넘어옴 - 제네릭으로
List<Ovject[]> resultList = em.createQuery(..).getResultList();하면됨 - (제일 깔끔하게) new 명령어로 DTO 로 바로 조회 가능 (내가 DTO 프로젝션이라 불렀던 것) → type 을 DTO 로 주고
em.createQuery("select new jpql.aDTO from ~", aDTO).getResultList();로 가져올 수 있음 → 순서와 타입이 일치해야 함
- TypeQuery 말고 Query 로 받으면 됨 그럼 내부에
- 오라클 데이터베이스 같은건 페이징이 거지같은데, JPA 는
setFirstResult(시작위치) 와setMaxResults(조회 데이터 수) 로 잘 추상화 되어있음 order by로 sort 까지 넣어야 페이징이 완벽히 잘되는지 확인되는거지 (순차적으로)- 미세팁) Entity
toString만들땐 연관관계 무한루프 주의 - 페이징도 각 데이터베이스의 방언에 맞춰서 만들어줌
String jpal = "select m from Member m order by m.name desc";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setFirstResult(10)
.setMaxResults(20)
.getResultList();- 조인은 SQL 조인이랑 실행은 똑같은데 객체 스타일로 조인 문법이 나감
// List<Member> result = em.createQuery("select m from Member m left outer join m.team t", Member.class) // outer 는 생략 가능
List<Member> result = em.createQuery("select m from Member m inner join m.team t", Member.class) // inner 는 생략 가능
.getResultList();- JPA 2.1 부터 on 절을 활용하여 조인 대상을 필터링하고
String jpql = "select m, t from Member m left join m.team t on t.name = 'a'"- (하이버네이트 5.1부터) 연관관계가 없는 엔티티 외부 조인을 지원한다 (내부조인은 원래 됐음)
String jpql = "select m, t from Member m left join Team t on m.username = t.name" // left 빼면 당연히 내부조인 - 우리가 알고있는 SQL 서브쿼리와 거의 동일
- JPA 서브쿼리의 한계: 원래 JPA 표준에서는 WHERE 과 HAVING 에서만 서브쿼리가 가능 → 하이버네이트 구현체는 SELECT 도 가능
- FROM 절에서의 서브쿼리(인라인뷰)는 현재 JPQL 로 아예 불가능 → JOIN 으로 풀어서 해결해야함 → 여기까지 해도 안되면 그냥 네이티브로 해결 or 애플리케이션레이어에서 해결 (그냥 여러번의 쿼리 날림)
- 문자와 숫자를 어떻게 JPQL 에서 표현하는가? 문자는 그냥
'으로 - 숫자는 그냥 10L, 10D, 10F 다 표현 가능
- Boolean 은 TRUE, FALSE
- ENUM 을 바로 쓰고싶은경우는 패키지명을 다 입력해줘야함
- 다형성을 이용한 엔티티 타입: TYPE(m) = Member →
"select i from Item i where type(i) = Book" - 앵간한 표준 SQL 은 다 지원함
- (CASE 식, coalesce, nullif 등 문법 실습) → 이런 대부분의 표준함수는 디비 엔진과 상관없이 동작함
- JPQL 의 표준함수 (디비와 상관없는), 사용자 정의함수를 DB 방언에 추가해서 사용 → 기본적으로 하이버네이트 내부에
registerFunction으로 대부분 미리 등록되어있음 - (사용자 정의 함수 등록하는 방법 알려주심)
- 경로표현식: 점(
.) 찍어서 객체 그래프를 탐색하는 것 - 상태필드 →
m.username처럼 필드 값을 저장하기 위한 필드- 경로 탐색의 끝이라 더이상 탐색이 안됨
- 연관 필드 →
m.team처럼 연관관계를 위한 대상이 엔티티인 필드(@xxxToOne),t.users처럼 대상이 컬렉션인 컬렉션값 연관필드(@xxxToMany)- 단일값 연관경로(엔티티 대상)는 묵시적 내부조인(innter join) 으로 탐색을 더 할 수 있음 (e.g.
m.team.xxx.name) → 묵시적으로 조인하면 쿼리를 튜닝하기 쉽지않고, 어디서 어떻게 조인이 발생했는지 팔로우하기 쉽지않아서 실무나 큰 어플리케이션에선 직관적으로 판단할 수 있게 명시적으로 선언하는게 좋음 - 컬렉션값 연관경로은 묵시적 내부조인은 발생하지만 더이상 탐색은 불가능 (e.g.
t.members) 이뒤로안됨 - 컬렉션 값 연관경로는 명시적 조인을 사용하면 더 탐색할 수 있음 (e.g.
"select m.username from Team t join t.members m" - (강사님 피셜) 이런거 다 무시하고 일단 “묵시적조인”을 그냥 쓰면안됨 → 튜닝도 어렵고 조인이 어떻게 발생할지, 쿼리가 어떻게 나갈지 알아보기도 어려움
- 단일값 연관경로(엔티티 대상)는 묵시적 내부조인(innter join) 으로 탐색을 더 할 수 있음 (e.g.
- 묵시적 조인은 항상 내부조인(inner join) 만 발생한다
- fetch join 에 대해서 알아보고, 실무에서 진짜 중요함 → 디비에 있는게 아님(SQL 조인 종류가 아님)
- JPQL 에서 성능 최적화를 위해 사용 (연관된 것들을 SQL 한번에 받아올때 사용)
- e.g.
[LEFT [OUTER] | INNTER] JOIN FETCH {조인경로}→select m from Member m join fetch [m.team](http://m.team)(이러면 SQL 에 Team 의 모든 정보도 select 함) - (중요) 대부분의 N+1 문제는 그냥 거의 fetch join 으로 해결
- 지연로딩으로 셋팅을 해도 fetch join 이 항상 우선임
- DB 입장에서 1:N 조인하면 데이터가 fetch join 시 뻥튀기가됨 →
select t from Team t join fetch t.member하면 조인된 행만큼 똑같은 데이터가 중복해서 들어오니까- 중복은 SQL 의 DISTINCT 를 사용하면 되는데, JPQL 에서 distinct 를 사용하면 객체의 중복값도 제거 해준다 →
select distinct t from Team t join fetch t.members하면 됨 - 근데 distinct 해도 sql 을 실행한 결과는 (멤버 데이터가 다르므로) 뻥튀기 된 중복 자체가 제거되지 않음 → 근데 JPQL 은
distinct를 인지하고 컬렉션에서 똑같은 값을 한번 더 걸러줌 - 당연하지만 N:1 관계는 뻥튀기가 되지않으므로 그냥 아무렇게나 써도 상관없음
- 중복은 SQL 의 DISTINCT 를 사용하면 되는데, JPQL 에서 distinct 를 사용하면 객체의 중복값도 제거 해준다 →
- (정리하면) 일반 조인은 조인된 데이터 안에는 데이터가 안들어있고 getter 를 실행할때 개별로 호출, 페치조인은 조인된 데이터도 안에 데이터를 다 채워서 동시에 (한번에) 가져옴
- 근데 페치 조인 대상에는 관례상 별칭을 사용할 수 없음 (e.g.
.. join fetch t.members as m이런식의as같은 별칭을 주면안됨 → 데이터를 조회할 때 SQL 로 엉뚱한 데이터가 넘어오게 작성될 수 있음) - 유일하게 쓰는경우는
select t Team t join fetch t.members m join fetch m.xxx뭐이런식으로 진짜 복잡한 페치의 경우 강사님도 써본 적 있음 - 객체그래프를 탐색하는 데이터를 다 조회하는게 좋음 → 근데 그 중에 몇개 데이터만 필요하면 (e.g. 멤버 5개만 필요하면) 팀에서 조회하는게 아니고, 멤버 도메인 가서 조회해야함
- 애초에
@OneToMany같이 연관된 데이터는 다 가져오도록 설계되어있고, 그렇게 사용하는게 맞음 - 둘이상의 컬렉션은 페치조인 불가능함 (어떻게 야매로 가능해도 쓰면안됨 → 이래도 데이터가 잘 안맞음)
- 컬렉션을 페치조인하면 페이징 (e.g. setFirstResult 같은) API 를 사용할 수 없음 → 이것도 1:N 에서 페치조인하면 멤버 수만큼 데이터가 뻥튀기 되었을 때 페이징으로 결과를 짤라서 사용하면 데이터가 원래 데이터와 다를 수 있음 (정합성이 안맞음) → 하이버네이트 구현체에서 강제로 페이징을 사용하면 (얘네도 알 수 없기때문에) 그냥 다 가져와서 메모리에 적재해버림 → 이건 터지면 큰 장애로 발생할 수 있음
- 1:1, N:1 은 페치조인해도 페이징 가능함 → 따라서 멤버(N) 으로부터 팀(1) 로 방향을 바꿔서 해당 문제를 해결 할 수도 있음
- 페이징이 필요할 때
@BatchSize(size=100)를 사용하면, LAZY 걸린 N 개의 문제도 한번에 여러개의 조회쿼리 (where 에 id 를 배치만큼 쭉 넣어버림) 가 나가서 해결할 수 있음 (배치사이즈는 관례상 1000 이하의 수) → 주로 글로벌로 사용하는 경우가 많은데,hibernate.default.batch_fetch_size를 사용하면됨 - 그냥 페치조인을 잘못사용할 때 정합성이 깨지는 모든 경우의수는 사용에 주의해야됨 (거의쓰면안됨)
- 페치조인은 객체그래프를 유지할 때 사용하면 효과적임
- 최근의 성능 튜닝은 굳이 복잡한 sql 이 아니더라도 fetch join 같은 방법론들로 대부분 해결
- (크게 중요한건 아님)
- 예를들어서 다형성을 활용하여 엔티티를 설계했을 때, 조회 대상을 특정 자식으로 한정 할 수 있음 → Item 중에 Book, Album 을 조회
// JPQL
select i from Item i where type(i) in (Book, album)
// 여기서 type(i) 부분이 실제 SQL 쿼리의 DTYPE 으로 캐스팅됨- 자바의 타입 캐스팅과 유사한
TREAT가 있음 (JPA 2.1)
// JPQL
"select i from Item i where treat(i as Book).auther = 'kim'"
-> (싱글테이블쿼리) select i.* from Item i where i.DTYPE = 'B' and i.auther = 'Kim'- 엔티티를 직접사용하면 SQL 에서 해당 엔티티의 기본키를 사용함
// JPQL
"select count(m.id) from Member m" 대신 "select count(m) from Member m" 이렇게 해도 동일한 SQL이 실행됨
-> select count(m.id) as cnt from Member m
// where 문에 "where m.id = :member" 로 써도 똑같이 m.id=? 으로 나감- 엔티티 직접사용 외래키 값 e.g.
where m.team = :team이렇게 써도 SQL 은where m.team_id = ?으로 나감
- 엔티티에
@NamedQuery를 사용해서 이름으로 이 쿼리를 호출할 수 있음 (재활용) - 얘는 어노테이션이랑 xml 에서도 정의해놓을 수 있음 → xml 이 우선권을 가짐
- 얘는 애플리케이션 뜰때 (로딩될때) JPA 가 캐싱해서 들고있음 → 로딩시점에 이 쿼리가 맞는지 검증됨
- 네임드쿼리의
name은 그냥entityName.abc로 관례로 많이 씀 - 지금 보면 이렇게 엔티티에 써놓는 경우는 거의없고, 나중에 Spring data JPA 에서 사용할 때
@Query이 어노테이션을 사용하는게 바로 네임드쿼리라 실행시점에 문법오류를 바로알 수 있음 → 엔티티 상위에 있으면 엔티티만 지저분해짐
- 만약에 JPA 변경감지 (더티체킹) 으로 업데이트를 하려면 데이터가 많아질수록 변경건에 대한 Update Query 가 너무많이 나감 → 한번에 동시에 업데이트(==벌크) 가 필요함
- 쿼리 맨뒤에
.executeUpdate()를 쓰면 사용가능
int resultCount = em.createQuery("update Member m set m.age = 20").executeUpdate(); // 반환값은 영향받은 행 갯수 - 하이버네이트 구현체는 insert 랑 delete 같은것도 지원함
- (중요) 벌크연산은 영속성 컨텍스트를 무시하고 그냥 데이터베이스에 때려박음 → 해결법은 아래 2개
- 영속성 컨텍스트가 빈 상태면 벌그연산을 가장먼저 실행
- 영속성 컨텍스트에 데이터가 있으면 벌크연산 수행 후에 영속성 컨텍스트를 밀어버림 (다시조회함)
- 쿼리 날리면 AUTO 모드일 때 commit 하거나 query 나갈때 자동호출되니 고민안해도 되는데, 문제는 영속성컨텍스트 데이터가 있을때 벌크를 전송하고 바로 이어서 다시 값을 읽어오면 엉뚱한 값이 나오니까 습관적으로 벌크연산 이후엔
em.clear()를 호출하는게 좋음 - 참고로, Spring Data JPA 쓰면
@Modifying을 사용하면 자동으로 auto clear 해줌 → 이런걸 위해 이론을 잘 알고있어야 함
- 스프링부트에서 하이버네이트를 사용하면 Entity 를 생성할때 칼럼의 필드명을 카멜케이스 → 스네이크 케이스로 자동으로 바꿔준다
- TIP) 양방향 연관관계에서 다쪽에 있는
List는= new ArrayList<>();로 초기화 해주는게 Add 할때 nullpointException 이 안뜨니까 관례로 많이 씀 - 실무에선 세터를 잘 안쓰고, 생성자에서 만들거나 빌더패턴을 사용
- (당연한거지만) 에러는 컴파일타임 → 런타임, 로딩시점 → 사용자사용시 순으로 좋은 에러
- tip) JQPL 문자열이 intellij 에서 문법오류로 반환할 경우 옵션+엔터 했을때 나오는창에서
Inject language or reference에 Hibernate QL 로 지정할 수 있음



