본문 바로가기
개발공부 개발새발/DB

JPA ) JPA 요약 (내가 검색하는 용도)

by 휴일이 2022. 11. 29.


JPA와 CRUD

저장
jpa.persist(member)

조회
Member member = jpa.find(memberId)

수정
member.setName("변경이름")

삭제
jpa.remove(member)


EntityManagerFactory는?
하나만 생성하고, 애플리케이션 전체에서 공유한다

EntityManager는?
쓰레드간의 공유는 X, 사용하고 버려야 한다

***
JPA의 모든 데이터 변경은 트랜잭션 안에서 실행한다
***

 

 

 

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

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 {
            // DB에 들어있는 PK가 1L인 Member 객체를 찾는다
            Member findMember = em.find(Member.class, 1L);
            // 1L의 Name을 HelloJPA로 변경
            findMember.setName("HelloJPA");
            // 커밋
            tx.commit();

        } catch (Exception e) {
            // 오류가 날 경우, 롤백한다
            tx.rollback();
        } finally {
            // em은 반드시 닫아준다
            em.close();
        }
        emf.close();
    }
}

 

 

 

 

 

 

JPQL (객체 지향 쿼리 언어)

JPA를 사용하면 Entity 객체를 중심으로 개발한다
검색 쿼리를 어떻게 짤 것인가?
->테이블이 아니라 Entity 객체를 대상으로 검색한다!
모든 DB 데이터를 객체로 변환해서 검색하는 것은 안 된다
애플리케이션이 필요한 Data만 DB에서 불러오려면,
결국 검색 조건이 포함된 SQL이 필요하다

JPQL은 객체를 대상으로 쿼리
SQL은 DB 테이블을 대상으로 쿼리

 

 

// m(Member.class) 라는 Entity(객체)로 찾는 모습
List<Member> result = em.createQuery("select m from Member as m", Member.class)
        // 5번째부터
        .setFirstResult(5)
        // 8개 가져와
        .setMaxResults(8)
        // 리스트로 가져와야함(결과가 많으니)
        .getResultList();

for (Member member : result) {
    System.out.println("member = " + member.getName());
}

 

 

요런 쿼리를 쓴다

 

 

 

 

 


JPA에서 가장 중요한 2가지


1) 객체와 관계형 데이터베이스 매핑하기
2) 영속성 컨텍스트
ㄴ JPA가 내부에서 어떻게 동작하는지 알 수 있다



영속성 컨텍스트란? " Entity를 영구 저장하는 환경 "

INSERT 할 때 쓰는
EntityManager.persist(entity)
ㄴ entity를 영속성 컨텍스트에 저장한다는 뜻


엔티티의 생명주기

비영속 (new / transient)
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태

영속 (managed)
영속성 컨텍스트에 관리되는 상태

준영속 (datached)
영속성 컨텍스트에 저장되었다가 분리된 상태

삭제 (removed)
삭제된 상태



비영속?
객체만 생성한 상태, EntityManager와 전혀 관련이 없음

영속?
객체를 생성하고, EntityManager를 가져오고
EntityManager에 객체를 저장한 상태

 

// 비영속
Member member = new Member();
member.setName("HelloJPA");
member.setId(100L);

// 영속, DB에 저장되는 상태는 아님
em.persist(member);
// 준영속, 영속성 컨텍스트에서 분리함
em.detach(member);
// 삭제, DB에서 데이터를 지우겠어
em.remove(member);

// 커밋하는 시점에서 DB에 쿼리가 날라감
tx.commit();

 

 

 

 

영속성 컨텍스트의 이점

1) 1차 캐시

KEY를 @ID로 두고 Entity 객체를 불러옴
1차 캐시에 먼저 저장하기 때문에 DB까지 안 가도 불러올 수 있음
(1차 캐시에 없다면, DB에서 1차캐시에 저장한 다음 불러옴)

 

 

 

// 객체 생성
Member member = new Member();
member.setId(103L);
member.setName("HelloJPA");

// DB에 INSERT 위한 영속성 컨텍스트에 저장
em.persist(member);
// 위에 만든 객체를 불러온다
Member findMember = em.find(Member.class, 103L);

// 1차 캐시에 저장되어 있기 때문에, 쿼리가 동작하기 전에도 불러올 수 있다
System.out.println("findMember.getName() = " + findMember.getName());
System.out.println("findMember.getId() = " + findMember.getId());

tx.commit();

 

 

 

// 영속
Member findMember1 = em.find(Member.class, 103L);
Member findMember2 = em.find(Member.class, 103L);

두번 불러와도

 

쿼리가 한번 동작함

첫 번째에 1차 캐시에서 불러왔기 때문에

두 번째는 DB까지 갈 필요가 없음

 

 

 



2) 동일성 보장

1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의
트랜잭션 격리 수준을 DB 가 아닌 애플리케이션 차원에서 제공한다

같은 PK의 데이터를 불러와서 다른 객체에 저장해도
그 두 객체는 동일한 객체로 취급된다

(같은 트랜잭션에서 실행할 경우)


 

// 영속
Member findMember1 = em.find(Member.class, 103L);
Member findMember2 = em.find(Member.class, 103L);

// 두 객체는 같은 객체로 취급된다
System.out.println("result = "+(findMember1 == findMember2));

 

 

 

 

 



3) 엔티티 등록할 때, 트랜잭션을 지원하는 쓰기 지원


쓰기 지연 SQL 저장소에 있던 SQL들이 commit 순간, DB로 날라감
그 전까진 쓰기 지연 SQL 저장소에 SQL들이 담겨짐

 

잘 활용하면 성능이 더 조아질 수 있어용

 

Member member1 = new Member(150L, "A");
Member member2 = new Member(160L, "B");

// 여기서는 쓰기 지연 SQL 저장소에 저장만 해둠
em.persist(member1);
em.persist(member2);

// commit 시점에 DB 들어감
tx.commit();

 

 

 

 



4) 엔티티 수정할 때, 변경 감지 가능

자바 컬렉션 다루듯이 변경이 가능
List에 넣은 객체의 값을 변경하고도 다시 넣지 않아도 되는 것처럼
find()로 가져온 객체에 setName() 하더라도 다시 넣지 않아도 됨,
set 하면 바로 바뀜

Member member1 = em.find(Member.class,150L);
// setName만 해줘도, 자동으로 update쿼리 실행
member1.setName("ZZZZZ");

tx.commit();

 

 

 

업데이트 쿼리가 실행되고

 

 

ID 150인 행의 이름이 바뀌어있다

 

 

 



어떻게 가능할까?

commit을 실행 하면
1) flush()
2) 1차 캐시 안에 있는 엔티티와 스냅샷(값을 읽어온 최초 시점)을 비교함
3) 어? 바꼈네? -> 업데이트 쿼리를 생성 -> DB에 반영하고 commit함

 

 

 

 

5) 엔티티 삭제
걍 조회하고, remove 실행하면 commit 시점에 삭제함ㅎㅎ

 

 

 

 

 



플러시?
ㄴ 영속성 컨텍스트의 변경 내용(쿼리)을 DB에 반영하는 것

- 변경을 감지합니다
- 수정된 엔티티를 쓰기 지연 SQL 저장소에 등록
- 쓰기 지연 SQL 저장소의 쿼리를 DB에 저장
(등록, 수정, 삭제 쿼리)


영속성 컨텍스트를 플러시하는 방법
- 직접 호출 : em.flush() (테스트 할 때 사용, 거의 쓸 일 없긴 해요)
- 자동 호출 : 트랜잭션 commit
- 자동 호출 : JPQL 쿼리 실행


 

직접 호출 : flush

 

Member member1 = new Member(200L, "member200");
em.persist(member1);

// 쿼리를 미리 보고 싶어요
em.flush();

System.out.println("================");

tx.commit();

 

==== 전에 쿼리 실행된다

 

 

플러쉬를 해도 1차 캐시는 다 유지됩니다

쓰기 지연 SQL 저장소에 있는 것들을 데이터에 반영하는 동작일 뿐입니당

 

 

 



JPQL 쿼리 실행시 플러시가 자동으로 호출되는 이유?


persist를 해놓고
중간에 JPQL select문을 실행하면 일단 고거 flush함

 

 

 




플러시는?!


영속성 컨텍스트를 비우는 것이 아니에요
ㄴ 변경 내용을 DB에 동기화 하는 것뿐

트랜잭션이라는 작업 단위가 중요해요
ㄴ 커밋 직전에만 동기화하면 됨

 

 

 

 

------------------------


준영속 상태

영속상태에서 준영속상태로 갈 수 있다
persist 뿐만 아니라 find 같은 1차 캐시에 있는 상태도 영속상태이다
영속 상태의 엔티티가 영속성 컨텍스트에서 분리되는 것

 

 

 

