실제 JPA 가 DB 와 어떻게 엔티티를 매핑할까요?
1) 객체와 테이블 매핑 (@Entity, @Table)
@Entity - JPA 가 관리할, 엔티티라 부른다. (No Args Contructor 필수), 이상한 클래스로는 불가
@Table - Entity Class 이름과 다르게 들어가야 한다면 name 지정 가능
2) DB Schema 자동생성
JPA에서는 앱 로딩 시점에 DB table 을 생성하도록 함 -> 당연히 운영은 쓰면 안되고, 완성된 DB 구조를 넣어야 함. (운영에서 쓸 때는 테스트 서버, 릴리즈 서버에 있는 것을 다듬은 다음에 보내야 함)
ddl-auto 속성
1) CREATE - 매번 모든 테이블을 (있으면) 모두 삭제후 다시 초기화 시킴 (@Entity 모두 확인 후) (이건 처음에 DROP 후 CREATE)
2) CREATE-DROP - CREATE와 똑같지만, 1)은 앱 종료해도 DB가 유지된다. 하지만 2)은 종료시 다 삭제시킨다. (TestCase 수행 후 마지막에 깔끔하게 없애는게 좋을 때, 앱이 종료된 이후 보기 싫을 때)
3) UPDATE - Alter Table을 하고 싶음. 칼럼을 추가하고 싶은데, 다 삭제하기는 싫음. (일단 DB를 유지하는 방향임)
4) VALIDATE - 엔티티와 테이블이 정상 매핑되어있는지만 확인. 즉, 만약 어떤 엔티티에서 String 하나를 추가 했음. CREATE이나, UPDATE이 아닐 때는 현재 DB 테이블에 추가되어 있지 않을테니, 엔티티와 테이블이 서로 맞지 않는다는 것을 감지. 에러 발생.
5) NONE - DB를 건드리지 않는다. 주석처리하는거랑 똑같은 것.
********** 운영에서는 1,2,3 은 절대로 사용하면 안된다.
로컬 같은 초기 TEST - 1,2 적합
테스트 서버 - 3,4, 적합 (여러 개발자가 함께 사용) - 함께쓰고 있는데 b 개발자가 다시 시작해서 DB 다 날라가면 안됨
운영 서버 -4,5 적합
------ 운영서버에서는 걍 쓰지마셈. 5 추천. DB를 원하는것 외에는 그냥 건드리면 안됨. 어떤어떤 상황에서 에러가 나서 시스템이 종료되면 절대 안됨. 운영에서 CREATE 했다는 상상을 한다면..... ㅋㅋㅋ.... ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ........... 솔직히 그냥 회사 망하는거임 ㅋㅋㅋㅋ (BU DB)
(그래서 사실 이 이전에는 웹 앱 계정은 Drop, Alter을 못하도록 다 분리하는게 맞음) - 서버 계정 말하는건데, 이거 잘 모르는 영역
3) 필드와 칼럼 매핑 (@Column)
@Column(unique = true, length = 10) 이런식으로 하면 실제 DB에서도 제약조건이 걸려서 들어감.
Member 변수가 다음과 같이 들어간다고 들어가자.
(회원 = 일반 회원과 관리자 구분, 회원 가입일과 수정일, 회원 설명 필드 필요)
@Entity
@Data
@NoArgsConstructor
public Member{
@Id
@Column(name="member_id")
private Long id;
@Column(name="name")
private String username;
private Integer age;
@Enumerated(EnumType.String)
private RoleType roleType;
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date updatedDate;
@Lob
private String description;
// DB랑 상관 없이 사용하고 싶은, 메모리에만 임시적으로 쓰고 싶음
@Transient
private int temp;
}
@Lob - 굉장히 큰 내용을 넣고 싶을 경우 넣음 (VARCHAR을 넘어서)
@Transient - DB에 넣고 싶지 않은 칼럼 데이터
@COLUMN (제일 중요)
-- nullable (중요) false 시 NOTNULL 제약 조건이 걸린다. (없으면 에러 조져줌)
-- unique 제약 조건 - unique 제약 조건이 걸린다. 하지만 잘 안쓴다! (제약조건이 걸리는 이름이 이상하게 나옴
---------------------> @Table(unque~~ 이런식으로 거는게 맞음!!)
-- columnDefinition 칼럼정보를 직접 명시함. ex) varchar(100) default 'EMPTY'
@EnumType : 참고로 EnumType 을 쓸때는 ORDINAL, STRING 타입을 선택할 수 있는데,, 무조건, 100% String 을 저장해야 한다. ORDINAL 은 나중에 다른 값을 반영했을 때 기존 값들을 반영해주지 않는다. 선택사항이 아님.
@Temporal : 참고로, 요즘 자바에 LocalDate, LocalDateTime 이 들어온 이후로 필요 없어진 칼럼임. 그냥 LocalDAte, LDT 쓰셈 ㅇㅇ
4) 기본 키 매핑 (@Id) (매우매우 중요~~)
@Id, @GeneratedValue (직접 할당할 경우 @Id만 쓰면 됩니다) 두 개가 있음.
@GeneratedValue Strategies
1. IDENTITY - 난 모르겠고 데이터베이스에게 알아서 하라고 위임
보통 앱에서는 NULL로 넣어줌 객체에. DB로 가면 DB에서 다음 번호를 알아서 순차적으로 할당해줌. AUTO_INCREMENT 느낌.
2. SEQUENCE - 데이터베이스 Seq Object 를 사용. (DB에 있는 SEQ OBJECT를 통해서 값을 할당함)
>> DB와 통신을 해준다음에 next_value를 가져온다고 뜸. (hibernate_sequence)
>> 테이블마다 따로 관리하고 싶으면 Sequence Generator 를 따로 지정해줘서 사용해주면 됨 ㅇㅇ 통합적으로 관리되길 원하면 걍 다 안쓰면 됨 ㅇㅇ
@SequenceGenerator 는 다 알만한 값들이 있다가 allocationSize 라는 녀석이 있음.
3. TABLE - 키 생성용 테이블 따로 사용. (굳이? 싶음)
>> TABLE 전략은 시퀀스를 약간 흉내내는 전략임.
>> 장점 : 모든 DB에 사용 가능 // 단점 : 성능 이슈. 직접적인 DB통신을 해야하기 때문. SEQUENCE 통신 X
4. AUTO - DBMS에 따라 설정된 기본값들이 다름. 그것을 사용. (MYSQL - IDENTITY 등등)
** 그래서 뭘 쓸까??
* 기본 키 제약 조건 : null 아님, 유일해야함. 변하면 안됨. (변하면 안된다는게 매우 중요)
* 주민등록번호 같은 걸 기본 키로 하는 것도 적절하지 않음. 왜냐면 주민번호 바꿔질 수도 있고, 주민번호를 보관하면 안된다는 법적인 조치가 있기도 함.
* 다른 테이블들이 Foreign Key 로 보통 기본 키를 들고 있기 때문에, 이 테이블만의 문제가 아니라 전체 DB의 문제가 될 가능성이 높음.
권장 : Long 형을 쓰고, UUID 같은 대체키도 쓰고, 키 생성전략도 잘 쓰자.
============================================================================================
JPA 와 키 생성전략의 이슈.
객체를 생성시 ID를 넣지 않고 생성함 (GeneratedValue 활용시)
JPA는 그리고 Insert SQL을 트랜젝셧 커밋 시점에 실행된다. (실행되면 flush 함) 그리고 null 값으로 날라오면 DB에서 그 때 값을 세팅해준다.
영컨에서 관리되기 위해서는 무조건! PK 값이 있어야함. 1차캐시에 Key 값이 PK값이기 때문.
IDENTITY 전략 - DB에 넣기 전까지는 PK값을 알 수 없으니, 1차 캐시를 활용할 수 없음. 그래서 이렇게 해결함 :::: IDENTITY 전략일 경우 persist를 한 시점에 INSERT QUERY를 날려버린다. 그러면 DB에서 id 값을 세팅을 해주고, 그 값만 select 해서 JPA가 가지고 옴. 그리고 1차 캐시에 보관해보림. 다음과 같이 TEST 해보자. (따라서 Identity 는 버퍼링 기능을 지원하지 않음)
void test(){
tx.begin();
try{
// MEMBER 가 현재 GENERATED_TYPE = IDENTITY일 경우
Member member = new Member();
member.setUsername("C");
System.out.println("=====================");
em.persist(member);
System.out.println("member.id = " + member.getId()); // Persist시 날리고 PK값을 가져오기 때문에 ID값이 찍힌다.
System.out.println("=====================");
tx.commit(); // 원래 이 시점에 SQL이 날라가야 함.
}catch(Exception e){
tx.rollback();
}finally{
em.close();
}
emf.close();
}
member.getId() 가 잘 찍히는 걸 알 수 있다. (SELECT 문이 또 나가는게 보이진 않아도, SEQ 전략이나 이 전략이나 내부적으로 JDBC가 ID를 들고 오는 방식은 다 짜져 있음)
저 위와 동일하게 SEQUENCE 전략을 진행할 경우,
Hibernate :
call nex value for ~~SEQ
라는 문이 나가는 것을 알 수 있음. (DB에 Insert Query가 날라가지 않는다). 너무 네트워킹이 많은 것 같으면, Allocation Size 라는 것을 지원해준다.
*** SEQ 타입으로 지정하게 될 경우, 모든 SEQ 타입으로 지정된 테이블은 한 SEQ 내에서 기본적으로 통합 관리된다 **
비슷한 객체인 KeyTest2 를 만든 다음에 Seq 타입으로 지정을 했다. 그리고 다음과 같이 Test 를 해봅시다.
@Test
@DisplayName("Generate Type : SEQ2")
void test3() {
System.out.println("SEQ Type 은 SEQUENCE Table 지정 없이는 통합 관리된다");
tx.begin();
try {
KeyTester testA = new KeyTester();
testA.setTestName("TESTER A");
em.persist(testA);
KeyTester2 testB = new KeyTester2();
testB.setTestName("TESTER B");
em.persist(testB);
KeyTester testC = new KeyTester();
testC.setTestName("TESTER C");
em.persist(testC);
System.out.println("testA.getId() = " + testA.getId());
System.out.println("Other Table but PK in Sequence:: testB.getId() = " + testB.getId());
System.out.println("testC.getId() = " + testC.getId());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
위와 같이 되면, 서로 다른 테이블에서 관리되는 객체임에도 불구하고
testA.getId() = 1
Other Table but PK in Sequence:: testB.getId() = 2
testC.getId() = 3
1,2,3 이 순차적으로 출력되는 것을 볼 수 있다. 이렇게 SEQ Type 으로 지정된 객체들의 SEQ 는 하나로 관리되어 순차적으로 들어가게 되어 있다. 하지만 한 객체내에서 PK 공백이 뜨는 것을 비선호하거나, 따로 SEQ가 관리되는 것을 원할 때 다음과 같이 Entity Class에서 지정해줄 수 잇다.
@Entity
@Data
@SequenceGenerator(
name = "KEY_TESTER_SEQ_GENERATOR",
sequenceName = "KEY_TESTER_SEQUENCE",
initialValue = 1, allocationSize = 50
)
public class KeyTester {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE
, generator = "KEY_TESTER_SEQ_GENERATOR")
private Long id;
private String testName;
}
저렇게 generator 를 생성해주고, seq Name 을 지정해주면 DB에 다음과 같이 생기는 것을 볼 수 있다.
이렇게 된다면 위 Test 를 다시 해준다면 다음과 같이 출력이 바뀌는 모습을 확인할 수 있다.
testA.getId() = 1
Other Table but PK in Sequence:: testB.getId() = 1
testC.getId() = 2
**** allocationSize ****
allocationSize = 50 : 한번 던질 때 DB값은 50을 올려놓는다. 앱 Memory 상에 있는 값을 1씩 쓰는 것. 여러 호출이 있어도 동시성 이슈 없이 잘 동작한다. 이러면 Hibernate : call next value for SEQ 가 안나가고 그냥 올라감.
다음과 같이 짜보자.
void test(){
tx.begin();
try{
// MEMBER 가 현재 GENERATED_TYPE = IDENTITY일 경우
Member memberA = new Member();
member.setUsername("A");
Member memberB = new Member();
member.setUsername("B");
Member memberC = new Member();
member.setUSername("C");
System.out.println("=====================");
em.persist(memberA);
System.out.println("memberA.getId() = " + memberA.getId());
System.out.println("=====================");
em.persist(memberB);
System.out.println("memberB.getId() = " + memberB.getId());
System.out.println("=====================");
em.persist(memberC);
System.out.println("memberC.getId() = " + memberC.getId());
System.out.println("=====================");
tx.commit(); // 원래 이 시점에 SQL이 날라가야 함.
}catch(Exception e){
tx.rollback();
}finally{
em.close();
}
emf.close();
}
다음과 같이 출력됨.
===========================
Hibernate :
call nex value for ~~SEQ
Hibernate :
call nex value for ~~SEQ
memberA.getId() = 1
===========================
memberB.getId() = 2
===========================
memberC.getId() =3
처음에 두번 가져오는건, 메모리에서 알아둬야 하기 때문. 얘가 어디까지 저장해 놓을건지.
첫번째 call 때 1을 가져오고, DB는 allocationSize만큼 올릴 것. 그리고 두번째 call 때 51을 가져옴.
그리고 그 범위를 알아두고 그 SEQ 를 사용하는 객체는 쭉 메모리에 할당된 그 allocation 값들을 쓰는 것. (그 이후 더이상 Hibernate가 호출되지 않는 것을 알 수 있음 51번까지)
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 웹서버가 Down 되면 구멍이 생김. 그래서 너무 크게는 하지 말고 50 정도가 적절하다고 함. ====== 낭비가 되기 떄문. 성능사엥는 큰 이슈가 없긴 함.
--- 추후 연관관계 매핑 예정 (@ManyToOne, @OneToMany, @JoinColumn .. etc)
'Spring > JPA' 카테고리의 다른 글
[JPA] - 값 타입 (0) | 2022.12.29 |
---|---|
[JPA] 프록시와 연관관계 - 2 (0) | 2022.12.28 |
[JPA] 프록시와 연관관계 정리 (0) | 2022.12.26 |
[JPA] 연관관계 매핑 -2 (0) | 2022.12.26 |
[JPA] JPA 개요와 영속성 컨텍스트 (0) | 2022.12.18 |