해당 글은 인프런 김영한 님의 JPA 프로그래밍 기본편를 바탕으로 저의 생각을 정리한 글입니다. 저의 글로는 해당 강의에 나오는 내용을 다 이해할 수 없습니다. 본 글을 보시고 JPA에 관심이 생기신다면 정말 좋은 강의이니 꼭 들어보시는 것을 추천합니다 !
JPA소개
- ‘객체지향’ 방식인 java와 ‘관계형’ DB.. 저장을 위해선 어쩔 수 없이 SQL로 객체를 변환해서 DB에 저장을 한다. 반대로 불러오려면 SQL로 불러와서 객체로 매핑을 한다 ..
- RDBMS에 PK,FK써서 자바의 객체 상속 관계를 풀어낸 것을 슈퍼타입, 서브타입 관계라고 한다.
- 이런 상속 관계를 이용하면 조인SQL도 쓰고 각 테이블에 맞는 객체 또 매핑해야 되고 …
- 근데, Collection을 사용해서 다형성을 이용하면 이런 상속 관계를 편하게 사용할 수 있다.
- 객체는 참조를 하며 연관관계를 맺는다. 즉, 어떤 테이블을 참조하면 해당 테이블의 PK를 FK로 갖는 모델링을 그대로 구현하면 객체답지 못한 모델링임.. 따라서, 참조하는 테이블의 객체를 가지고 있는 형태로 모델링을 한다.
- 즉,DB를 컬렉션이라 생각한다면, list.add(member) / Member member= list.get(id),Team team= member.getTeam(); 이렇게 구현할 수 있다.
- team만 join한 쿼리로는 member의 order 객체까지 get할 수 없다 → 계층형구조(controller,service..)에서는 신뢰도가(member.getTeam, member.getOrder같은 호출을 했을 때 정확하게 객체를 불러올 수 있다는 신뢰) 중요한데 확신할 수 없게된다.
- 즉, 객체를 확신을 갖고 사용할 수 없게 된다.
- 또한, SQL로 쿼리 날려서 나온 값을 새로운 객체를 만들어서 매핑해서 리턴하는 find메소드를 똑같은 id로 두번날리면 두 객체는 다름!(인스턴스가 다르니까) 근데, 컬렉션에서 같은 id로 get하면 당연히 똑같은 객체가 나옴(인스턴스 자체가 같은 아이니까)
- 객체를 컬렉션에 저장하듯이 DB에 저장하는 방법이 JPA 이다!
- ORM : Object Relational Mapping
- 객체는 객체대로 RDB는 RDB대로 설계하는데, ORM프레임워크가 중간에서 이 둘을 매핑 해주는 것
- java뿐 아니라 대부분 ORM기술이 존재한다.
- JPA는 기존에 JAVA 애플리케이션이 JDBC API를 통해 DB에 접근하던 방식에서, 애플리케이션과 JDBC사이에서 동작해서 불일치 문제를 해결한다.
- JPA는 표준 인터페이스임 이걸 구현한 구현체로 hiberante, eclipse link 등이 있는데 대부분이 hiberante구현체를 사용한다.
- 막 우리가 컬렉션 내의 객체 하나의 이름을 바꾸려면 객체 get해서 setName만 새로운 걸로 해주면 되는 것 처럼 JPA에서도 똑같이 setName하면 수정이 된다 !
- JPA를 사용하면 컬렉션을 사용하는 것과 같은 개념이 되니, 엔티티,계층이 신뢰할 수 있게 된다 !
- 놀랍게도, 동일한 트랜잭션 내에서 jpa.find로 꺼낸 두 객체는 (물론 id도 같음) 실제로 같은 객체임을 보장해준다.
- 이게 1차 캐시를 사용한다는 것 (아주 약간의 성능 향상이 생김)
- DB 고립 단계가 read commit이어도 애플리케이션에서 자체적으로 Repeatable Read를 보장한다
- 트랜잭션을 지원해서 쓰기 지연이 가능함 (JDBC BATCH SQL기능을 이용한다)
- 즉, 트랜잭션 열어서 여러 내용들을 만들고 한 시점에서 커밋하면 네트워킹 비용이 감소한다
- 즉, 버퍼 writing이 가능하다
- 지연로딩, 즉시로딩
- 지연로딩 : 객체가 실제로 사용될 떄 로딩하는 것
- 멤버를 조회하지만 팀을 자주 쓰는 것이 아니라면 지연로딩을 하는 것이 좋다!
- 즉시로딩 : JOIN SQL로 한번에 연관된 객체까지 미리 조회
- 즉, 멤버를 조회할 때 팀이라는 객체가 항상 같이 사용된다면 즉시로딩이 좋다.
- 지연로딩 : 객체가 실제로 사용될 떄 로딩하는 것
JPA시작
- dialect: 방언, SQL표준을 지키지 않는 특정 DB의 기능을 사용한다는 것
- javax. ~ : JPA표준을 지키는 것, hibernate말고 다른 걸 써도 되게끔 한 것
- hibernate ~ : hibernate구현체에서만 사용가능한 property
- JPA는 Persistence라는 클래스가 META-INF/persistence.xml에 적힌 설정을 조회하고 EntityManagerFactory라는 팩토리클래스를 생성한다. 그 후, 어떤 요청이 있을 때 마다 EntityManager를 해당 팩토리 클래스가 em을 생성해서 동작한다.
- emFactory는 반드시 하나만 생성해서 애플리케이션 전체에서 공유한다
- em은 쓰레드간에 공유하지 않는다
- jpa의 모든 데이터 변경은 트랜잭션 내에서 실행되어야 한다 (즉, 트랜잭션을 만들어서 그 안에서 실행해야 수행된다.)
- JPA는 컬렉션처럼 다루도록 설계되었기에 jpa써서 객체 꺼내고 변경하고 싶은 부분 변경하면 알아서 update문을 날려줌
- 변경 부분은 트랜잭션이 커밋되기 직전에 바로 update문을 날리고 커밋되며 처리된다.
- JPQL : 전체 회원 조회나 뭐 특정 조건 걸어서 SQL을 직접 만들어서 날리는 것 인데 em.createQuery형태로 만들고 문법도 조오끔 다르긴한데 거의 비슷 (JPA쓸때 쓰던 쿼리들)
- SQL을 추상화 했다고 생각하면 된다
- 엔티티 객체를 대상으로 쿼리를 날린다고 생각하자
영속성관리-내부동작방식
- JPA에서 가장 중요한 것이 객체랑 RDB 매핑하는것과 영속성 컨텍스트
- 엔티티를 영구 저장하는 환경 이라는 뜻
- 논리적인 개념이고 EM을 통해 접근한다
- EM을 생성하면 영속성 컨텍스트와 연결이 된다
- 스프링 프레임워크를 쓰게되면 여러 EM과 영속성 컨텍스트가 N:1관계를 가짐
- 엔티티 생명주기
- 비영속(new/transient) : 객체를 생성만 한 상태
- 영속(managed) : 객체를 저장한 상태 (em.persist)
- 준영속(detached) : 영속성 컨텍스트에서 객체를 분리한 상태 (em.detach)
- 삭제(removed) : 객체를 삭제한 상태 (em.remove)
- 영속성 컨텍스트의 이점
- 1차캐시 : key로 저장한 객체의 id를 저장하고 value로 객체 자체를 사용해서 1차캐시에 저장함
- 만약 persist한 객체를 바로 find하면 1차 캐시에서 먼저 찾아보고 있으면 반환해줌
- 1차 캐시에 없다면 DB에서 조회하고 그걸 1차 캐시에 저장한다.
- 근데, 사실 트랜잭션 내에서만 효과적이기 때문에 사실 큰 성능 개선은 이루어지지 않는다
- 뭐, 트랜잭션 내에서 같은 객체를 두 번 조회하는 경우, 첫 조회는 DB에서 찾으니 select문이 나가지만 두번째는 첫 조회에서 1차캐시에 객체를 저장하기에 select문이 나가지 않는다.
- 영속 엔티티의 동일성 보장
- 똑같은 객체를 두번 조회하는 경우 조회된 두 객체는 완전히 똑같은 객체이다.
- 즉, DB에서 고립단계가 설정되어있는 것과 별개로 그냥 애플리케이션 차원에서 REPEATABLE READ를 제공하는 것
- 쓰기지연
- 영속화 한 객체들을 트랜잭션이 커밋되는 순간에 SQL을 쏘기 때문에 쓰기지연이 가능
- 영속성 컨텍스트 내에 존재하는 쓰기 지연 SQL 저장소에 영속화 한 객체를 DB에 저장하는 SQL문을 쌓아 놓음
- 트랜잭션 커밋되면 쓰기지연 된 애들 flush
- <property name="hibernate.jdbc.batch_size" value="10"/>
- 이렇게 배치사이즈를 주면 10개 쌓이면 작업을 수행함
- 변경감지 (Dirty Checking)
- em.find로 찾은 객체를 setter로 변경하면 실제로 DB에 update문이 쏘는 행위
- find할 때 1차캐시에 사실 스냅샷이란 것도 같이 저장함
- 우리가 커밋할 떄 우리가 트랜잭션 내에서 사용한 객체와 스냅샷을 비교하고 변경점이 있다면 (check Dirty) Update 쿼리문을 쓰기지연 저장소에 추가한다
- 스냅샷은 영속성 컨텍스트에 처음으로 객체를 사용한 시점에 생성 ( persist , find ..)
- 1차캐시 : key로 저장한 객체의 id를 저장하고 value로 객체 자체를 사용해서 1차캐시에 저장함
- Flush : 알다시피 영속성 컨텍스트 변경 내용을 DB에 반영하는 것
- em.flush()로 직접 호출하거나
- 트랜잭션을 커밋해서 플러시를 자동호출하거나
- JPQL쿼리 실행해서 플러시를 자동 호출한다
- 예를들어 a,b,c를 persist하고 트랜잭션 내에서 조회하는 JPQL을 쏘는상황이라 생각하자
- 이때 DB에 실제로 insert하지 않으면 조회가 정상적으로 되지 않음
- 따라서, JPQL을 수행하기전 flush를 자동으로 먼저 수행하게 설정되어있다.
- 만약, 실행하는 JPQL이 persist한 개체와 전혀 상관없는 객체를 조회한다면 FlushModeType.COMMIT으로 설정해서 커밋할때만 플러시 되게끔 할 수 있긴 함
- 근데, 굳이 뭐 이렇게 할 필요없이 Default값인 AUTO로(커밋or쿼리 시 플러시)놔두면 됨
- 플러시 한다 해서 1차 캐시가 지워지거나 그런건 없음 그냥 더티체킹하고 쓰기지연 저장소에 쌓여있는거 배출하는 역할임
- 즉, 영속성 컨텍스트를 비우는게 아니라 영속성 컨텍스트 변경내용을 DB에 동기화 하는 것
- (1차 캐시에 올라간 상태가 영속 상태다)
- 준영속 상태
- 영속성 컨텍스트에서 영속 상태의 엔티티를 분리한 것
- detach하거나 clear close하면 준영속 상태가 됨
- detach는 특정 엔티티를 준영속 상태로 만들고
- clear는 영속성 컨텍스트 전부를 초기화 하는 것
- 똑같은 객체를 두번 조회할 때 그 사이에 clear하면 다시 select쿼리가 나감(첫번째로 조회한 객체는 clear하면서 1차 캐시에서 사라졌기 때문에 2번째 조회 때 새로 select해서 1차캐시에 저장하고 리턴하기 때문)
- close는 뭐, 애초에 영속성 컨텍스트를 종료하는 행위여서 당연히 반영이 안됨
- 커밋 전에 준영속 상태로 만들게 되면 변경내용이 반영이 안됨
- 영속성 컨텍스트에 존재하지 않기 때문 !
엔티티매핑
- @Entity : 뭐 알자나
- 기본 생성자는 무조건 필수 (public or protected) / jpa spec상 필요하기 때문
- 엔티티 클래스 내에 final, enum, inteface, inner class 이런거 사용 안됨
- @Table : 기본적으로 Member엔티티 만들면 member테이블이 생성되는데, 요구 사항이 mbr이렇게 테이블 만들어지게 하면 @Table(name=”MBR”)이렇게 하면 됨
- 그 외에 catalog,schema, uniqueConstraints를 설정할 수 있음
- DDL(Data Definition Language) : 데이터베이스의 구조와 테이블, 뷰, 인덱스, 프로시저와 같은 개체를 정의
- ddl-auto 설정하는거 생각해보면 엔티티 매핑 되어 있는 애들을 찾아서 db에 맞는 dialect를 이용해서 DB스키마를 자동으로 생성해줌
- 근데, 이렇게 생성된 DDL은 개발 장비에서만 사용해야 한다!!
- 운영장비에서 사용하기에는 안전하다고 확신할 수 없음
- 개발 초기는 create나 update 사용하고
- 테스트 서버는 update 나 validate , 근데 가급적이면 테스트 서버에서도 validate나 none이 좋다
- 스테이징,운영 서버는 validate나 none을 써야 한다.
- 잘못 추가한 내용이 alter를 잘못 수행해서 대규모 DB가 고장나버리면(락이걸리면) 서비스가 중단되는 대참사가 발생
- 직접 테이블을 만들거나 create시 생기는 SQL을 꼼꼼히 보고 다듬어서 DB에 조작해야 함
- @Column 의 속성
- name : 매핑할 컬럼의 이름을 정하는 것
- insertable, updatable : 등록,변경 가능 여부 설정 (Default : true)
- false설정 시 해당 컬럼의 삽입이나 수정이 불가능해짐
- nullable : false시 NOT NULL 제약조건이 생성됨
- unique : @Table의 uniqueConstraints와 같지만 한 칼럼에 간단하게 유니크 제약조건을 걸기 좋음
- 근데 제약조건 이름이 너무 랜덤하게 설정되어서 쓰지 않고 대게는 @Table에서 제약조건을 건다
- columnDefinition : 컬럼 정보를 직접 줄 수 있음 (ex. varchar(100) default ‘EMPTY’ 이런식)
- length : 문자 길이 제약조건, String에서만 걸 수 있음
- percision,scale : BigDecimal타입( 아주 큰 숫자)에서 사용, 즉 엄청 큰 수나 엄청 세밀한 수를 저장하는 경우 사용
- DDL기능은 뭐 말그대로 데이터 정의어니까 JPA의 실행 로직에는 영향을 전혀주지 않음
- enum 타입을 사용하고 싶으면 @Enumerated 사용
- EnumType.ORDINAL : default값, enum 순서를 데이터베이스에 저장
- 얘는, ENUM 값을 숫자로 저장한다
- 근데, 이렇게 되면 ENUM타입에 새로운 타입을 추가하고 걔의 순서를 맨뒤에 추가하는 방식으로 고정하는게 아니면 정말 큰 오류가 생길 수 있다
- 예를 들어 새로운 guest란 타입의 enum을 추가하는데 맨앞에 추가 해버리면 기존에 있던 0번인 user와 구분할 수 없게 됨
- 따라서, 그냥 무조건 STRING으로 타입 설정해주는게 좋다
- EnumType.STRING : enum이름을 데이터베이스에 저장
- EnumType.ORDINAL : default값, enum 순서를 데이터베이스에 저장
- @Temporal은 날짜 타입 매핑
- 요즘은 LocalDate,LocalDateTime 형태로 그냥 필드 만들어서 쓰면 알아서 DB에 date,timestamp타입으로 들어감
- @Lob은 BLOB, CLOB매핑
- 문자면 CLOB, 나머지는 BLOB
- @Transient는 특정 필드를 컬럼에 매핑하지 않는 것 (매핑을 무시하는 것)
- @GeneratedValue : 기본키 매핑할 떄 사용하는 어노테이션, 자동으로 생성한다는 것
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- IDENTITY : 기본키 생성을 DB에 위임 (MySQL,Postgres,SQL server, DB2)
- 직접 id를 넣으면 안된다. null로 설정하고 날리면 DB에서 그때 알아서 만들어서 추가함
- 근데, 이러면 DB에 삽입 전까진 객체의 PK를 알 수 없음
- 이러면 1차캐시에 저장할 PK을 알 수 없게 됨. 즉 1차 캐시를 사용할 수 없음
- 그래서, 이 전략을 사용하면 em.persist를 한 시점에 바로 insert쿼리를 날려버림
- 즉, 모아서 쏘는 걸 못하게 되고 insert는 바로바로 쏘게됨
- 사실, 이렇게 해도 성능에 큰 변화는 없다..
- SEQUENCE : 시퀀스 오브젝트를 통해 기본키를 넣음 (오라클, Postgres, DB2,H2)
- 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트다
- @SequenceGenerator : 이 시퀀스를 설정해서 generator 형태로 필드에서 사용할 수 있게 함
- @GeneratedValue에 generator로 등록해서 사용
- start값 증가값 설정을 따로 안하면 1부터 시작하고 1씩증가함
- persist하게 되면 영속화 직전에 sequence전략인걸 확인하고 지정한 시퀀스 오브젝트에서 next value를 얻어와서 영속화 하려는 객체의 PK로 설정하고 영속화를 한다
- allocationSize : 시퀀스를 한 번 호출할 때 증가하는 val의 수 ( 성능 최적화에 사용 된다.)
- 약간 안전범위 느낌임 처음 호출하면 시작값 부터 1+할당크기*0 까지 (즉, 1이 시작값이고 허용범위가 1이니까 다음 턴 호출)
- 가용불가능하니까 한번 더 호출 > DB는 그럼 문제가 있으니 증가 시켜달라는 것으로 간주하고 증가 크기만큼 (기본값 50) 증가시키고 seq의 val이 51이 된다
em.persist(member1); //영속화 전 next call 두 번 호출( 1 ->51) 51은 가용가능하니 메모리에 저장 em.persist(member2); // 메모리에 저장되어 있으니 2로 바로 세팅 em.persist(member3); // 마찬가지로 3으로 바로 세팅
- 이렇게 되면 next call을 자주 안불러도 되니 (50개가 다 차지 않으면 부를 이유가 없지) 성능면에서 최적화가 이루어진다.
- 동시성 문제를 해결할 수 있게된다.
- 여러 서버에서 호출해도 어떤 애는 1~50 다른애는 52? ~ 100 이런식으로 할당받기 때문에 동시성 문제를 해결할 수 있다.
- TABLE : 키 생성 전용 테이블을 만들어서 시퀀스를 흉내내는 전략
- 모든 DB에 적용가능 하지만 성능이 좋지 않음
- @TableGenerator로 엔티티에 등록하고 필드에서 시퀀스처럼 사용
- 테이블에 next_val 이 점점 증가하는 형식으로 사용 됨
- 시퀀스와 마찬가지로 allocationSize기능 존재
- AUTO : 위 3개중 하나를 Dialect에 맞춰서 선택
- 키본 키 제약조건을 정말 먼 미래까지 유지되려면 Long형으로 키 생성전략을 사용하는 대체키(랜덤값, 생성값)을 사용하는 것이 좋다.
- index나 column명이나 컬럼 길이 같은 정보는 실제 DB에 스크립트로 명시하지는 것보다 코드상에 적는 것이 바람직하다.
- 원래는 자바식 camelcase를 그대로 db에다 저장하는데, 스프링 부트를 쓰면 부트가 알아서 camelcase를 dba가 원하는! order_date로 바꿔서 저장해줌
연관관계 매핑 기초
- 그냥 외래키 자체를 값으로 저장하면 유지보수성도 떨어지고 객체지향적인 방법을 사용하기 힘들다.
- 그래서, 참조 객체를 객체그대로 저장하되 매핑을 한다
- 단방향 매핑은 뭐 말 그대로 한쪽 테이블이 일방적으로 다른 테이블을 참조하는 것
- 양방향 매핑 : 말 그대로 테이블이 서로서로를 참조하는 것
- member에서 getTeam으로 가고, Team에서 List<Member>로 가는 것
- 여기서 team은 member를 OneToMany로 참조함
- 선언할 때 List<Member> members= new ArrayList<>();이렇게 초기화하는게 관례
- mappedBy를 적어줘야 함 ( 이름으로는 자신이 Member에 매핑되어있는 변수 이름을 적음 (여기서는 team))
- 객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다
- 객체는 회원→팀, 팀→회원으로 두개가 생김
- 테이블은 회원↔팀 1개의 연관관계만 가짐
- 예를들어 멤버가 새로운 팀으로 이동하고 싶다면 멤버의 team의 값을 바꿔서 DB의 FK를 변경할 지 Team의 members를 변경해서 FK를 변경해야 할 지 애매한 상황이 생긴다
- 그래서, 이런 애매한 상황을 해결하기 위해 둘 중 하나를 주인으로 정해야 함
- 이게 바로 연관관계의 주인이고 mappedBy로 따까리를 결정함
- 그니까 따까리 객체가 mappedBy로 지정된 주인에게 말그대로 매핑된 것이다
- 즉, 주인만이 외래키를 관리하고 따까리는 읽기만 가능하다.
- 주인은 외래키가 있는 곳을 주인으로 정해야 함( Member에서 TEAM_ID 랑 매핑해서 외래키를 관리하니 얘가 주인이 되는게 맞다)
- 일대 다 관계에서 DB기준으로 FK가 존재하는 곳은 다쪽의 테이블에 존재 함.
- 그러므로, 다 쪽의 테이블에 해당하는 엔티티가 연관관계의 주인이 되는게 맞다
- 예를 들어 Team이 FK를 관리하면 Team에서 조작을하면 member테이블에서 조작을 하니 관계가 이상해 보이고 성능적 이슈도 있다.
- 가장 많이하는 실수
- 연관관계의 주인에게 값을 할당하지 않고 따까리에게만 할당하는 경우 실제로 DB에는 저장되지 않음
- 예를들어, team.getMembers().add(member)이렇게 추가하면 실제로 DB에는 반영이 되지 않음
- member.setTeam(team)이렇게 추가해야 한다 !
- 주인만 값을 설정하고 팀에서 getMembers하면 지연로딩으로 select해서 뽑아온다.
- 근데, 이렇게 양쪽이 아닌 주인만 값을 넘겨주는 경우 생길 수 있는 문제
- flush,clear를 하지 않고 team을 find하면 1차캐시에서 객체를 뽑아오고 해당 객체의 Member List는 비어있기 때문에 getMembers해도 아무것도 나오지 않게 된다.
- 테스트케이스 작성 시 jpa없이도 많이 쓰는데, 주인만 넣고 쓰게되면 결국 team.getMember를 해도 null이 나오게 됨
- 따라서, 양쪽 모두에 값을 세팅해주는게 좋다.
- 이걸 계속 체크하기 힘드니 주인쪽의 연관관계를 세팅할 때
- 즉, member.setTeam을 할 떄
- setTeam에서 team.getMembers().add(this) 이런식으로 추가하면 편하게 사용할 수 있다.
- 그리고 이런 연관 관계 편의 메소드는 중요한 메소드이니, setTeam보단, changeTeam같이 좀 중요해보이게 이름을 변경해놓는게 좋다.
- 뭐, team.addMember(member)해서 member.setTeam(this), mebers.add(member) 이런식으로 작성해도 된다.
- 대신! 둘 중 하나를 선택해서 하나만 사용해야 함 (아니면 헷갈림.. 무한루프 걸릴 수도..)
- 무한루프
- toString : toString을 만들어보면 엔티티내의 모든 변수에 대해서 tostring을 만들게 되는데 이때 member에서는 team을 가지니 team의 toString을 호출하게 됨
- 근데, 여기서 team의 toString또한 members를 사용하니 또 member의 toString을 호출함
- 즉, 이렇게 서로 부르고 부르는 무한 루프에 빠지게 된다.
- 그러므로, 롬복에서 @ToString 을 막 달아놓거나, 컨트롤러에서 객체를 반환하면 json으로 변환해서 보내는데 이때 또 toString이 불리기때문에 무한루프가 발생할 수 있게 됨( 또한, 엔티티를 api에서 직접 반환하면 엔티티를 변경하는 순간 api spec이 바뀌기 떄문에 앵간해서는 DTO로 변환해서 반환하는게 맞다 !)
- 쉽게 정리하면 롬복에서 tostring은 앵간해선 쓰지마라.
- 그리고, api에서 엔티티를 직접 반환하지말고 DTO로 변환해서 반환해라
- 단방향 매핑으로 쭉 설계를 끝내고, 양방향매핑은 그저 필요에 따라 추가하는 것 (그저 반대 방향으로 조회기능이 추가되는 것 뿐이니)
- 실시간 애플리케이션에서 특정 회원의 주문내역을 보고싶으면 order테이블을 이용해서 구하는게 맞지, Member객체에 order를 List형태로 가지고 있는 형태는 관심사를 제대로 끊어내지 못한 설계다.
- 반대로, order로 orderitem을 가지는 것은 비즈니스적으로 의미가 있다.
- !! OneToMany리스트 만들 떄 앵간해선 그냥 new ArrayList로 초기화 꼭 해주자 (삽질 조금 했음 ;)
다양한 연관관계 매핑
- 바로 이전 챕터에서 공부했듯이 테이블은 외래키 하나로 두 테이블이 연관관계를 맺고 객체는 참조용 필드가 있는 쪽으로만 참조가 가능하다
- N:1 : DB설계상 N쪽에 외래키가 가고 객체에서도 N쪽이 주인으로 외래키(따까리 객체)를 선언한다.
- 양방향으로 만들려면 뭐 반대 방향(따까리쪽)에서 OneToMany하고 mappedBy하면 됨
- 1:N : 1쪽이 연관관계의 주인인 경우 (사실 지원은 하는 기능이지만 사실 거의 사용하지 않는 기능)
- team,member관계에서 Team에서 List members를 가지고 어떤 관리작업을 하면 DB의 Member테이블의 FK인 Team_ID를 이용해서 변경함
- team, member를 각각 삽입하고,team에서 getmembers해서 add member하는 경우 insert쿼리가 두번 나가고 update쿼리가 한 번 더 나간다(Member테이블의 TEAM_ID값을 수정해야 하기 때문)
- 실무에서는, 메소드로 축약해서 많이 쓰는데 쿼리를 추적하다가 update문이 나가게 되면 이유를 찾기도 힘들고 성능상에 큰 하락은 없다만 그래도 어쨌든 비용의 증가는 생기게 된다.
- 그래서, 왠만해서는 다대일 양방향을 만들어서 쓰는게 좋다.
- 그리고, 일대다 단방향의 경우 @JoinColumn을 사용하지않으면 JoinTable을 해버림 즉, 테이블을 하나 더 만들어버린다.
- 일대다 양방향
- @JoinColumn(name = "TEAM_ID",insertable = false, updatable = false)
- 이렇게 매핑을 하는데, 갱신이랑 삽입이 안되게 즉 ReadOnly형식으로 만들어버리는 것
- 그러면 Member입장에서 Team을 조회할 수 있고, Team의 입장에서는 Member테이블의 FK를 조작할 수 있게 된다.
- 이게 공식적인 방법은 아니지만, 읽기전용필드를 사용해서 만들어내는것
- 그냥 다대일 양방향을 쓰자
- 1:1 : 주 테이블이나 대상 테이블 중 외래키를 선택이 가능함
- DB에서 외래키에 DB유니크 제약조건을 추가한 것
- Member 테이블에 LOCKER_ID를 FK로 가지고 유니크 제약조건을 두면 1:1 단방향 매핑이 된 것
- 사실, 다대일 단방향과 매우 유사하다.
- 양방향은 그냥 주인 테이블은 OneToOne하고 JoinColumn그대로 쓰면 되고,
- 따까리는 OneToOne인데 mappedBy해서 주인 지정해주면 된다.
- 만약, 외래키가 Member가 주인이 되고싶은데 Locker테이블에 외래키에 존재하는 경우 Member가 Locker테이블의 외래키를 조작할 수 없다. (일대다 단방향느낌?)
- 대신, 대상 테이블(따까리)에 외래키가 있는데 일대일 양방향인 경우 그냥 Locker에 member가 있고 얘를 연관관계의 주인이라 여기면 끝 (사실 그냥 일대일 양방향 (주인 테이블에 외래키있는경우)랑 똑같다.)
- DB입장에서 시간이 흘러 한 멤버가 여러Locker를 소유할 수 있게될 때
- Locker에 외래키가 있다면 그냥 유니크 제약조건을 없애면 된다
- Member에 있다면 Locker에 컬럼 추가(외래키)하고 Member의 FK,유니크제약조건은지워야 함
- 근데! 반대로 여러멤버가 한 라커를 쓰게 된다면?
- 그러면 멤버에 외래키가 있는게 더 좋은 방법이 됨
- 개발자의 입장에서는, Member테이블을 더 많이 조회하는 경우 멤버테이블에 locker외래키를 만드는게 더 좋다
- 주 테이블(많이 쓰는 테이블, Member)에 외래키를 두는 경우
- 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
- 대신, 값이 없으면 외래 키에 null을 허용하게 된다
- 대상 테이블에 (Locker) 외래키를 두는 경우
- 전통적인 DB 개발자가 선호
- 주 테이블과 대상 테이블을 일대일에서 일대다로 변경할 때 유리
- 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩이 된다.
- JPA가 멤버를 불러올 때 주테이블에 외래키가 있는 경우 라커를 사용하기 전까진 걍 놔두고(지연 로딩 사용시) 나중에 사용하면 그냥 값이 있는지 없는지만 보면끝(프록시 사용)
- 근데, 두번째 경우 Member테이블만 보고 locker의 유무를 확인할 수 없기에 Locker 테이블에 쿼리를 날려 확인해야 하기 때문에 어쩔 수 없이 확인하게 되므로 프록시를 사용하지 않는다 (즉시로딩을 한다)
- M:N : 실무에서는 절대 사용하지 않는 매핑이다.
- RDB는 테이블 2개를 다대다 관계로 표현할 수 없다
- 그래서 연결 테이블을 추가해서 일대다 다대일 관계로 풀어야 함
- ManyToMany매핑으로 연결테이블을 자동으로 만들어내고 다대다 매핑으로 두 객체를 이용할 수 있긴 하지만
- 만약 연결테이블에 추가할 정보가 있다면 해당 방법으로는 추가할 수 없게 된다.
- 그리고, 연결테이블의 pk는 외래키의 조합으로 pk를 두는 것 보단, 그냥 generateValue로 따로 연결테이블의 pk를 만들어주는게 좋다.( 나중에 유지보수에 편리)
고급 매핑
- 상속관계 매핑이 RDB에는 존재하지 않음
- 그나마 슈퍼타입, 서브타입 관계라는 모델링 기법이 객체의 상속과 유사하다
- 그래서, 상속관계 매핑이라 하면 객체의 상속과 DB의 슈퍼타입 서브타입 관계를 매핑하는 것이다.
- 이걸 DB에서 구현하는 방법으로는
- 조인 전략 : 자식 객체를 각각 테이블로 변환하는 것
- 부모 테이블의 PK를 자식테이블이 PK로 가진다(즉, PK이자FK), 부모 테이블에 자식테이블의 Type을(종류를) 저장하는 컬럼을 둔다(참조?)
- 단일 테이블 전략 : 그냥 한 테이블에 컬럼들을 다 넣어놓고 DataType칼럼을 넣어 구분하는 것
- 구현 클래스마다 테이블 전략 : 부모의 컬럼 (ex, name,price)를 자식 테이블이 각각 자신의 컬럼으로 추가하고 부모의 PK를 자신의 PK로 가지는 것
- 조인 전략 : 자식 객체를 각각 테이블로 변환하는 것
- JPA에서 그냥 상속해서 만들게 되면 기본적으로 단일 클래스전략으로 만들게 된다.
- 당연히 부모 클래스는 abstract class여야 함(상속 개념 생각해보셈)
- 엔티티에 @Inheritance어노테이션을 추가하여 구현 한다
- 부모의 id(item id)를 다 PK로 여기기에 (물론 단일 테이블은 PK가 딱히 없긴 하지만..) 자식 클래스에선 id를 정의할 필요도 해서도 안됨 !
- @DiscriminatorColumn
- 이걸 또 적어주면, DTYPE이라는 컬럼을 만들고 해당 Item의 타입(Movie,Album,Book)을 알아서 명시해준다.( default가 컬럼이름이 DTYPE이고 들어오는 값은 엔티티이름으로 설정되어 있음)
- 만약 뭐, DTYPE말고 DATATYPE이라 쓰고 싶으면 그냥 어노테이션 뒤에 name=””으로 지정하면 됨
- Type의 이름을 변경하고싶으면 @DiscriminatorValue("AA") 이런 식으로 자식 엔티티에 어노테이션을 달아 원하는 이름을 설정하면 된다.
- @Inheritance(strategy = InheritanceType.*JOINED*) :조인 테이블 전략
- 이렇게 상속관계 생성 전략을 설정할 수 있음
- 이렇게 하고 Movie객체를 만들어서(Item을 상속받는) 저장하게 되면, Item테이블에 먼저 저장하고 Movie테이블에 정보를 저장한다.
- 조회하는 경우 JPA가 알아서 조인해서 정보를 가져와줌
- 조인 전략을 많이 사용하긴 하지만, 이런 상속관계가 정말 좀 단순하고 복잡할 이유가 없는 경우 단일 테이블 전략을 많이 채용하기도 함
- 장점
- 테이블이 정규화 되어 있다
- 외래키 참조 무결성 제약조건을 활용가능하다
- 다른 테이블의 입장에서는 부모 객체(Item)만 조회해도 된다
- 저장공간이 효율적이게 된다
- 단점
- 조회시 조인을 많이 사용하기에 성능이 저하되고, 복잡하다
- 데이터 저장시 삽입 쿼리가 2번나감
- 근데, 이런 부분은 별로 단점이 되지 않지만 복잡하다는 점은 단점이 맞긴 함
- 단일 테이블 전략 @Inheritance(strategy = InheritanceType.*SINGLE_TABLE*) (or) 그냥 전략 설정 x
- DisciminatorColumn 설정 안해도 자동으로 설정 됨
- 성능 면에서 우수하고 단순하다.
- 자식 객체를 저장할 때 쿼리가 한번만 나가면 되고
- 조회시에도 조인 없이 한번만해도 된다.
- 장점
- 조인이 없으니 성능이 좀 좋고 단순하다
- 단점
- 자식 엔티티가 매핑한 컬럼들 (뭐, 영화의 감독 이런것들) 모두 null을 허용해야 함
- 무결성을 고려하면 안좋을 수 있다
- 테이블이 커질 수 있고, 상황에 따라 성능이 조인방식에 비해 느려질 수도 있다.(거의 없긴 함)
- 자식 엔티티가 매핑한 컬럼들 (뭐, 영화의 감독 이런것들) 모두 null을 허용해야 함
- 만약 JPA를 안쓰고 조인전략에서 단일 테이블 전략으로 변경하는 경우 매우 시간이 많이 들지만, JPA는 그냥 테이블 전략만 JOINED에서 SINGLE_TABLE로 바꾸면 끝
- @Inheritance(strategy = InheritanceType.*TABLE_PER_CLASS*)
- Item테이블이 생성되지 않고 값을 넣은 테이블 하나만 조회한다
- 근데, 만약 부모객체를 조회한다면 자식 객체를 전부 뒤져서 찾아보게 된다.
- 비추하는 전략
- 장점
- 서브타입을 명확하게 구분할 때 효과적
- Not null제약조건 사용이 가능 함
- 단점
- 뭔가 연관되어서 묶어놓은 것들이 없음→ 가격 정산 하는 상황에선 모든 테이블을 다 조회하고 뭔가 추가되는 경우 관리하기 힘들어짐
- 여러 자식 테이블을 함께 조회하는 경우, 수정하는 경우 매우 쿼리가 어려워 진다.
- MappedSuperClass : 상속관계 매핑과는 별 관계가 없다.
- 그냥 객체에서 id,name이란 필드가 계속 나오는 경우 그냥 이걸 공통 속성으로 여기고 이걸 부모에 그냥 박아두고 상속받아서 사용하려는 것
- 그냥 어떤 클래스에 공통으로 사용할 컬럼들을 적고 @MappedSuperclass 박아준 후(물론 게터 세터도 있어야 함) (그리고, 컬럼에 이름 박아줄 수 있음)
- 해당 공통 컬럼이 필요한 엔티티에서 extends해서 사용한다.
- 상속관계 매핑이 아니다.
- 엔티티가 아니기에 테이블과 매핑도 되지 않는다.
- 그냥 자식 클래스에 매핑 정보만 제공하는 것, 조회,검색도 불가능하다
- 직접 해당 클래스를 테이블로 만들 일이 없기에 추상 클래스로 만드는 것을 권장한다.
- 주로, 등록일 수정일 등록자 이런 공통 으로 적용되는 정보를 모을 때 사용함
- @Entity클래스는 같은 엔티티나 이렇게 MappedSuperClass만 상속이 가능하다.
- 편하게 쓰려고 만드는 것 !
#ManyToMany로 한 중간 테이블은 당연히 extends를 박을 수 없으니 이런 상속을 사용하지 못함.
근데, 애플리케이션이 좀 많이 커지게 되면,, 상속관계가 있는 것 조차도 상당히 복잡해진다.
뭐, json으로 말아 넣으신다는디.. ㅋㅋ
프록시와 연관관계 정리
- 우리가 회원을 조회할 때 매번 Team을 함께 조회해야 하나 ?
- 프록시
- JPA에서는 em.find도 있지만 em.getReference도 있다
- em.find는 DB를 통해 실제 엔티티 객체를 조회함
- em.getReference는 DB조회를 미루는 가짜(프록시) 엔티티 객체를 조회한다.
- em.getReference로 객체를 찾아 온 경우, id를 가져오는건 이미 찾을 때 사용하니까 알 수 있지만, 실제 객체의 정보 ( username ..)을 사용하는 경우 JPA가 실제 DB에서 값을 들고와서 채워 넣는다.(실제 쿼리가 나가게 된다)
- getClass해보면
- findMember = class org.example.entity.Member$HibernateProxy$l59ckOmN
- 이렇게 나오게됨 → 뭔가 Hibernate가 만든 이상한 놈인거지
- 껍데기만 있고 안에 id만 가지는 객체가 반환되는 거임
- 실제 엔티티를 상속받아서 만들어지는 것(hibernate가 하는 거임)
- 그러니, 사용하는 입장에선 실제 여부를 구분하지 않고 사용해도 된다 (이론상으로는)
- 프록시 객체는 실제 객체의 참조(target)을 보관한다
- 유저가 실제 객체의 값을 얻으려고하면 (call getName)
- 프록시 객체가 영속성 컨텍스트에 초기화를 요청하고(진짜 가져와!)
- 영속성 컨텍스트에서 DB에서 실제 객체를 찾아서 만들어 줌(진짜 여기..)
- 그럼 프록시 객체가 진짜 객체의 메소드를 호출해서 받은 값을 사용자에게 돌려주는 것
- 한 번 찾으면 당연히 target에 실제 객체를 연결했으니 뒤 부턴 쭉 사용가능
- 특징
- 처음 사용할 때 한 번만 초기화 한다 (바로 위에 말한 것 처럼 ..)
- 초기화시 프록시 객체가 실제 엔티티로 변경되는 것이 아니다
- 이것도 뭐 위에 말했듯이 프록시 객체가 target을 연결해서 실제 객체한테 값을 받아 그 값을 유저(클라이언트)에게 전달해주는 것 프록시 객체가 사라지거나 변경되는게 아님
- 원본 엔티티를 상속받는 객체이므로, 타입 체크시 ==비교는 안된다 ! instance of를 사용해야 함
- 그냥 타입 비교는 어떤 상황이든==으로 하는게 아니라 instance of로 해야 함
- 복잡한 로직에서 메소드 비교하는 로직이 섞여있는 경우 파라미터로 프록시 객체가 들어올지 실제 객체가 들어올 지 모르니깐 !
- 만약, 영속성 컨텍스트에 (1차캐시안에) 실제 찾는 엔티티가 존재하는 경우 getReference를 해도 실제 엔티티가 반환됨 (프록시 객체가 아님!)
- 이미 1차캐시에 존재하는데 굳이 가짜 객체를 만들어 줄 이유가 없다
- 그리고 JPA는 한 트랜잭션 내에서 RepeatableRead를 보장해주는데, 이미 찾았던 객체를 다른 방식으로 찾는다고 해서 다른 객체를 주면 ==비교나 뭐 그런게 안된다
- 여기서 좀 재밌는게, 만약 proxy객체를 먼저 만들면 그 후에 find로 실제 객체를 찾아도 프록시 객체를 반환함(1차캐시에 프록시 객체가 있으니)
- 반대로, find를 먼저하면 getReference해도 실제 객체를 반환한다.
- 근데 Find하면 DB에 select쿼리는 나감 ㅋㅋ
- 만약, 준영속 상태에서 프록시를 초기화 하면 문제가 발생한다
- org.hibernate.LazyInitializeException 예외 터트림
- proxy객체를 가져온 후 detach나 뭐 close, clear로 준영속 상태로 만들고 실제 데이터를 불러온다면(초기화를 한다면) 오류가 발생
- emf.getPersistenceUnitUtil().isLoaded(m1)
- 엔티티 매니저 팩토리에서 PersistenceUnitUtil의 isLoaded로 해당 프록시가 초기화 되었는지 확인 할 수 있음
- member.getName이렇게 부르면 강제 초기화가 되는 거긴 하지만..
- Hibernate.*initialize*(m1); 이렇게 강제 초기화를 시킬 수 있다.
- JPA표준이 아니라 그냥 Hibernate가 지원하는 스펙임.
- 다시한번 생각해보자, 회원을 조회할 때 팀도 매번 조회해야 하나?
- 멤버만 사용하는 로직에선 그럴필요가 없지 !
- 이럴 때 사용하는게 지연로딩
- @ManyToOne(fetch = FetchType.*LAZY*)
- 이렇게 하면 Team객체를 Proxy객체로 만들어서 주게 됨
- 즉, em.find해서 멤버를 찾아올 때 Team테이블을 join하지 않음
- 팀이 프록시 객체니, member.getTeam.getName뭐 이렇게 초기화를 하는 시점에 쿼리가 발생
- 만약 반대로, 멤버를 조회할 때 팀을 자주 같이 조회하는 로직인 경우
- @ManyToOne(fetch = FetchType.EAGER)
- 처음에 바로 같이 조인해서 데리고 오는 것
- 뭐, 구현하는 방식으로는 대부분 처음 조회 시 테이블 조인해서 쿼리 하나로 정보를 다 가져오긴 함, 근데 뭐 멤버 정보만 불러오고 다시 거기다가 팀 정보도 불러오는 두개의 쿼리로 만들 수도 있긴 하지만 굳이..
- 주의 사항
- 가급적 지연 로딩만 사용하는게 좋다
- 즉시로딩을 사용하면 예상치 못한 SQL이 발생하고 특히, JPQL에서 N+1문제를 일으킨다
- JPQL에서 select문을 날려보면 쿼리가 두번나가는걸 볼 수 있음
- 즉, select all member하는 쿼리를 날리면 멤버만 가지고 오라는 뜻이니 번역 후 그냥 Member정보만 멤버 테이블의 갯수만큼 가져옴
- 근데, EAGER로 설정되어 Team객체도 데리고 와야 되면 다시 쿼리를 한번 쏴서 해당 멤버마다 Team을 붙여줌
- 여기서, 만약 회원이 두명이 있고 소속된 팀이 다르다면
- 회원을 쭉 조회하고
- 회원1의 팀을 찾는쿼리
- 회원2의 팀을 찾는 쿼리
- 이렇게 3개의 쿼리가 나감
- 따라서, 최초 멤버를 찾는 쿼리 1개와 N개의 탐색쿼리가 발생하는 문제를 N+1문제라고 한다
- 뭐 두명에서 두팀인 경우는 2+1인거지..
- 해결방안(나중에 배우게 됨)
- 일단 무조건 지연로딩으로 설정 한다.
- 그 후, Fetch Join (동적으로 원하는 애를 데리고오는 것)
- 나중에 나옴
- LAZY해도 멤버랑 팀을 전부 다 가져옴
- 루프를 돌리든 뭘 하든 이미 값이 다 채워져있는 실제 객체이므로 쿼리가 더 안나감
- 혹은, @EntityGraph
- 뭐, BatchSize로도 해결.. (1+1)
- ManyToOne, OneToOne은 default가 즉시로딩이니 지연로딩으로 설정하는게 좋다
- OneToMany,ManyToMany는 default가 지연로딩임
- 그냥 가급적도 아니다 모든 관계에 지연 로딩을 걸고 다른 문제는 fetch join, entity graph 같은 걸 써서 해결하는게 맞다
영속성 전이
- 특정 엔티티를 영속화 할 때 연관된 엔티티도 함께 영속화 하고 싶을 때 사용
- 부모를 저장할 때 자식도 함께 저장하는 것
- OneToMany안에 cascade옵션을 주는 것으로 설정을 하는 것
- 영속성 전이는 연관관계 매핑과 아무 관련이 없다
- 그저 엔티티를 영속화 할 때 연관된 엔티티도 함께 영속화 하는 것
- 옵션
- ALL : 라이프 사이클을 전부 맞출 때
- PERSIST : 저장 할 때만,,
- REMOVE : 삭제할 때만, 근데 위에 두개만 주로 쓰고 안 쓴다.
- 게시판과 첨부파일,댓글 같은 경우 게시판에서만 관리되는 것이니 사용 가능하지만
- 다른 곳에서도 관리된다면 사용하면 안된다.
- 즉, 단일 엔티티에 완전히 종속적 인 경우, 그러니까, 라이프사이클이 완전히 동일한 경우
- 예를들어, 멤버 객체가 또 child list를 가지는 경우는 사용하면 안된다
- 고아객체
- 부모객체와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능
- parent.getChildList().remove(index)하면 해당 자식 객체와 부모의 관계가 끊어지는 것이고 orphanRemoval=true 로 설정해 놓으면 이 때 자식 객체를 삭제하는 쿼리르 날려버린다.
- 참조하는 곳이 하나일 때 사용해야 한다.
- 즉, 부모만 자식을 참조하는 경우만 사용해야 한다.(개인소유 하는 경우에만 사용해야 한다)
- 부모 엔티티 제거 시 cascade옵션을 안주고 orphanremoval True로 해놓은 상태면 부모 지워지면서 부모의 childList도 날라가니 자식 객체도 다 사라지게 된다.
- 영속성 전이랑 고아객체를 다 키는 경우
- 부모 엔티티를 통해 자식의 생명주기를 관리할 수 있다
- 도메인 주도 설계 (DDD)의 Aggregate Root(이게 부모객체) 개념을 구현할 때 유용하다.
값 타입
- JPA의 데이터 타입 분류
- 엔티티 타입
- @Entity로 정의하는 객체
- 데이터가 변해도 식별자(id)로 지속해서 추적 가능
- 뭐, 회원의 이름, 키 이런거 변경해도 식별자로 인식 가능
- 값 타입
- int,Integer,String같은 애들
- 식별자가 없으니 추적 불가능
- 엔티티 타입
- 값 타입의 분류
- 기본값 타입
- 자바 기본 타입(primitive type) (int,double ..)
- 얘는 애초에 절대로 공유 되지 않는다.
- 애초에 다른 변수에 값을 넣어도 공유가 되는게 아닌 그 시점의 값을 복사하는 것이니..
- wrapper 클래스(Integer,Long..)
- 얘는 공유가 됨, 근데 어차피 변경이 안됨 a.setValue이런 기능이 없으니..
- String
- 얘도 공유가 됨, 얘도 마찬가지로 변경은 안됨
- 자바 기본 타입(primitive type) (int,double ..)
- 임베디드 타입(복합 값 타입)
- 좌표같이 복합적으로 주로 기본 값 타입을 모아서 커스텀하게 만들어서 쓰는 것
- int나 String처럼 얘도 “값 타입” 이다 → 추적 불가능
- 그냥 뭐 클래스 뽑아서 그안에 필드 넣는 그런 놈이지
- @Embeddable : 값 타입을 정의하는 곳에 표시
- Period 클래스 에 다는 거지
- @Embedded : 값 타입을 사용하는 곳에 표시
- Member안에 period 필드 위에 다는 거지
- 근데, 뭐 생략해도 되긴 하지만 달아주는게 좋다 !
- 재사용성이 좋고 높은 응집도를 가지며, 어떤 값 타입만 사용하는 의미 있는 메소드를 만들 수 있고 얘도 값 타입이니 자신을 소유한 엔티티에 생명주기를 의존함(당연하지)
- 예를 들어 city위에 @Column(length=10) 이렇게 제약 조건을 걸거나
- public String fullAddress(){ return “~~~”;} 이런 메소드를 만들 수 있음
- 그냥 DB의 Member에 (name, startdate,enddate,city,street,zipcode)라는 애트리뷰트가 있으면 이걸 name, Period(startdate,enddate), Address(city,street,zipcode) 이렇게 만드는거
- 임베디드 타입 사용하기 전과 후의 테이블의 입장에서 차이는 없음
- 근데,이렇게 사용하면 객체와 테이블을 아주 세밀하게 매핑할 수 있게 된다( 임베디드 타입 안에 여러 메소드를 정의해서 사용하기 용이해짐)
- 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수 보다 클래스의 수가 더 많다 (이런 값 타입이 많다)!
- 근데 뭐, 현업에서 진짜 도배때리는 수준으로 많은 건 아니지만, 사용하면 관리하기 용이해지겠다 싶은 부분에 사용한다고 함
- 값 타입 안에 다른 엔티티를 넣을 수도 있음 (연관 없는)
- 그냥 외래키 저장하는 것 뿐이니
- 만약, member안에 address를 두 개 사용해서 집주소와 직장 주소를 저장하고 싶다면?
- 이렇게 사용하면 컬럼이 따로 또 만들어짐
- @AttributeOverrides({ @AttributeOverride(name = "city",column = @Column(name="WORK_CITY")), @AttributeOverride(name = "street",column = @Column(name="WORK_STREET")), @AttributeOverride(name = "zipcode",column = @Column(name="WORK_ZIPCODE")) })
- 컬렉션 값 타입
- 자바 컬렉션에 기본 값이나 임베디드 값타입 넣어서 쓰는것
- Member에 Set<String> 이나 뭐 List<Address> 이렇게 추가하는 것
- DB에는 컬렉션 개념이 없으니 값 타입의 모든 값과 Member의 PK를 다 묶어서 PK로 하는 테이블을 만들어 내야 한다.
- 이걸 해주는 어노테이션이 @ElementCollection, @CollectionTable 이다
- @ElementCollection : 값 타입 컬렉션을 사용하겠다.
- @CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID")) : 컬렉션 테이블의 이름 및 join기준을 설정
- 원래 임베디드 값 타입은 @Column으로 이름 지정했는데, Set<String>이렇게 단일 칼럼은 예외적으로 Set<String>위에 칼럼 어노테이션을 허용한다.
- 저렇게 만들어 놓으면 DB에는 그냥 FAVORITE_FOOD라는 테이블이 생기고 거기에 회원ID랑 FoodName을 PK로 가지게 된다.
- 여기서 주의해야 하는게 얘도 여전히 값 타입임. 그러니까, 소속된 엔티티와 생명주기를 완전히 똑같이 한다.
- 이건 영속성 전이(cascade.all) + 고아객체 제거 기능을 기본으로 탑재한다고 생각해도 된다.
- 조회
- 컬렉션 값 타입은 기본적으로 지연로딩
- 나머지, 임베디드나 기본 값 타입은 즉시로딩
- 수정
- 값 타입 객체는 불변객체로 만들어야 하기에, setter이용하는게 아니라 새로운 객체를 만들어서 바꿔 끼워야 한다.
- 컬렉션도 마찬가지 remove하고 새로운 객체 만들어서 add해야 함
- 임베디드 값타입으로 이루어진 컬렉션의 경우 equals,hashcode정의 하고 remove(완전히 똑같은 값을 가진 객체)하면 컬렉션 내의 해당 객체가 삭제 됨
- 근데 여기서 컬렉션 값 타입에서 값 타입이 기본 값 타입인 경우 삭제한 객체에 대해 delete쿼리가 나가고 추가한 객체에 대해 insert쿼리가 나가지만
- 임베디드 타입 인 경우, 해당 객체 하나를 삭제해도 delete쿼리가 해당 컬렉션 테이블 전체를 대상으로 삭제를 해버리고, 다시 add한 객체 + 남아있는(삭제되지 않은) 컬렉션의 객체들 만큼의 insert쿼리가 나가게 된다
- 이유 : 값 타입 컬렉션의 제약 사항
- 값 타입은 엔티티와 다르게 식별자가 없기에 추적이 불가능함
- 그러니까, DB에서 특정 객체만 추적해서 수정하기 힘들다.
- 그래서, 그냥 해당 테이블 값들을 전부 삭제 하고 수정하려는 객체를 포함해서 기존의 객체들을 전부 다 다시 추가함
- 뭐, @OrderColumn이걸로 할 수 있긴 한데, 이것도 좀 복잡하다
- 차라리, 이렇게 변경하려면 값 타입 컬렉션 대신 일대다 관계를 사용하는 것이 낫다
- 그냥, Address라는 임베디드 값 타입을 Member에서 컬렉션 값 타입으로 쓰는 게 아니라
- 임베디드 값 타입 Address를 가지고 식별자 id를 가진 AddressEntity로 만들어서 쓰는 것
- 대신, 이 상황에선 일대다 단방향 매핑(Member가 주인)+cascade.ALL+orphanRemoval을 사용
- 단반향 매핑이니 AddressEntity를 삽입하면 insert문이 나가고 그 이후 Member의 id를 FK로 넣어줘야 하니 update문도 한번 나가게 된다
- 그렇다면 언제 값 타입 컬렉션을 써야 할까?
- 정말정말 단순한 상황
- 피자,치킨,족발 이런걸 check하는 경우 저 음식들을 컬렉션에 담는 경우
- 이럴때만 값 타입 컬렉션을 사용하고 대부분의 상황에서는 엔티티로 사용한다.
- 자바 컬렉션에 기본 값이나 임베디드 값타입 넣어서 쓰는것
- 기본값 타입
- 값 타입은 복잡한 객체 세상을 단순화 하려고 만든 개념, 그러므로 값 타입은 단순하고 안전하게 다룰 수 있어야 한다
- 만약, 임베디드 값 타입을 여러 엔티티에서 공유하는 경우 , 생각치 못한 위험이 생길 수 있다
- ex) member1 과 member2가 같은 address(임베디드 값타입)을 공유하는데 member1.getAdress().setCity(”newCity”)하면 member1만 바뀌는게 아니라 member2도 바뀜
- 만약 이런걸 공유해서 사용하고 싶다면 값 타입이 아닌 엔티티 타입으로 만들어 공유해야 한다.
- 만약 이렇게 주소라는 값 타입을 공유하고 싶다면 실제 인스턴스인 값을 공유하는게 아닌, 값을 복사해서 사용해야 한다
- address copy = new address(homeAddress.getCity(), homeAddress.getStreet(), homeAddress.getZipcode());
- 이렇게 하면 member1에 할당된 homeAdress의 값을 바꿔도 member2는 바뀌지 않는다
- 근데, 누군가 실수로 복사한 놈을 넣지 않고 member1의 주소를 그대로 member2의 주소로 설정하는 경우를 애초에 막는것이 불가능하다 !
- primitive type처럼 값이 복사되는 게 아니라 참조 값을 복사해서 넣기 때문
- 그렇기에 객체의 공유 참조를 피할 방법은 없다
- 때문에, 값 타입을 불변 객체로 설계해야 한다.
- 즉, 생성 시점(뭐 생성자 정도)이후에 값을 절대로 변경할 수 없게 만들어야 한다.
- 생성자만 만들고, setter를 만들지 않아야 한다.(Integer,String처럼)
- setter를 private으로 만들어서 컴파일러 레벨에서 부작용 체크를 가능하게도 할 수 있음
- 새로운 값으로 변경하고 싶으면 그냥 Address라는 값 타입 객체를 통으로 새로 만들어서 바꿔 끼워야 한다.
- 만약, 임베디드 값 타입을 여러 엔티티에서 공유하는 경우 , 생각치 못한 위험이 생길 수 있다
- 값 타입의 비교
- 인스턴스가 달라도 그 안의 값이 같으면 같은 것으로 봐야 한다
- int a=10, int b=10이면 a는 b와 같다
- 근데, 객체를 만들어서 비교를 하면 참조 값으로 비교하니 false가 뜬다.
- 동일성(identity)비교: 인스턴스의 참조 값을 비교 ( == 사용)
- 동등성(equivalence)비교 : 인스턴스의 값을 비교 (equals비교)
- 값 타입은 동등성 비교를 해야 하는데, equals를 적절하게 재정의 해야 한다
- 기본 설정이 ==비교라서 재정의 안하면 동일성비교가 돼버림
- 뭐 generator에 equals,hashcode 생성해서 사용하면 됨 (앵간해서 기본 값 그대로)
- 그리고, equals비교 시 hashcode는 항상 있어야 한다는 것
- 뭐, 현업에서 그렇게 많이 사용할 일은 없지만,, 그래도 알아두는게 좋다
- 인스턴스가 달라도 그 안의 값이 같으면 같은 것으로 봐야 한다
- 값 타입이 실제로 자주 사용되진 않지만, 값 타입은 정말 값 타입이라 판단될 때만 사용하자
- 엔티티와 값 타입을 혼동하지 말자
- 지속적으로 값을 추적, 변경해야 한다면 그건 엔티티로 만들어야 한다
- 그리고 equals, hashcode만들때 usegetter 체크박스 있는데 체크 안하면 직접 값에 접근하게 됨
- 근데 프록시를 사용하게 된다면 이게 정상적으로 접근이 되지 않음 (프록시는 알다싶이 직접 get해서 부르는게 아니면 값이 존재하지 않으니까) 그러므로, 앵간해선 getter쓰게 체크해줘서 만들자
객체지향 쿼리 언어 1- 기본 문법
- JPA는 다양한 쿼리 방법을 지원함
- JPQL : SQL과 유사, 뭐 select, where groupby having join 다 지원
- 엔티티 객체를 대상으로 쿼리
- SQL을 추상화해서 특정 DB SQL에 의존하지 않는다
- 객체 지향 SQL이라 봐도 된다
- 사실 이거로 거의 대부분을 사용할 수 있긴 함
- JPA Criteria
- JPQL의 쿼리는 단순한 문자다 → 동적 쿼리를 만들기 어렵다 ( 스트링 다 자르고 더하고 해야 함)
- 자바 표준에서 지원하는 문법, 자바 코드로 JPQL을 쓸 수 있고, 오류 찾기도 쉽다
- 근데 이게 너무 복잡하고 실용성이 없다 →QueryDSL을 써라!
- QueryDSL
- 얘도 자바코드로 JPQL작성 가능
- 오픈소스 라이브러리임(레퍼런스 document잘되어 있다)
- JPQL만 제대로 다루면 얘는 문서만 봐도 따라할 수 있음
- 컴파일 시점에 문법 오류 똑같이 찾을 수 있음
- 동적쿼리 작성이 매우 편리함 !
- 단순하고 쉽다 !
- 실무에서 사용을 권장한다 !
- Native SQL
- JPA가 제공하는 SQL을 직접 사용하는 기능
- 즉, JPQL로 해결할 수 없는 특정 DB에 의존적인 기능
- 뭐 오라클의 connect by이런거
- 그냥 진짜 DB 생 쿼리 문자열로 짜서 em.createNativeQuery하면 됨
- 뭐 네이티브로 짤 때 이걸 쓰던가 JdbcTemplate을 사용하기도 한다고 함
- JDBC API 직접 사용, MyBatis, SPringJdbcTemplate
- JPA를 사용하면서 JDBC커넥션을 직접 사용하거나 스프링 JdbcTemplate, 마이바티스등을 함꼐 사용가능
- 대신, 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야 한다.
- 뭐 JPA를 통한 쿼리가 아닌 우회된 SQL을사용 사용하는 경우 JPA가 자동으로 쿼리 날리기전에 플러시를 하지 않기때문에 영속성 컨텍스트를 수동으로 플러시 해주어야 함
- JPQL : SQL과 유사, 뭐 select, where groupby having join 다 지원
- JPQL (Java Persistence Query Language)
- JPQL은 SQL을 추상화 하여 특정 DB에 의존적이지 않고, 결국 SQL로 변환되는 것
- 엔티티와 속성은 대소문자 구분을 한다(객체의 값 들, Member, age)
- JPQL 키워드는 대소문자 구분을 하지 않는다 (select, from ..)
- from절에 쓰는 이름은 엔티티의 이름임
- 별칭은 필수다 (as는 생략가능) select m from Member as m
- 뭐 집계함수 집단함수 다 가능
- TypeQuery : 반환 타입이 명확할 때 사용
- em.createQuery(”~~”,Member.class)
- Query : 반환 타입이 명확하지 않을 때 사용
- em.createQuery(”select m.username, m.age from Member m”)
- 뭐, getSingleResult는 정확히 하나 인 경우만 제대로 반환하고 결과가 없으면 Noresult, 둘 이상이면 NonUnique 예외를 던짐
- getResultList는 결과 없어도 그냥 빈 리스트 반환
- Spring Data Jpa에선 findOne하면 Optional반환( getSingleResult해서 Try catch로 Optional로 반환 해줌)
- 파라미터 바인딩
- 이름기준 : select m from Member m where m.username=:username .setParameter(”username”,usernameParam)
- 위치기준 : select m from Member m where m.username=?1
- 위치는 말그대로 위치니까 순서가 바뀌면 파라미터 값이 꼬일 수 있다
- 그니까 앵간하면 이름기준으로 작성합시다
- .setParameter(1,usernameParam)
- 프로젝션
- Select절에 조회할 대상을 지정하는 것
- 뭐, 엔티티, 임베디드,스칼라타입을 대상으로 삼을 수 있음
- 엔티티 프로젝션으로 꺼내온 객체들은 전부 영속성 컨텍스트에 반영이 됨
- 임베디드 타입 프로젝션은 자신이 속한 엔티티로부터 불러와야한다는 한계
- 스칼라타입은 createQuery("select m.username, m.age from Member m") 이렇게 쓴다
- 값은 어떻게 받아야 할까?
- 얘는 TypeQuery가 아님
- 그래서 그냥 Object List를 반환함
- List resultList1 = em.createQuery("select m.username, m.age from Member m").getResultList(); Object o = resultList1.get(0); Object[] objList= (Object[]) o; System.out.println("objList[0] = " + objList[0]); System.out.println("objList[1] = " + objList[1]);
- 리스트의 제네릭에 그냥 Object[]를 넣으면 한 줄 줄일 수 있음
- List<Object[]> resultList1 = em.createQuery("select m.username, m.age from Member m").getResultList(); //Object o = resultList1.get(0); //Object[] objList= (Object[]) o; Object[] objList = resultList1.get(0); System.out.println("objList[0] = " + objList[0]); System.out.println("objList[1] = " + objList[1]);
- new명령어로 조회하기 ←이게 제일 깔끔한 방법
- select new 패키지명(조회할 컬럼) from 엔티티 약어
- 이렇게 조회하면 그냥 DTO형태로 뽑아짐
- 대신, 순서와 타입이 일치하는 생성자가 필요하고 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
- em.createQuery("select new org.example.MemberDTO(m.username, m.age) from Member m", MemberDTO.class).getResultList();
- 페이징
- JPA는 페이징을 setFirstResult(조회 시작 위치), setMaxResult(조회할 데이터 수) 두 개의 API로 추상화 한다
- 설정한 dialect맞는 paging쿼리를 알아서 만들어 냄 (뭐 h2,mysql이런건 limit offset, oracle은 rownum)
- 조인
- 내부조인 : select m from Meber m (inner) join m.team t
- 외부 조인 : select m from Member m left (outer) join m.team t
- 세타 조인 : select count(m) from Member m,Team t where m.username=t.name
- ON절을 활용한 조인
- 조인 대상을 필터링 하거나, 연관관계가 없는 엔티티를 외부 조인 할 수 있다.
- 옛날에는 연관관계가 없으면 내부조인만 가능했음
- select m,t from Member m left join m.team t on t.name=’A’
- SQL: SELECT m.,t. FROM MEMBER m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name=’A’
- select m,t from Member m left join Team t on m.username=t.name
- 연관관계가 없는 경우도 on을 이용해 leftjoin이 가능하다 !
- 서브쿼리
- 내부쿼리랑 같은 말이지 where절에 또 (select)이런 느낌
- 뭐 지원 함수는 exist, all , any ,some, in 이런거 있지
- any,some은 같은 의미
- ex)
- JPA표준 스펙에선 where,having에서만 사용가능
- 근데, 하이버네이트에서 select절에서도 사용가능하게 함
- from절의 서브쿼리는 JPQL에서 불가능했지만 .. ! 하이버네이트6에서 from절의 서브쿼리를 지원함
- 타입 표현
- 문자 : Hello = ‘Hello’
- 숫자 : 10L, 10D ..
- Boolean : TRUE, FALSE
- ENUM : 패키지명 포함해서 작성, com.example.MemberType.Admin
- 엔티티 타입 : TYPE(m)=Member (엔티티 상속 관계에서 사용) (Item- Book)
- 기타 표현
- 뭐, exist, in, and, or, not, 대소비교, between, like, is null 다 됨
- 그냥 표준 SQL은 다 지원한다고 생각하면 됨
- Case식
- 기본 case식 : select case when m.age≤10 then ‘학생요금’ when m.age≥60 then ‘경로요금’ else ‘일반요금’ end from Member m
- 단순 case식 : select case t.name when ‘team1’ then ‘학생요금’ when ‘team2 then ‘경로요금’ else ‘일반요금’ end from Tema t
- COALESCE : 하나씩 조회해서 null아니면 반환
- select coalesce(m.username,’이름없는회원’) from Member m (널이면 뒤의 디폴트값을 반환)
- NULLIF : 두 값이 같으면 null 다르면 첫번째 값 반환
- select NULLIF(m.username,’관리자’) from Member m (이름이 관리자면 Null,아니면 자기 이름 반환)
- JPQL기본함수(DB와 관계없이 사용가능)
- CONCAT, SUBSTRING, TRIM, LOWER, UPPER, …
- 사용자 정의 함수
- 사용하는 DB방언을 상속받고 사용자 정의 함수를 등록해서 사용한다
- 뭐 H2Dialect 상속받은 MyDialect만들어서 생성자에 사용자 정의 함수를 등록( 레퍼런스 참고해서) 하고 persistence.xml에서 MyDialect로 방언을 바꾸면 적용됨
- 근데, DB에 종속적이긴 하지만 그래도 앵간한 함수들은 다 이미 등록되어 있다
- 사용하는 DB방언을 상속받고 사용자 정의 함수를 등록해서 사용한다
객체지향 쿼리 언어2 -중급 문법
- 경로 표현식
- 점을 찍어 객체 그래프를 탐색하는것
- m.username, m.team, m.orders 이런거
- 상태필드 : m.username 처럼 단순히 값을 저장하기 위한 필드
- 경로 탐색의 종점, 더이상 탐색 불가능
- 연관필드 : 연관관계를 위한 필드
- 단일 값 연관필드 : @ManyToOne, @OneToOne처럼 대상이 엔티티인 경우(m.team)
- 묵시적 내부조인 발생, 탐색 가능
- 뭐 당연하긴 하지.. 다른 테이블에 있는 놈 데리고 오는거니
- 묵시적 조인이 생긴다는 것은 쿼리 튜닝시 어려워 진다는 것을 의미한다
- 그러니, 앵간해선 묵시적으로 조인되게 하면 안됨
- 즉, JPQL이랑 SQL이랑 좀 맞춰서 써줘야 한다. (묵시적으로 조인되는 구문이면 그냥 명시적으로 join해주기)
- m.team.name 이렇게 가능하다
- 묵시적 내부조인 발생, 탐색 가능
- 컬렉션 값 연관필드 : @OneToMany,@ManyToMany처럼 대상이 컬렉션인 경우
- 묵시적 내부조인 발생, 탐색 불가능
- m.orders. x ←이게 안됨query="select m.username from Team t join t.members m";
- 이렇게 아래 처럼 써야 한다.
- query="select t.members.username from Team t";
- FROM절에서 명시적 조인을 통해 별칭을 얻게되면 별칭을 통해 탐색은 가능
- 그냥 제일 중요한건 묵시적 조인은 절대 쓰지말고 다 명시적 조인을 사용해야 한다.
- 묵시적 조인은 항상 내부조인으로 이루어 진다
- 단일 값 연관필드 : @ManyToOne, @OneToOne처럼 대상이 엔티티인 경우(m.team)
- 점을 찍어 객체 그래프를 탐색하는것
<aside> 💡 페치조인
</aside>
- 성능 최적화를 위해 JPQL이 제공하는 기능
- 연관된 엔티티나 컬렉션을 SQL 한 번으로 조회 하는 기능
- 회원을 조회하면서 연관된 팀도 함께 조회하는 것
- "select m from Member m join fetch m.team";
- 이렇게 JPQL로 쓰면 SQL은
- select M.* , T.* from member m inner join team t on m.team_id=t.id; 이 실행됨
- ManyToOne에서 지연로딩 설정을 했으니, 원래라면 getTeam을 하면 조회 쿼리가 또 나갔지만, fetch join을 하게 되면 애초에 처음 가져올 때 조인해서 가져오니 (eager처럼 가져오는거지) 추가 쿼리가 나가지 않는다.
- 팀1 : 멤버1,멤버2 / 팀2: 멤버3 이렇게 있는 경우 (다대일 페치조인)
- 페치조인 미사용+지연로딩 : 멤버 N명을 조회한 쿼리에 대해 N개의 추가 쿼리(조회)가 나가는 문제를 N+1문제라고 함
- 멤버 1 : 팀1이 없으니 조회 쿼리
- 멤버 2: 영속성 컨텍스트에 팀2가 있으니 바로 로딩
- 멤버 3: 팀2가 영속성 컨텍스트에 없으니 조회 쿼리
- 페치조인 + 지연로딩 : 즉시로딩! (지연로딩 설정을 해도 페치조인을하면 eager로딩하는거임)
- 멤버1 ,2, 3 : 처음 회원들을 가져올 때 팀에 관한 정보도 가져오니 (inner join) 추가적인 쿼리가 나가지 않음( 프록시 객체로 팀이 존재하는게 아니라 진짜 팀 객체이니깐)
- 컬렉션 페치 조인 : 일대다 관계에서 페치 조인 (팀에서 members fetch)
- select t from Team t join fetch t.members where t.name=’team1’ (JPQL)
- select t from Team t inner join Member m on t.id=m.team_id where t.name=’team1’ (SQL)
- 근데 이런 경우 팀1에 멤버1,멤버2가 속해 있고 팀2에 멤버3이 존재한 상황에서 inner join을 하면
- 팀1+멤버1 , 팀1+멤버2, 팀2+멤버3 이렇게 3개의 row가 발생함 (팀은 2개의 row임)
- 그래서, 팀1에 관한 row가 두 개가 반환된다.
- 각 팀1은 완전히 똑같은 객체이고 멤버1,2 또한 똑같다
- 이런 중복을 제거하기 위해 distinct를 붙여서 날려준다.
- SQL입장에서 붙여도 멤버1과 멤버2는 다른 id를 가지니 중복이 아님
- 근데 JPA가 애플리케이션에서 영속성 컨텍스트에 중복된 엔티티가 있는 지 확인하고 중복된 엔티티가 있다면 (team1) 중복을 제거 해준다.
- 근데 hibernate 6부터는 안붙여도 중복 제거가 자동으로 된다
- 페치 조인은 객체 그래프를 SQL 한번에 조회하는 것! (즉시로딩을 하는 것!)
- 페치조인의 한계
- 페치 조인 대상에 별칭을 줄 수 없음
- select t from Team t join fetch t.members as m
- 이렇게 써서 뒤에다가 뭐 where m.~~ 이렇게 쓰면 안됨
- 즉, select t from Team t join fetch t.members 이렇게 써라
- 사실 하이버네이트는 가능하긴 하지만 앵간해선 사용해서 안된다(정합성이슈때문)
- 뭐 중첩해서 join fetch를 사용하고 하는 경우에 쓰기도 하는데 이런 경우 제외하고는 사용하지 말아야 한다
- 예를들어 멤버가 5명인 팀에서 3명만 골라서 조작하는 것은 상당히 위험하다
- 애초에 설계를 객체 그래프에 존재하는 데이터 전부를 다 가져오는 것으로 했다
- 만약, 전체 중 일부를 가져오고 싶다면 fetch join으로 연관관계를 이용해 탐색하는 것이 아닌 따로 해당 객체에 대해 select 쿼리를 만들어서 조회하는 것이 바람직 하다.
- 둘 이상의 컬렉션은 페치 조인 할 수 없음
- 이것도 데이터 정합성이 맞지 않을 수 있기에 바람직하지 않다
- 심지어 얘는 (1:N):N의 관계니 데이터가 무지막지하게 늘어날 수 있다.
- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인으로도 페이징이 가능함
- 일대다 다대다는 안됨 ( 데이터가 뻥튀기 되니까 )
- 예를들어, 페이징은 1개로 했으면 팀1+멤버1 / 팀1+멤버2 중 팀1+멤버1 row만 가져감 즉, 팀1에 멤버는 멤버1뿐이라고 밖에 안보임
- 하이버네이트는 경고로그를 남기고 메모리에서 페이징을 함
- WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
- 즉, 실제로 DB에 페이징 쿼리를 날리는게 아님
- 그냥 서버 메모리에 데이터 전부 긁어와서 쪼개서 주는거임
- 페이징을 사용하고 싶은 경우 그냥 반대로 Member로 부터 팀을 페치조인을 해서 페이징을 하면 됨
- 혹은, 페치 조인 부분을 그냥 빼버림
- 그러면, lazy로 걸어놨기 때문에 loop돌면서 조회하는경우 루프 도는 횟수만큼 (뭐 중복되지 않는다면) 조회 쿼리를 날려 로딩을 할 수 있긴 함
- 성능 저하
- Team에 근데 @BatchSize(size=n)라는 어노테이션을 OneToMany로 매핑한 멤버컬렉션 위에 달아주면, where 절에 team_id in (?,? .. ) 이렇게 조회 쿼리가 변경됨
- 즉, 여러 team에 속한 멤버들을 전부 조회하는 것(여기서 in에 들어가는 team_id의 갯수가 max n)
- 이 배치사이즈 어노테이션은 전역설정으로도 할 수 있음
- <property name="hibernate.default_batch_fetch_size"value="100"/>
- 페치 조인 대상에 별칭을 줄 수 없음
- 정리
- 연관된 엔티티들을 하나의 sql로 조회 하여 성능을 최적화 한다
- 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선(지연로딩 설정해도 얘가 우선)
- 실모에서 글로벌 로딩 전략은 모두 지연 로딩
- 최적화가 필요한 곳은 페치조인 적용
- 그러나, 모든 것을 페치 조인으로 해결할 수는 없다
- 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다
- 여러 테이블을 조인해서 엔티티가 아닌 전혀 다른 결과 조합을 만들어 내야 한다면 일반 조인을 사용하고 해당 결과 조합에 맞는 DTO로 변환하는 것이 효과적이다.
- 엔티티 직접 사용
- select count(m.id) from Member m //엔티티 아이디 사용
- select count(m) from Member m// 엔티티 직접 사용
- JPQL에서 엔티티를 직접 사용하면 SQL에선 해당 엔티티의 기본키를 대입하기에 두 쿼리는 같은 결과를 만든다
- select count(m.id) as cnt from Member m
- 파라미터로 넘겨도 마찬가지다
- select m from Meber m where m=:member / setParameter(”member”,member)
- select m from Member m where m.id= :memberId
- 이 두 쿼리도 똑같은 SQL
- 외래키 값으로도 넘길 수 있음
- select m from Meber m where m.team=:team / setParameter(”team”,team)
- select m from Meber m where m.team.id=:teamId / setParameter(”teamId”,team.getId())
- Named쿼리
- 미리 이름을 달아놓은 쿼리를 만들어서 나중에 불러올 수 있음
- 정적 쿼리고, 어노테이션이나 xml에 정의함
- 애플리케이션 로딩 시점에 초기화 후 나중에 재사용하면 된다
- 즉, 이 부분을 캐시에 넣어놓고 있는 것
- 애플리케이션 로딩 시점에 쿼리의 유효성을 검증함
- 만약 문법에 오류가 있으면 애플리케이션 로딩 과정에서 오류를 뱉음
- 이건 되게 좋은 오류다
- 사용자가 뭔가 행동을 해야 생기는 오류는 안좋은 오류임
- xml에도 정의할 수 있는데, XML에 정의된게 항상 우선권을 가짐
- 스프링 data jpa에서 인터페이스 위에 @Query로 적어놓은 쿼리문이 사실 named 쿼리임
- 벌크 연산
- 뭐 그냥 update Member m set m.salary= m.salary*1.1 where m.age>30; 이런거
- 쿼리 한 번으로 여러 테이블 로우를 변경하는 것
- update, delete를 지원 함
- 하이버네이트의 경우 insert into도 지원 함
- executeUpdate()라는 걸 마지막에 붙여주면 됨
- 반환 값은 해당 구문에 영향을 받은 엔티티의 수를 반환 함
- 주의점
- 얘는 영속성 컨텍스트를 무시하고 DB에 직접 쿼리를 바로 날려버림
- 해결방안
- 벌크연산을 먼저 실행 하는것, 영속성 컨테스트에 값 넣기 전에 그냥 벌크연산부터 수행
- 벌크연산 수행하면 쿼리를 나가는 것이니 영속성 컨텍스트가 flush된다. 그러니, 벌크 연산 수행 후 영속성 컨텍스트를 초기화 하는 것
- 그니까 em.clear해주고 em.find를 해서 새로 select쿼리로 객체를 받아오면 정상적으로 로직이 수행됨
- 스프링 data jpa 에서 @Modifying 쿼리를 날리면 벌크 연산을 하게 되는데 이놈이 바로 이 두번째 해결방안을 채택해서 내부에서 구현해 놓은 것
'Inflearn' 카테고리의 다른 글
스프링 MVC 2편 (3) (0) | 2023.06.10 |
---|---|
스프링 MVC 2편 (2) (1) | 2023.06.10 |
스프링 MVC 2편 (1) (1) | 2023.06.10 |
JPA 실전 2편 정리 (0) | 2023.04.05 |