detach

// 영속 상태
Member member1 = em.find(Member.class,150L);
member1.setName("ZZZZZ");

// 영속 상태에서 분리함, JPA에서 관리하지 않음
// commit되지 않음
em.detach(member1);

tx.commit();

 

 

 

select문만 나오고 update 쿼리는 없다

 

 

 

 

 

 

clear

 

 

// 영속 상태
Member member1 = em.find(Member.class,150L);
member1.setName("ZZZZZ");

// 영속 상태에서 분리함, JPA에서 관리하지 않음
// 엔티티에 있는 영속성 컨텍스트를 전부 분리함
em.clear();

tx.commit();

 

 

 

select문만 나오고 update 쿼리는 없다

 

 

 

 

close() 영속성 컨텍스트를 종료

 

 

 

 

 

 

-----------------------------

 

 

정리)

 

JPA에서 가장 중요한 2가지는?

1) 객체와 DB 매핑

2) 영속성 컨텍스트

 

 

 

 

영속성 컨텍스트는?

엔티티를 저장하는 공간

엔티티매니저를 생성하면, 영속성컨텍스트가 드러갑니다

-> 스프링이랑 들어가면 살짝 달라짐

 

 

 

 

엔티티는 생명주기가 있어요

비영속, 영속, 준영속, 삭제

 

 

 

영속성 컨텍스트의 이점

1) 1차 캐시 -> 한번 조회햇던거 또 조회하면 DB까지 안들림

2) 동일성 보장 -> 객체 == 객체 같아요

3) 트랜잭션을 지원하는 쓰기 지연 / 버퍼링했다가 -> 한번에 쫙 커밋

4) 변경 감지해서 update 자동

5) 지연 로딩 -> 쿼리를 나중에 날리기 가능~

 

 

 

플러시

영속성 컨텍스트의 변경 내용을 DB에 동기화

영속성 컨텍스트를 비우지 않음

** 트랜잭션이라는 작업 단위가 중요해요 -> 커밋 직전에만 동기화하세용

ㄴ 처음에 설계할 때, 영속성 컨텍스트랑 트랜잭션 주기를 맞춰서 설계해야해요

 

 

 

 

 

 

엔티티 매핑!

객체와 테이블 매핑
@Entity , @Table

필드와 컬럼
@Column

기본 키
@Id

연관관계 매핑
@ManyToOne, @JoinColumn




객체와 테이블 매핑?
@Entity가 붙은 클래스는 JPA가 관리한다, 엔티티라고 부른다
JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 필수

- 기본 생성자 필수
- final, enum, interface, inner 클래스 사용 불가
- 저장할 필드에 final 사용 불가(수정이 안대자낭)


@Entity는 기본값이 클래스 이름 그대로, @Entity(name="")도 가능하긴 함
Entity name은 JPA에서 얘를 관리할 때 쓸 이름

@Table 엔티티와 매핑할 테이블명을 지정한다



데이터베이스 스키마 자동 생성

<property name="hibernate.hbm2ddl.auto" value="create" />

애플리케이션 실행 시점에 자동 생성됩니다
객체 매핑을 다 해놓으면 App 시작 시점에 Table 자동 생성도 가능

DB 방언을 활용해서 DB에 맞는 적절한 DDL 생성
ㄴ 개발에서만 쓰세요, 운영서버에서는 사용 X
ㄴ 다듬으세용

방언에 맞춰서 실행해줌니당



주의)
!!!운영 장비에는 절대 CREATE, CREAT-DROP, UPDATE 사용 금지!!!
개발 초기 단계에는 create 또는 update
테스트 서버에는 update 또는 validate - 가급적 쓰지마세요
스테이징과 운영 서버는 validate 또는 none - 가급적 쓰지마세요



DDL 생성 기능
ㄴ Unique 제약 조건
ㄴ 실행 자체에 영향 없음 DDL 생성에만 도움을 줌





------------------

필드와 컬럼 매핑


@Id
-> Primary Key

@Column
-> DB컬럼명과 엔티티 필드명이 다를 때 @Column(name = "name")

Integer 타입으로 했다면?
-> Integer와 가장 적절한 숫자 타입이 선택됨


@Enumerated(EnumType.STRING)
-> Enum 타입 쓰기 가능

@Temporal(TemporalTYPE.TIMESTAMP)
ㄴ DATE, TIME, TIMESTAMP 알려줄 때

@Lob
ㄴ varchar를 뛰어넘는 더 큰 단위를 사용할 때

@Transient
ㄴ 얘는 DB랑 관련 없는 필드야




@Column 속성?
name -> 필드와 매핑할 테이블의 컬럼 이름, 기본 필드명
inserttable -> 등록 가능 여부 , 기본 true
updatable -> 변경 가능 여부 , 기본 true
nullable -> null허용 여부 설정, false는 not null
unipue -> 유니크 제약 조건
columnDefinition -> DB 컬럼 정보를 직접 줄 수 있음, @Column( columnDefinition = varchar(100) default 'EMPTY' )
length -> 문자 길이 제약 조건, STRING에서만 사용
<BigDecimal(BigInteger) 타입에만 사용>
precision -> 소수점을 포함한 전체 자릿수 지정
scale -> 소수점의 자리수



@Enumerated 주의
EnumType.ORDINAL : enum 순서를 DB에 저장(기본값)
!! String 얘만 씀 !!
EnumType.String : enum 이름을 DB에 저장
!! String 얘만 씀 !!



@Temporal
날짜 타입을 매핑할 때 사용, 최신 하이버네이트는 지원하니까 걍~ 거의 안 씀



@Lob
필드가 문자면 CLOB, 나머지는 BLOB






-----------------

기본 키 매핑

@Id
직접 할당


@GeneratedValue
자동 할당

@GeneratedValue의 전략




strategy = "IDENTITY"
기본 키 생성을 DB에 위임(auto-increment)
: DB에 INSERT를 해봐야 ID값을 알 수 있음
: persist를 호출한 시점에 INSERT 쿼리가 날라감 (commit하기 전에도 날라감)


strategy = "SEQUENCE"
시퀀스 대상 컬럼은 Long 타입으로 씁시다
시퀀스 직접 만들고 싶다면? @SequenceGenarator를 써서 가능하긴 해요잉

@SequenceGenarator ( name = "필드에서 사용할 시퀀스 이름", sequenceName = "DB에 넣을 시퀀스 이름")
class 어쩌구

-방법
@Id
@GeneratedValue(strategy = GenerateType.SEQUENCE, genarator = "클래스에 붙은, 필드에서 사용할 시퀀스 이름")
필드(변수)

SEQUENCE 전략에서는 DB 시퀀스 값을 알아야 INSERT할 때 새 시퀀스를 줄 수 있음
시퀀스 값 주세요 쿼리 호출
그 다음 persist 하고, 영속성 컨텍스트 호출
버퍼링 주다가 commit 으로 날려주기 가능

미리 allocationSize(시퀀스 한 번 호출에 증가하는 수) -> 성능 최적화
로 미리 n개(50 기본) 미리 가져와서 시퀀스에 메모리 하나하나 올림




strategy = "TABLE"
키 생성 전용 테이블을 만들어서, DB 시퀀스 흉내냄
장점:모든 DB 적용 가능
단점:성능이 떨어짐
- 운영에서는 잘 안 씀니다



권장하는 식별자 전략
- 기본 키 제약 조건 : NOT NULL, 유일, 변하면 안 됨
ㄴ 미래까지 이 조건을 만족하는 자연키는 찾기 어렵다, 대체키(비즈니스와 전혀 상관없는 키)를 사용하자
ㄴ 주민번호도 기본키로 적절하지 않다
권장 : Long + 대체키(시퀀스) + 키 생성 전략

 

 

 

 

 

연관관계 매핑

객체의 참조와 테이블의 외래 키를 매핑하자 !


방향 : 단방향, 양방향
다중성 : n:1, 1:n, 1:1, n:m (다대일, 일대다, 일대일, 다대다)
* 연관관계의 주인 : 객체 양방향 연관관계는 관리주인이 필요 *

 


단방향 연관관계(객체지향모델링)

 

			// FK를 가지고 있는 객체에게 얘랑 조인할거야 알려주는 애너테이션
            @ManyToOne // Member가 1, team이 n
    		@JoinColumn(name = "TEAM_ID") // 얘랑 JOIN 할거얌
    		private Team team;
            
            ===============================
            
            // 팀 저장
            Team team = new Team();
            team.setName("TeamA");
            em.persist(team);

            // 회원 저장
            Member member = new Member();
            member.setUsername("member1");
            // 내가 설정한 Team의 Id가 저장됨
            member.setTeam(team);
            em.persist(member);


            // Member 객체의 Id값(PK)로 찾아주고
            Member findMember = em.find(Member.class, member.getId());

            // 직접 getTeam해서 불러옴
            Team findTeam = findMember.getTeam();
            System.out.println("findTeam.getName() = " + findTeam.getName());

 

 

 

간단하게 불러오깅

 

 

 

 

 


**양방향 연관관계와 연관관계의 주인**



mappedBy

객체와 테이블이 관계를 맺는 차이


객체 연관관계 = 2개
회원 -> 팀 (단방향)
팀 -> 회원 (단방향)
ㄴ 단방향 연관관계가 두개 있으니까 그냥 양방향 연관관계라고 하는 것
ㄴ 참조 두개로 왔다갔다

테이블 연관관계 = 1개 (외래키로 양방향)
회원 <-> 팀 (양방향)
ㄴ 키 하나(참조 하나)로 와따가따


객체 : 객체를 양방향으로 참조하려면, 단방향 연관관계를 2개 만들어야하는 것.
테이블 : 외래키 하나만 있으면 테이블의 연관관계가 형성됨



연관관계의 주인 ?
양방향 매핑을 할 때, 객체 두 관계중 하나를
연관관계의 주인으로 지정한다
*연관관계의 주인만이 외래 키를 관리(등록, 수정)
*주인이 아니면 읽기만 가능
주인은 mappedBy 속성 사용 안 함
주인이 아니면 mappedBy로 주인이 뭔지 지정하기




그러면 누구를 주인으로 해야 좋을까?
!!외래 키(FK)가 있는 곳!!
PK가 있는 곳을 주인으로 해버리면,
다른 PK를 외래키로 가지고 있는 것들이 같이 업데이트 될 수 있음
성능도 좀 떨어져용


DB는 FK가 있으면 무조건 n, PK는 1 -> 다수 쪽이 주인이 됩시다




양방향 매핑시 실수를 주의하자
*** 연관관계의 주인에 값을 입력하지 않으면 X

 

 

            // 주인 : Team
            // 주인이 아닌 걸 먼저 넣어버리면
            Member member = new Member();
            member.setUsername("member1");
            em.persist(member);

            // 주인이 member 객체를 넣어도, 키값이 안 바뀜
            Team team = new Team();
            team.setName("TeamA");
            team.getMembers().add(member);
            em.persist(team);

            // 결과는 MEMBER의 TEAM_ID가 null

역방향(주인이 아닌 방향)만 연관관계 설정하면 안 됨

 

// 주인 : Team
Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);

//정상적으로 값이 들어감

 

 

 

 

 

사실 그냥 양 쪽에 값을 다 넣어주는 것이 맞음

(양 쪽에 안 넣어주면, 순수 객체를 find 할 가능성이 있어요)

 

            // 주인 : Team
            Team team = new Team();
            team.setName("TeamA");
            em.persist(team);

            Member member = new Member();
            member.setUsername("member1");
            member.setTeam(team);
            em.persist(member);

            // 1. 양쪽 다 값을 입력하지 않으면?
//            team.getMembers().add(member);
            
            // 2. 순수한 team 객체를 찾기때문에(JPA, DB와 관련X)
            // 3. em.find로는 team을 찾을 수 없어요
            Team findTeam = em.find(Team.class, team.getId()); // 1차 캐시
            // 4. 그래서 members.size() == 0
            List<Member> members = findTeam.getMembers();

 

 

 

 

flush, clear가 없으면 1차 캐시에 아무것도 없어서 DB에서 다시 조회해 온다

하지만 1차 캐시에 값이 존재할 경우, 양쪽에 다 넣지 않으면 못 가져온다 JPA가 동작하지 않는다

 

 

// 주인 : Team
Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setUsername("member1");
member.setTeam(team); // 1. 나한테 넣기
em.persist(member);

team.getMembers().add(member); // 2. 쟤한테도 넣기

 

이렇게 넣는 걸 까먹을 수도 있으니

 

 

 

public void setTeam(Team team) {
    this.team = team;
    team.getMembers().add(this);
}

주인이 아닌 메서드에 mapped 된 객체를 가져오는 set 메서드에 이런 식으로 추가하면

(셋팅 시점에 걍 양쪽에 넣어버리기)

 

 

            // 주인 : Team
            Team team = new Team();
            team.setName("TeamA");
            em.persist(team);

            Member member = new Member();
            member.setUsername("member1");
            member.setTeam(team); // 1. 나한테 넣기
            em.persist(member);

//            team.getMembers().add(member); // 2. 쟤한테도 넣기

 

쟤한테도 넣기를 생략 가능하다

 

 

 

단순 setter로만 생각될 수 있으니

 

public void changeTeam(Team team) {
    this.team = team;
    team.getMembers().add(this);
}

이렇게 메서드명을 특이하게 바꿔주는 것도 조은 방법이다

1에나 n에 넣을 수 있지만, 양쪽에 다 넣진 않는다

 

 

 

 

 

양방향 매핑할 때 무한루프 조심~

toString(), lombok, JSON 생성 라이브러리

 

 

toString()

 

@Override
public String toString() {
    return "Member{" +
            "id=" + id +
            ", username='" + username + '\'' +
            ", team=" + team + //team의 toString을 불러온다
            '}';
}
@Override
public String toString() {
    return "Team{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", members=" + members + // 근데 얘는 members.toString()을 불러온다(무한루프)
            '}';
}

 

 

양쪽으로 무한 호출된다

 

lombok

ㄴ toString 자동 생성 쓰지 마세요

 

 

JSON 생성 라이브러리

컨트롤러에서 엔티티를 JSON으로 바꿀 때 ---- 문제생김

해답 : Controller에서 Entity 소환 금지, JSON으로 반환할때 문제됨

Entity -> DTO로 변환해서 소환하세욘~~!

 

 

 

 

------------

 

 

양방향 매핑 정리

 

처음 설계는 단방향으로 하세요

단방향 매핑만으로도 이미 연관관계 매핑은 완료! 

ㄴ 양방향은 반대 방향으로 조회 기능이 추가된 것 뿐이에요

 

JPQL에서는 역방향 탐색할 일이 많지만

 

단방향 매핑을 잘 하고, 양방향은 필요할 때 추가해도 됩니다(테이블에 영향 주지 않아요)

 

 

연관 관계 주인을 정하는 기준?

ㄴ비즈니스 로직을 기준으로 연관관계 주인을 선택하면 안 됨

주인은 외래 키의 위치를 기준으로 해야합니다

 

 

 

 


다양한 연관관계 매핑!


연관관계 매핑시 고려사항 3가지
- 다중성 (1:n , n:1 ....)
- 단방향, 양방향
- 연관관계의 주인


1) 다중성
다대일 @ManyToOne 제일 많이 씀
일대다 @OneToMany 자주 씀
일대일 @OneToOne 가끔 씀
XX다대다 @ManyToMany 실무에서 쓰면 안 됩니다


다중성 애매할 때?
어떻게 알 수 있지?
- 반대로 생각해보자
회원과 팀의 관계 / 팀과 회원의 관계
n : 1 / 1 : n



2) 단방향, 양방향

테이블은?
외래 키 하나로 두 테이블이 연관관계
사실 방향이라는 개념 XX다대다

객체는?
참조용 필드가 있는 쪽으로만 참조 가능함
한쪽만 참조하면 단방향
양쪽이 서로 참조하면 양방향 ->
객체 입장에서는 방향이 하나입니당! 서로 왔다갔다 하는 것뿐
양방향 관계의 객체는 참조가 두군데임
그 둘 중 참조에서 외래 키를 관리할 곳을 지정해야 합니다

3) 연관관계의 주인
연관관계의 주인 : 외래 키를 관리하는 참조
주인의 반대편 : 외래 키에 영향 못줌, 조회만 가능







@ManyToOne
다대일(n:1)

Member와 Team이 있을 때, 멤버가 N
N쪽에 외래키(FK)가 가야합니다

외래키를 가진 쪽(주인)이
안 가진 쪽 객체를 가져와서 참조하기!


가장 많이 사용하는 연관관계!


단방향 다대일

 

Member가 주인

@ManyToOne // Member가 n, Team이 1
@JoinColumn(name = "TEAM_ID") // 얘랑 JOIN 할거얌
private Team team;

 

 

양방향 다대일

일(Team)에서 다(주인 Member)을 참고할때

@OneToMany (mappedBy = "team") // Member 클래스의 team(변수명) 필드와 연관되어 있어요
private List<Member> members = new ArrayList<>();

 

 

 

다대일 양방향은?

* 외래 키가 있는 쪽이 연관관계의 주인이다

* 양쪽을 서로 참조하도록 개발한다!

 

 

 

 

@OneToMany
일대다(1:n)

DB에서는 다(n) 쪽에 무조건 외래키가 들어간다


일대다 단방향 (실무에서 거의 안 씁니다요~!)


 

일(1)이 주인

@OneToMany // team이 주인
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();

 

 

 

// 1 Insert
Member member = new Member();
member.setUsername("mamber1");

em.persist(member);

Team team = new Team();
team.setName("teamA");
// 얘가 한번 더 업데이트 되어야함
// 3 Update
team.getMembers().add(member);

// 2 Insert
em.persist(team);

 

 

우리는 persist만 두번 실행했지만,

쿼리는 세번

insert , insert , update 실행된다

왜?

외래키가 없는 입장에서는 insert가 먼저 되어도 외래키가 존재하지 않아서

일단 insert를 하고, 외래키 테이블도 insert 한 후

외래키가 없는 입장에서 외래키 테이블의 외래키값을 가져와서 다시 update 해야함

(복잡, 성능안좋음, 너무헷갈림-> 엥? 난 테이블 하나만 만졋는데 왜 쿼리가 얘도 수정되게 실행되지?)

 

 

객체와 테ㅔ이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조

 

@JoinCulmn 꼭 사용!!!

안 그러면 조인 테이블 방식을 사용함니다(둘을 조인할 중간 테이블, 새 테이블)

 

 

일대다 단방향 단점

1) 엔티티가 관리하는 외래 키가 다른 테이블에 있음

2) 연관관계 관리를 위해 추가로 update SQL이 실행됨

-> 일대다 단방향보다는 다대일 양방향 매핑을 사용하자 ^^!

 

 

 

일대다 양방향

 

 

다(n)에 그냥 @JoinCulmn 넣어버리면 둘 다 주인이 돼서 망함

그래서 수정 불가 상태를 만들어줘야함(읽기 전용 매핑)

    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) // 두개 false 넣어줘야함(수정 불가)
//    @JoinColumn(name = "TEAM_ID") // 얘만 있으면, 얘도 주인이 돼서 망함
    private Team team;

 

요런 식으로 하믄 댐니당 !

 

 

 

일대다 양방향은 공식적으로 존재는 안 하지만, 사용은 가능하다

@JoinColumn(insertable = false, updatable = false)

얘를 써서 읽기 전용 필드를 만들고 사용 가능

필요할 때가 있긴 함

 

-> 되도록 다대일 양방향을 사용합시다 ^_^

 

 

 

@OneToOne

일대일(1:1)

 

 

주 테이블 / 대상 테이블 중에 외래키 선택 가능

외래 키에 데이터베이스 유니크(UNI) 제약조건을 추가해야한당

 

 

주 테이블에 외래 키 단방향?

다대일 단방향 매핑과 똑같음! 어노테이션만 다름니다

 

주테이블에 외래키~

@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;

 

Locker 테이블

@Entity
public class Locker {

    @Id @GeneratedValue
    private Long id;
    private String name;
}

 

 

 

다대일 단방향 매핑과 유사함니다

외래 키가 있는 곳이 연관관계 주인이고, 반대편은 mappedBy 사용해요

 

 

 

대상 테이블에 외래 키가 있는 단방향

ㄴ 얘는 안대연 XXXX

ㄴ JPA에서 지원 안 됨

 

 

 

 

대상 테이블에 외래 키 양방향

내 외래키는 내가 직접 관리함니다

그래서 주인에서 가져온 객체로 관리하면 됨

 

 

1:1 관계의 경우

주로 select하는 테이블에 외래키가 있는 것이 편함니다~

Member가 Locker를 가지고 있도록 ^-^

대신 양방향을 거는 걸어야 하긴 하지만~ 그래도 뭐 어때~

 

 

주 테이블에 외래 키 ** 개발자들이 조아해용

- 객체 지향 개발자가 선호

- JPA 매핑 편리

장점) 주 테이블만 조회해도 대상 테이블에 데이터 있는지 확인 가능

단점) 값이 없으면 외래키에 null

 

대상 테이블에 외래 키

- 전통적인 DB 개발자 선호

장점) 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할때 테이블 구조 유지

단점) 어쩔 수 없이 양방향으로 만들어야 함

프록시 기능의 한계로, 지연 로딩으로 설정해도 항상 즉시 로딩됩니다ㅠㅠ

(있는지 없는지 무조건 확인해야하기 때문)

 

 

 

@ManyToMany

다대다(n:m)

얘는 안써연~~쓰지마세연~~~ㅎㅎ;

 

관계형 데이터 베이스는 정규화된 테이블 2개로 다대다 관계 표현 불가

연결 테이블을 추가해서 일대다, 다대일 관계로 만들어야 합니다(중간다리)

 

그런데, 객체는 컬렉션을 사용해서 객체 2개로 다대다 가능해요

그래서 객체끼리는 다대다

DB에서는 테이블-연결테이블-테이블

 

@ManyToMany

@JoinTable(name = "연결테이블명")

해서 사용 가능하긴 해요...

 

 

실무에서 사용 XXXXX

연결 테이블이 단순히 연결만 하고 끝나지 않는데, 중간 추가 정보를 넣을 수는 없음

중간 테이블 때문에 쿼리도 이상하게 됨

 

 

다대다를 쓰고 싶다면?

연결 테이블을 @Entity로 만들고 (추가정보 넣기 가능)

@OneToMany / @ManyToOne 로 관계를 맺어준다(다대일, 일대다)

 

 

PK는 그냥 의미 없는 값을 쓰세요잉~

(비즈니스에 의미 없는 값)

Id가 어딘가에 종속되어있으면, 유연하게 시스템을 갈아치우기 쉽지 않음

@Id @GenarateValue ->걍 묶어서 쓰는 게 편해요

private PK

 

 

 

 

 

 


관계형 DB는 상속 관계가 없으나
슈퍼타입 - 서브타입 관계는 상속 관계와 비슷하긴 해서
슈퍼타입 - 서브타입 관계를 자바에서 상속 관계로 구현한 것이 상속관계 매핑이다



구현 방법 세 가지
조인 전략 : 슈퍼, 서브타입을 각각 테이블로 만들어서 조인으로 불러오기
단일 테이블 전략 : 통합 테이블로(하나로 통합) 만들기
구현 클래스마다 테이블 전략 : 서브타입 테이블만 만들기



어노테이션
@Inheritance(strategy = Inheritance.Type.~~~)
Type 뒤에 무슨 전략을 쓸 건지 적어주면 됨
JOINED : 조인 전략
SINGLE_TABLE : 단일 테이블 전략
TABLE_PER_CLASS : 구현 클래스마다 테이블 전략

@DiscriminatorColumn(name=“DTYPE”) 
조인 전략을 쓸 때,
슈퍼 테이블에 서브 테이블을 구분할 수 있는 구분 컬럼을 추가하기 위해 쓴다
name = "컬럼명"
무조건 추가해주자 !

@DiscriminatorValue(“XXX”)
구분 컬럼을 추가했는데,
엔티티 이름으로 구분하는 것이 아닌
새 이름을 쓰고 싶을 때 사용
(이 엔티티는 DTYPE에 이 이름으로 저장되게 해줘~)




조인 전략(기본으로 깔고 가기)
*장점
- 테이블 정규화 (많이 써용)
- 외래 키만 참조하면 값을 가져올 수 있다
- 저장 공간이 효율적이다(나눠서 담으니)

*단점
- INSERT가 두 번 들어가야 한다(슈퍼, 서브)
- 조회 쿼리도 복잡하다
- 조인도 많이 사용해야 한다 
- 결과적으로 성능이 떨어진다(그래도 대부분 신경쓸만큼은 아닌듯?)




단일 테이블 전략(복잡한 비즈니스를 할 경우)
* 장점
- 조인이 필요 없으니, 조회 성능이 좋다
- 조회 쿼리가 단순하다

* 단점
- 자식 엔티티가 값을 넣지 않은 컬럼은 무조건 null이다
- 단일 테이블에 모든 걸 저장해서 테이블이 커질 수 있음
- 상황에 따라서 오히려 조회 성능이 떨어질 수도 있다




구현 클래스마다 테이블 전략(XX이거 쓰지마세연XX)
* 장점
- 서브 타입을 명확하게 구분해야할 경우 좋다
- not null도 사용 가능!

* 단점
- 여러 자식 테이블을 함께 조회할 때에 성능이 매우 느림(UNION으로 다 뒤져야됨)
- 자식 테이블 통합 쿼리가 어려움
- 테이블이 늘어나고 수정될 수록 감당이 안 됨
- 걍 쓰지 마세여~

 

 

 

 

@MappedSuperclass
!추상 클래스로 쓰쟈!

공통 매핑 정보가 필요할 때 사용
- 중복 정보인데(id, name..)
- 얘 맨날 엔티티 만들때마다 만들기 귀차나~
- 속성만 쓰고 싶어ㅠㅠ
- DB는 아예 따로임(관련 X) 그냥 객체에서 씁니다요
- 테이블은 생성 안 됨XXX 속성만 가져옴

1. 공통 매핑 정보 클래스를 상속받고
2. 공통 매핑 정보 클래스에 @MappedSuperclass 어노테이션 붙인다
3. 그러면 테이블을 생성하거나 수정할 때 이 속성도 자동으로 들어감
4. 슈퍼 타입에 상속해두면, 서브 타입도 같이 받기 때문에 개이득



@MappedSuperclass 클래스에 있는 속성에
@Column(name = "컬럼명") 해두면
속성명이 아닌 지정 이름이 컬럼명으로 생성됩니다


@MappedSuperclass

package jpabook.jpashop.domain;

import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@MappedSuperclass
public abstract class BaseEntity {

    private String createdBy;
    private LocalDateTime createdDate;
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;

    public String getCreatedBy() {
        return createdBy;
    }

    public void setCreatedBy(String createdBy) {
        this.createdBy = createdBy;
    }

    public LocalDateTime getCreatedDate() {
        return createdDate;
    }

    public void setCreatedDate(LocalDateTime createdDate) {
        this.createdDate = createdDate;
    }

    public String getLastModifiedBy() {
        return lastModifiedBy;
    }

    public void setLastModifiedBy(String lastModifiedBy) {
        this.lastModifiedBy = lastModifiedBy;
    }

    public LocalDateTime getLastModifiedDate() {
        return lastModifiedDate;
    }

    public void setLastModifiedDate(LocalDateTime lastModifiedDate) {
        this.lastModifiedDate = lastModifiedDate;
    }
}

 

@MappedSuperclass 클래스를 상속 받은 매핑

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item extends BaseEntity {

 

 

Item(슈퍼타입)를 상속받은 서브타입

@Entity
public class Movie extends Item {

 

 

 

 

 

 

 



프록시?

em.find() 실제 객체 조회
getReference() DB 조회를 미루는 가짜(프록시) 엔티티를 조회함니다
ㄴ 껍데기는 있는데 안에가 비어있음



프록시

실제 객체의 참조를 보관하고
프록시 객체를 호출하면?
실제 객체 메소드를 호출합니다


프록시 객체에서 메소드를 호출하면?
1. 영속성 콘텍스트에 초기화를 요청함
2. DB에서 조회해줌
3. 실제 Entity 생성함
4. 프록시 객체에 target(진짜 엔티티 객체를 가리키는 녀석)으로
해당 메서드를 불러온다
(마치 객체에 주소가 없다가 초기화해서 주소를 넣는 것과 비슷한듯)


**프록시 특징**

- 프록시 객체는 처음 사용할 때 한 번만 초기화됨

- 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 게 아님
프록시 객체를 통해서 실제 엔티티에 접근이 가능한 것뿐

- 프록시 객체는 원본 엔티티를 상속받을 뿐임, 그래서 타입 체크를 주의하세용
(== 비교 실패(같은 타입이라고 안 뜸), 대신 instance of 사용하기)

- 영속성 컨텍스트에 찾는 엔티티가 이미 있다면,
em.getReference()를 호출해도 실제 엔티티가 호출된다
ㄴ 같은 트랜잭션 안에서는 같은 엔티티라고 보장해주기 때문에 (== true) 그냥 원래 엔티티 반환해줌
ㄴ Reference 먼저 호출하고, 그 다음에 find로 호출해도 find는 proxy를 반환한다

- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태라면?
** 프록시를 초기화하면 문제가 발생합니다 ㅠ0ㅠ(찾을 수 없음)


프록시 확인하기

프록시 인스턴스의 초기화 여부 확인
emf.PersistenceUnitUtil().isLoaded(Object entity) 

프록시 클래스 확인 방법
entity.getClass()(..javasist.. or 
HibernateProxy…) 

프록시 강제 초기화
Hibernate.Hibernate.initialize(entity); 
참고: JPA 표준은 강제 초기화 없음
ㄴ member.getName() 이런식으로 강제초기화



실제 클래스를 상속받음, 그래서 껍데기가 같음
사용자는 어차피 걔가 진짜 사용할 때 쿼리가 나가니까
진짜 객체인지 프록시 객체인지 구분 안 하고 사용할 수는 있음(이론상 가능)

 

 

 

 

 

 

 



지연 로딩

@ManyToOne(fetch = FetchType.LAZY) //team을 지연로딩 하겠어!
@JoinColumn(name = "TEAM_ID")
private Team team

지연 로딩???
얘는 일단 프록시 객체로 두겠어~(안불러올거임)
실제로 얘를 사용하는 시점에 초기화 할거야~
그 전엔 프록시객체임(껍데기만 가져옴)


즉시 로딩

@ManyToOne(fetch = FetchType.EAGER) //team을 즉시로딩 하겠어!
@JoinColumn(name = "TEAM_ID")
private Team team

즉시 로딩???
얘는 둘 다 함께 자주 쓰이니까 엔티티 초기화 둘이 같이 해줘~(쿼리 함께 바로ㄱㄱ)
JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회




그런데... 즉시로딩 쓰지 마세염~

- 가급적 지연 로딩만 사용해요
ㄴ 성능 떨어짐, 복잡한 테이블이 얽혀있다면 더 문제, 걍 지연로딩 ㄱㄱ
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킴
ㄴ 쿼리 하나 날렸는데 N개의 문제가?!
- @ManyToOne, @OneToOne은 기본이 즉시로딩, LAZY 지연로딩으로 설정하세연
- @OneToMany, @ManyToMany는 기본이 지연로딩

1) 걍 먼저 지연로딩으로 깔음 ㄱㄱ
2) 필요할 때만 fetch join 해서 같이 가져오기



결론
- 모든 연관관계에 지연 로딩 쓰세연
- 즉시로딩 XXXXX 제발스지마연
- 같이 가져오고 싶으면 JPQL fetch 조인이나, 엔티티 그래프 기능 쓰세연
- 즉시 로딩은 쓰지마...상상못한 쿼리가 널 기다려~

 

 

 

 

 




영속성 전이 : CASCADE
: 얘는 연관관계, 즉시로딩 지연로딩과 아무 상관 없어여~

특정 엔티티를 영속 상태로 만들 때,
연관된 엔티티도 함께 영속 상태로 만들고 싶어요
ㄴ 부모 엔티티 저장할 때 자식 엔티티도 함께 저장하고 싶음...


부모를 persist 할 때,
얘랑 관련 된 자식들도 함께 persist 할 거야
@OneToMany(mappedBy="parent", cascade=CascadeType.ALL)


영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다
엔티티를 영속화 할 때 편리함을 주는 것뿐


하나의 부모만 해당 자식들을 관리할 땐 좋다
좋은 예) 게시판, 첨부파일 경로... 게시판에서만 첨부파일 경로를 씀
나쁜 예) 자식이 여러군데에서 쓰이면 ㄴㄴ






고아 객체


고아 객체란????
부모 엔티티와 연관관계가 끊어져서 고아가 됨...


@OneToMany (orphanRemoval = true)
고아가 된 자식 엔티티를 자동으로 삭제해용...
부모 엔티티랑 연관관계가 끊어지면 자동 삭제


주의)
참조하는 곳이 하나일 때만 사용하세여!!!!
특정 엔티티가 개인 소유할 때만 사용하세여!!!!
@OneToMany @OneToOne 만 사용 가능
부모를 제거하면 자동적으로 자식도 제거됩니다, CASCADE REMOVE와 똑같이 동작함





영속성 전이 + 고아 객체를 같이 써볼까용?

CascadeType.ALL + orphanRemoval = true ??
부모가 자식의 생명주기 관리하게 됩니다
도메인 주도 설계(DDD)의 Aggreate Root 개념을 구현할 때 유용해용

 

 

 

 

 

 

 

 





기본값 타입

JPA의 데이터 타입 분류

엔티티 타입
@Entity 객체
데이터가 변해도 식별자로 추적 가능
회원 엔티티의 키나 나이를 변경해도, 식별자로 인식 가능해요

값 타입
int, Integer, String 처럼 단순 값으로 사용하는 자바 기본타입이나 객체
식별자가 없고 값만 있음, 변경하면 추적 불가
숫자 100을 200으로 변경하면 완전히 다른 값이에요


값 타입의 종류

*기본값
자바 기본 타입(int, double)
래퍼 클래스(Integer, Long)
String

*임베디드 타입 embedded type
복합 값 타입
직접 커스텀해서 값 타입 사용하고 싶을 때

*컬렉션 값 타입 collection value type
임베디드, 기본 값 타입을 넣을 수 있음



기본값 타입
- 생명주기를 엔티티에 맡김
- 회원을 삭제하면 이름, 나이 필드도 함께 삭제됨
- 값 타입은 공유 절대 안 됨X
ㄴ 회원 이름 변경하는데 다른 회원 이름도 변경되면 큰일남


자바 기본 타입은 절대 공유 안 됨
- 기본 타입은 항상 값을 복사함
- 같이 변경이 되는 것이 아닌, 값을 복사만 하니까 공유하는 것이 아님
- Integer 같은 래퍼 클래스나 String 같은 특수 클래스?
ㄴ 공유 가능한 객체이지만 변경은 안 됨니다~




임베디드 타입
- 새로운 값 타입 직접 정의
- JPA는 임베디드 타입
- 주로 기본 값 타입을 모아서 복합 값 타입이라고도 해요
- int, String과 같은 값 타입(엔티티아님)


임베디드 타입은 어떻게 사용해요?
@Embeddable : 값 타입 정의할 때(클래스)
@Embedded : 값 타입 사용하는 곳(사용하는 필드)
*기본 생성자는 필수 !


장점)
- 재사용 가능
- 높은 응집도
- 해당 값 타입만 사용하는 의미 있는 메서드 만들 수 있다
- 임베디드타입을 포함한 모든 값 타입은,
값 타입을 소유한 엔티티에 생명주기를 의존합니당



DB에서는 똑같으나, 객체는 클래스로 연관있는 밸류를 합침



임베디드타입은 엔티티의 값일 뿐
임베디드타입을 사용하기 전과 후에도 매핑하는 테이블은 같음
-> 하지만, 객체와 테이블을 세밀하게 매핑 가능, 모델링 깔끔하게 떨어짐
잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스 수가 더 많습니당
임베디드 타입이 엔티티를 가질 수도 있음니다(외부 키로)



@AttributeOverride 속성 재정의
한 엔티티에서 같은 값 타입을 사용하면?
ㄴ 컬럼명 중복 ㅠㅠ
@AttributeOverrides, @AttributeOverride로
컬럼 명 속성을 재정의해용!


임베디드 타입이 null이면 매핑한 테이블 값도 다 null
(임베디드 타입 필드 있는 테이블의 값 다 null)





값 타입과 불변 객체

값 타입?
복잡한 객체 세상을 조금이라도 단순화하려고 만듬
그래서, 단순하고 안전하게 다룰 수 있어야 해용


임베디드 타입 같은 값 타입을
여러 엔티티에서 공유하면 위험해연
임베디드타입은 사용할 엔티티에서 유일하게 쓰고
다른 엔티티에서는 새로운 임베디드타입을 복사해서 쓰세연


그런데!
항상 값을 복사해서 사용하면 공유 참조 부작용을 피할 수 있지만
임베디드 타입처럼 직접 정의한 값 타입? 객체타입임
객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없음...(주소 복사)
객체의 공유 참조는 피할 수 없음....
이것이 객체 타입의 한계 ㅠ_ㅠ



그래서!
불변 객체!!!! 로 만들자 !!
ㄴ 생성 시점 이후에 절대 값을 변경할 수 없음
ㄴ 생성자로만 값을 설정하고, setter 안 만들기
객체 타입을 수정할 수 없음(부작용 차단)
값 타입은 불변 객체로 설계해야함

그래도 값을 바꾸고 싶다면?ㅠㅠ
걍 통으로 새로 만드세연....
new로 새로 객체 만들고 get으로 기존 필드 불러오고, 수정할 값만 바꿔줌니다
@@@@꼭 불변 객체로 만드세연@@@@





값 타입을 비교해보자!
값 타입은 인스턴스가 달라도, 안에 값이 같으면 같은 것으로 봐야합니당
하지만 ~ 객체는 인스턴스가 다르면 == 비교는 false ㅠ_ㅠ

동일성 비교 : 인스턴스 참조 값 비교. ==
동등성 비교 : 인스턴스 값을 비교. equals()

값 타입은 동등성 비교를 해야함 (equals)
그래서 값 타입의 equals() 메소드를 적절하게 Override 하자!
(모든 필드에서 ㄱㄱ)
대부분의 경우 equals 오버라이딩은 그냥 자동으로 생성해주는 걸로 쓰세연~
+ hashCode도 같이 추가해주세연~ HashMap 이런거 사용 가능하겡






값 타입 컬렉션?
- 값 타입을 컬렉션에 담아서 쓰는 것입니다
- DB는 컬렉션을 같은 테이블에 저장할 수 없어용
- 그래서 따로 테이블을 만들어냅니당
- 지연로딩

컬렉션 생성하고,(List, Set)

@ElementCollection <- 얘는 값 타입 컬렉션임
@CollectionTable(name = "테이블명", joinColumns = @JoinColumn(name = "조인할컬럼(외래키값)"))
속성이 String, Integer 같은 하나라면,
@Column ( name = "내가 사용할 컬럼명" ) 추가


다른 테이블이어도, 컬렉션을 넣은 엔티티가 관리하기때문에
해당 엔티티가 전부 관리함
하지만 지연로딩이기때문에 해당 엔티티를 조회해도
직접 값을 사용하지 않으면 쿼리가 안 나가고 프록시로 생김



수정할땐, 임베디드는 불변객체로 만듬 ㅡㅡ 직접 set XXXX
직접 get으로 불러온 후, 수정할 값만 새로 넣음(통으로 갈아껴야함)

기본 타입을 값 타입 컬렉션으로 만들었을 경우엔
변경할 값을 remove한 후 새 값을 add해야함


기본 타입이 아닐 경우에도 마찬가지인데,
remove하기 위해서는 add 했을 때와 똑같은 객체를 넣고 remove해야함
1) 기본적으로 equals 사용하기 때문에, 해당 객체에 equals+hashCode 만들어준 후
2) remove에 기존 객체와 똑같은 값을 가진 객체를 넣어서 삭제 후
3) 새로운 값을 add해야 함



값 타입은 식별자 기능 X
값 변경하면 추적이 어려움
값 타입 컬렉션에 변경 사항이 발생한다?
- 주인 엔티티와 연관된 모든 데이터 삭제
(하나만 수정해도, 관련된 모든 값 삭제 후 다시 INSERT...) 
- 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장


---결론
->걍 쓰지마세연~~값타입 컬렉션 쓰지마여~~~~
ㄴ 정말 개단순하고 변경할 거리가 없는 거에만 사용...
->다른 방법을 사용하도록 하세욘~~^.^
->일대다 관계를 쓰는 것을 고려해보세용!
ㄴ 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입 사용
ㄴ 영속성 전이 + 고아 객체 제거를 사용해서 값 타입 컬렉션처럼 사용

@OneToMany( 영속성전이 , 고아객체제거 )
@JoinColumn ( name = "외래키" )




엔티티 타입
- 식별자 O
- 생명 주기 관리
- 공유

값 타입 특징
- 식별자 X
- 생명 주기를 엔티티에 의존
- 값 공유 XX 하지마세연
- 불변 객체로 만드세요

 

 

 

 

 

 

 

 

 

 

 

 




JPQL


기본 문법, 기능


select
from
where
groupby
having
orderby


update = update / where
delete = delete / where



select m from Member as m where m.age > 18
- 엔티티와 속성은 대소문자 구분O (Member, age)
- JPQL 키워드는 대소문자 구분X (SELECt, From ...)
- 엔티티 이름 사용함, 테이블 이름 XXX
- 별칭 필수!!!(m), as 생략 가능!



TypeQuery : 반환 타입이 명확할 때 씀
Query : 반환 타입이 명확하지 않음



// 반환 타입이 명확할 때
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
List<Member> resultList = query.getResultList();


// 반환 타입이 명확하지 않을 때
Query query1 = em.createQuery("select m.username, m.age from Member m");



query.getResultList()
결과가 없으면 빈 리스트 반환 (null 걱정 X)

query.getSingleResult()
결과가 딱 하나만 있을 때 단일 객체 반환

-결과 없으면 NoResultException
-결과가 2개 이상이면 NoUniqueResultException

Spring Data JPA -> Exception 안나오고 걍 null 반환



파라미터 바인딩 - 이름 기준, 위치 기준

<이름 기준>
TypedQuery<Member> query = em.createQuery("select m from Member m where m.username = :username", Member.class);
query.setParameter("username", "member1");
Member singleResult = query.getSingleResult();
System.out.println("singleResult = " + singleResult);



<이름 기준> - 이렇게 쓰는 게 조아용
Member singleResult = em.createQuery("select m from Member m where m.username = :username", Member.class)
                    .setParameter("username", "member1")
                    .getSingleResult();



위치기반 쓰지 마세연~ 안조음... 이름 기반으로 쓰세용




프로젝션

- select 절에 조회할 대상을 지정
- 프로젝션 대상 : 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)

select m from Member m -> 엔티티
select m.team from Member m -> 엔티티
select m.address from Member m -> 임베디드 타입 (address가 값타입)
select m.username, m.age from Member m -> 스칼라 타입



// 엔티티 프로젝션으로 가져온 결과값은 영속성 컨텍스트가 관리함
            List<Member> result = em.createQuery("select m from Member m", Member.class)
                    .getResultList();

            Member member1 = result.get(0);
            // 값 바뀜
            member1.setAge(20);


JSQL은 최대한 SQL문과 비슷하게 써야됨

select m.team from Member m
보다는
select m.team from Member m join m.team t
처럼 명확하게 실제 SQL과 비슷하게 써주자



select distinct m.username, m.age from Member m
distinct -> 중복 제거~



프로젝션) 여러 값 조회하기


select m.username m.age from Member m

1) Query 타입으로 조회
2) Object[] 타입으로 조회
3) new 명령어로 조회 * 얘가 조와용 *
- 단순 값을 DTO로 바로 조회함
* select new jpabook.jpql.UserDTO(m.username, m.age) From Member m
- 패키지명을 포함한 전체 클래스명 입력
- 순서와 타입이 일치하는 생성자가 필요해요

<new 명령어 조회>
List<MemberDTO> query = em.createQuery("select new hellojpa.MemberDTO (m.username, m.age) from Member m", MemberDTO.class)
                    .getResultList();
            
MemberDTO memberDTO = query.get(0);





페이징 API

페이징은 이 두개를 써서 아쥬 쉽게~
setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
setMaxResults(int maxResult) : 조회할 데이터 수


List<Member> resultList = em.createQuery("select m from Member m order by m.age desc", Member.class)
                    .setFirstResult(0) // 0번째 데이터부터
                    .setMaxResults(10) // 10개 가져와줘
                    .getResultList();





조인

내부 조인 -> team 데이터가 없으면 아예 값이 안 나옴
select m from Member m [INNER] JOIN m.team t

외부 조인 -> team 데이터가 없어도 team 관련 데이터는 다 null뜨고 보여주긴 함
select m From Member m LEFT [OUTER] JOIN m.team t

세타 조인 -> Member , Team 다 가져온담에 m.username이랑 t.name 같은 거만 가져와~
select count(m) from Member m, Team t where m.username = t.name






ON절을 활용한 조인?
1) 조인 대상을 필터링한다
2) 연관관계 없는 엔티티 외부 조인

ON절: ON 뒤의 조건만 조인할래

Q) 회원과 팀을 조인하는데, 팀 이름이 A인 팀만 조인할래!

JPQL:
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'



Q) 회원과 팀을 조인하는데, 팀 이름이 A인 팀만 조인할래!
ㄴ 연관관계 없는 엔티티 외부 조인

JPQL:
select m, t from
Member m LEFT JOIN Team t ON m.username = t.name

SQL:
select m.* , t.* From
Member m LEFT JOIN Team t ON m.username = t.name







서브 쿼리 : 쿼리 안에 쿼리가 있는 것

메인쿼리랑 서브쿼리에 든 객체는 관계 없게 다른 이름으로 설정하세용(성능 업)


서브쿼리 지원 함수

[NOT] EXISTS : 서브쿼리에 결과가 있으면 참
ALL : 모두 만족하면 참
ANY , SOME : 조건이 하나라도 만족하면 참
[NOT] IN : 서브쿼리 결과 중 하나라도 같은 게 있으면 참



JPA 서브 쿼리의 한계... :(

- WHERE , HAVING 절에서만 서브 쿼리 사용 가능
- select 절도 가능
select (select avg(m1.age) from Member m1) as avgAge from Member m

- From 절의 서브 쿼리는 현재 JPQL에서 불가능
ㄴ 1. 조인으로 풀 수 있으면 풀어서 해결
ㄴ 2. 조인으로 안 되면... 쿼리를 두번 분해해서 날리거나~
ㄴ 3. (최후) Native SQL로 넘김







JPQL 타입 표현, 식




문자 'HELLO' 'She''s'
숫자 L(long) D(double) F(float)
boolean true false

enum 자바 패키지명 m.type = com.jpql.ADMIN (ADMIN is enum)
.parameterType("Member", userType.ADMIN)

엔티티 TYPE(m) = Member (상속 관계에서)
("select i from Item i where type(i) = book, Item.class")





JPQL = SQL과 문법이 같은 식
EXISTS , IN
AND , OR , NOT
= > >= <= <>
BETWEEN, LIKE, IS NULL, NOT NULL




조건식 - CASE

기본 CASE, 단순 CASE

COALESCE : 하나씩 조회해서 null이 아니면 반환

NULLIF : 두 값이 같으면 null, 다르면 첫번째 값 반환
- 사용자 이름이 있으면 이름, 없으면 이름 없는 회원을 반환
select coalesce(m.username, '이름 없는 회원') from Member m
- 사용자 이름이 '관리자'면 null, 나머지는 본인 이름
select NULLIF(m.username, '관리자') from Member m




기본 함수

JPQL이 제공하는 표준 함수 !!

CONCAT
SUBSTRING
TRIM
LOWER, UPPER
LENGTH
LOCATE
ABS, SQRT, MOD
SIZE, INDEX(JPA 용도)
team의 members의 Collection의 크기는?
- SIZE : select size(t.members) from Team t

@OrderColmn ( LIST의 값 타입 컬렉션에서 옵션으르 넣어서 쓸 수 있음, 컬렉션 위치값 구할때 )
- INDEX : select index(t.members) from Team t
ㄴ 근데 값 타입 컬렉션 되도록 쓰지 마세연 ㅠㅠ



사용자 정의 함수 !!

하이버네이트는 사용 전, 방언에 추가해야 함~
 사용하는 DB 방언을 상속받고, 사용자 정의 함수로 등록


select function( ~~~ ) from Item i

1) DIALECT 만들기(내가 사용하는 dialect 상속받기)
2) 생성자에서 registerFuntion 등록
ㄴ 방법은 그냥 그때그때 참조하기, 외우기 힘드러용 !
3) persistence.xml에 내가 만든 다이렉트 등록





DB종속적인 함수들 제공하긴 해용
- MYSQL DIALECT ) MYSQL 전용 방언 함수들




경로 표현식!

.을 찍어서 객체 그래프 탐색
select m.username -> 상태 필드
from Member m
join m.team t -> 단일 값 연관 필드(엔티티로 넘어감)
join m.orders o -> 컬렉션 값 연관 필드(컬렉션으로 넘어감)
where t.name = '팀A'


상태필드 ?
단순히 값을 저장하기 위해(변수느낌)

연관 필드 ?
연관관계를 나타내기 위한 필드
- 단일 값 : @ManyToOne , @OneToOne -> 대상이 엔티티
- 컬렉션 값 : @OneToMany, @ManyToMany -> 대상이 컬렉션(List, Set)


경로 표현식의 특징?

상태 필드 ? 경로 탐색의 끝, 탐색이 안 됨
select m.username From Member m
-> m.username에서 더 나아갈 수 없음, m.username이 끝

단일 값 연관 경로 ? 묵시적 내부 조인(inner join) 발생(무조건 내부 조인이 발생한다고 알고 있으셈), 탐색 O
select m.team From Member m
-> 여기서 탐색 더 할 수 있음
예) m.team.name -> 상태필드까지
예) m.team.members....
ㄴ 조심해서 쓰세요 ㅠㅠ 무조건 join 발생함..위험함
ㄴ 그냥 JPQL이랑 SQL이랑 거의 맞춰서 쓰세요...


컬렉션 값 연관 경로 ? 묵시적 내부 조인 발생, 탐색 X
select t.members From Team t (members == Member 컬렉션)
ㄴ 컬렉션 자체는 필드를 찍을 수 없음... (size만 가능)
From 절에서 명시적 조인을 통해 별칭을 얻는다?
-> 별칭 통해서 탐색 가능
select m.username from Team t join t.members m (별칭 생성 후 상태 필드 불러오기)



사실~
묵시적 조인 그냥 쓰지 마세연~~~
그냥 명시적으로 쓰십쇼~~~~~^-^
실무에서는 편하다고 썼다가 나중에 유지보수 등등 넘 힘듬



명시적 조인 ? 직접 join 키워드 사용
묵시적 조인 ? 경로 표현식에 의해 묵시적으로 SQL조인 발생, 내부 조인만 가능!
* 명시적 조인 ㄱㄱㄱㄱ



경로 탐색을 사용한 묵시적 조인....
- 항상 내부 조인
- 컬렉션은 경로 탐색의 끝이니, 명시적 조인으로 별칭 써야함
- 경로 탐색은 묵시적 조인으로 from join절에 영향을 줌

그래서!!!
- 그냥 명시적 조인 쓰세연~
- 조인은 SQL튜닝에 중요해요~ 그냥 명시적으로 쓰세욘
- 묵시적 조인은 쿼리가 어떻게 돌아가는지 한눈에 파악이 어려워요ㅠ


<<중요>>
*****페치 조인(fetch join) ******


SQL 조인이 아니에욘!
JPQL 전용 기능, 성능 최적화 굳
연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회 !!!
ㄴ 두번 쿼리할 걸 한번 쿼리로!
join fetch 명령어


엔티티 페치 조인

회원을 조회하며, 연관된 팀도 함께 조회하자
SQL을 보면 회원 뿐만 아니라 팀(t.*)도 함께 select

<멤버 조회할 건데, 한번에 team도 가져와~>
- JPQL
select m from Member m join fetch m.team

- SQL
select m.*, t.* from Member m
Inner join team t on m.team_id = t.id


join fetch 하면, team은 프록시가 아니라 진짜 엔티티임
그래서 처음부터 데이터가 채워져있고, team을 가져오는 쿼리를 또 찍을 필요 없음(지연로딩 해당 안함)




컬렉션 페치 조인

일대다 관계나 컬렉션 페치 조인

- JPQL
select t from t join fetch t.members
where t.name = '팀A'

- SQL
select t.*, m.* from team t
inner join member m on t.id = m.team_id
where t.name = '팀A'


조심할것...!
일대다 조인은 데이터가 뻥튀기 될 수 있음

그래서!!!! DISTINCT 사용 !!!!

SQL - DISTINCT
-> 중복된 결과를 제거
ㄴ 모든 결과가 똑같아야지만 중복이 제거 됨(모든 컬럼값 동일)

JPQL - DISTINCT
-> SQL에 DISTINCT 추가
-> 애플리케이션에서 엔티티 중복 제거
ㄴ "같은 식별자"를 가진 엔티티를 제거


그래서
select distinct t from t join fetch t.members
where t.name = '팀A'
ㄴ 같은 식별자를 가진 팀A 회원들만 가져와잉~

일대다는 뻥튀기 안 됨
다대일만 뻥튀기 되어용 조심스~




일반 조인?
연관된 엔티티를 함께 조회하지 않아요 !

페치 조인?
연관된 엔티티를 함께 조회해요 ! (지연로딩 X, 바로 가져옴)


JPQL은 사실?
결과 반환할 때 연관관계 고려 안 함
그냥 select 절에 지정한 엔티티만 조회
팀 엔티티만 조회하고 회원 엔티티는 조회 안 함

그런데 페치 조인은?
연관된 엔티티도 함께 조회(즉시 로딩 ㄱㄱ)
객체 그래프를 SQL 한번에 조회해용~





* 페치조인의 특징...그리고 한계...

1) 페치 조인 대상은 별칭 줄 수 없음, 관례임, 위험해~ 하지마~
ㄴ 페치 조인을 겹쳐서 마니 해야돼? 그 때는 사용할 수 있긴 해..

2) 둘 이상의 컬렉션은 페치 조인 안 돼요

3) 컬렉션을 페치 조인하면, API(setFirstResult, setMaxResults)를 사용 못 해요*존나위험해용~쓰지마잉*
ㄴ 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
- 일대다를 다대일로 반대로 바꿔서 할 수도...?
- 걍 DTO로 뽑기..?
- @BatchSize(size = n) 준다 (글로벌 세팅도 가능! xml에 추가ㄱㄱ)

@BatchSize란 ?
ㄴ 팀이 10개라면 일단 팀 10개 불러오고
ㄴ 해당 팀에서 where절로 해당 팀에 있는 id를 n개 검색해서 가져온다~
ㄴ 그래서 해당 팀 멤버를 쿼리 줄여서 가져온다
(이거 같긴 한데...걍 필요할 때 검색해봐야게씀 ㅋㅋ 잘모루겟당 ㅋㅋ


4) 연관된 엔티티들을 SQL 한번으로 조회 - 성능 굳 !

5) 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선한당
ㄴ @OneToMany(fetch = FetchType.LAZY) <- 글로벌 로딩 전략(지연 로딩)


!!!!그래서!!!!
* 실무에서 글로벌 로딩 전략은? 모두 지연로딩 ㄱㄱ
* 최적화가 필요한 곳은 페치 조인 적용ㄱㄱ





- 정리
모든 것을 페치 조인으로 해결하려고 하지 마라...
페치 조인은 객체 그래프를 유지할 때 사용하면 조와요~

여러 테이블을 조인해서 엔티티의 기본 형태가 아닌,
전혀 다른 결과를 내야 한다면...?
차라리 페치 조인 ㄴㄴ 일반조인 ㅇㅇ 
필요한 데이터들만 따로 조회해서 DTO로 반환해요잉









------------

다형성 쿼리


TYPE
- 조회 대상을 특정 자식으로 한정 가능
ex) Item(부모) 중에 Book, Movie만 조회해~

JPQL
select i from Item i
where type(i) IN (Book, Movie)

SQL
select i from i
where i.DTYPE IN ('B', 'M')



TREAT (자바 : 타입 캐스팅)
부모 타입을 특정 자식 타입으로 다룰 때
from, where, select

부모 Item, 자식 Book

JPQL
select i from Item i
where treat(i as Book).author = 'kim'

SQL
select i.* from Item i
where i.DTYPE = 'B' and i.author = 'kim'






엔티티 직접 사용


기본 키 값:
JPQL에서 엔티티를 직접 사용하면?
SQLd에서 해당 엔티티의 기본 키 값을 사용


JPQL
select count(m.id) from Member m -> 엔티티 아이디 사용
select count(m) from Member m -> 엔티티 직접 사용

SQL(위의 두 줄 다 동일한 쿼리)
select count(m.id) as cnt from Member m

-> 엔티티 자체를 넘기면 구분할 게 키값이기 때문에, 무조건 쿼리는 엔티티의 키값으로 ㄱㄱ



외래 키 값:

JPQL
select m from Member m where m.team = :team

SQL
select m.* from Member m where m.team_id = ?


이유 ?
ㄴ Member가 team에 대한 외래 키값을 들고 있어서 그걸로 team 구분,
ㄴ 그래서 team 엔티티라고만 해도 ~ team 외래 키값으로 검색됨





Named 쿼리

미리 정의해서 이름을 부여해두고 사용하는 JPQL
정적 쿼리
어노테이션, XML로 정의
애플리케이션 로딩 시점에 초기화 후 재사용
!!! 애플리케이션 로딩 시점에 쿼리 검증해줌 !!!

- 우선 순위
xml > 어노테이션



<어노테이션>
1)
@Entity
@NamedQuery // ** 요 어노테이션
(name = "Member.findByUsername",
query = "select m false Member m where m.username = :username")
public class Member


2)
List<Member> result =
em.createNamedQuery("Member.findByUsername", Member.class) // 이름으로 쿼리 불러오깅
.setParameter("username", "회원1")
.getResultList();





<persistence.xml>
1) 매핑 파일 경로 집어넣고

2) 매핑 파일에 쿼리 집어넣음 like MyBatis




Spring Data JPA에서는 ?
- 인터페이스를 만들고 그 메서드 위에 바로 적용 가능ㅇㅇ
- 존나 편해요잉`~~~~
- 그래서 걍 ㅋㅋ 스프링할때 새로 ㄱㄱ





----------------

벌크 연산(update, delete)


Q) 재고가 10개 미만인 모든 상품의 가격 10% 상승

JPA 변경 감지 기능으로 실행하면...
1) 재고 10개 미만인 상품 리스트로 조회
2) 상품 엔티티 가격 10% 증가
3) 트랜잭션 커밋 시점에 변경 감지 작동
-> 변경 데이터가 100건...? 100회 쿼리 작동 ㄷㄷ


벌크 연산으로 실행하면... 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
.excuteUpdate() 하면 걍 되어요!
- excuteUpdate 결과로 영향받은 엔티티 수 반환ㄴ
- update, delete 지원해요
- insert(하이버네이트가 제공)



하지만, 주의!!!
- 영속성 컨텍스트를 무시하고, DB에 직접 쿼리
그래서
* 벌크 연산을 먼저 실행
OR
* 벌크 연산 수행 후, 영속성 컨텍스트 초기화(clear)
- 영속성 컨텍스트를 무시하고 직접 쿼리하기 때문에
- DB 값이랑 애플리케이션이 알고 있는 값이랑 달라서 문제생김
- 그래서 영속성 컨텍스트 초기화!



Spring Data JPA에서는 ?
@Modifying 으로 쉽게 해주어요~

728x90