DB의 1 : 다 관게에서 FK는 "다"쪽에 들어가야한다.
JPA 애너테이션
- @Entity : jpa가 로딩될 때, 'jpa를 사용하는 객체구나. 내가 관리 해야하는 구나.' 인식.
- @Column : 컬럼 매핑
- @Temporal : 날짜 타입 매핑 (요즘에는 쓸 필요가 없다.)
애너테이션 없이, LocalDate | LocalDateTime 형을 사용하면 된다. - @Enumerated : enum 타입 매핑 (enumtype.string 옵션을 사용하자. enum에 데이터가 추가될 때 더 안전한다.)
- @Lob : BLOB, CLOB 매핑
- @JoinCloumn : 외래키 매핑
- @Transient : 객체와 테이블의 컬럼 관계를 끊는다.
https://gmoon92.github.io/jpa/2019/09/29/what-is-the-transient-annotation-used-for-in-jpa.html
- 매핑 옵션
- mapped by : 양방향 관계를 걸어주고 싶을 때...
기본적인 스프링 jpa 동작 순서:
'''Main.java'''
//1. 엔티티 매니저 팩토리 생성.
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("hello");
//2. 엔티티 매니저 팩토리에서 엔티티 매니저 생성.
EntityManager entityManager = emf.createEntityManager();
//3. 생성한 엔티티 매니저를 통해 객체화한 DB 컨트롤
entityManager.close();
entityManagerFactory.close();
엔티티 매니저의 동작:
- 외부의 요청 -> EntityManagerFactory에서 EntityManager 생성. -> 생성된 EntityManger는 DB와의 커넥션 풀을 사용한다. (이때, EM은 외부의 요청이 들어올 때마다 생성된다.)
- 엔티티 매니저를 생성하면 그 안에 1대1로 영속성 컨텍스트가 생성된다.(PersistenceContext)
- 엔티티 매니저를 일종의 자바 컬렉션처럼 생각하면 이해하기 수월하다.
EntityManagerFactory 🔸EntityManger
- EMF는 애플리케이션이 시작될때 1번 생성 ➡️ 애플리케이션이 종료될때 종료.
- EM은 애플리케이션이 실행되는 동안, 요청이 들어올때마다 생성과 종료의 반복.
➡️ 따라서, try-catch-finally를 활용해서 설계할 경우 아래처럼 EMF.close()는 finally 구간에서 실행되지 않고, 애플리케이션 종료 시점에 위치한다.
... main( ... ) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
...
tx.commit(); //만약, 이때 예외 상황이 터지면,,,
}catch(Excreption e) {
tx.rollback(); //여기서, 트랜젝션을 롤백
}
finally{
em.close(); //예외가 터지든 안터지든 코드수행 후 em은 항상 닫아야한다.
}
emf.close(); //하지만, emf는 생명주기가 애플리케이션 실행시점 ~ 종료시점, 따라서 finally 밖에서 close()...
}
더보기
추가로 읽어보면 좋은 글 링크...
영속과 비영속
//이때는 member 객체는 비영속 상태. 객체가 생성되기만 했다.
Member member = new Member();
member.setId(1L);
member.setName("name");
//엔티티 매니저에 persist() 해줌으로써, member 객체는 엔티티매니저의 영속성 컨텍스트에 들어가고,
//비로소, 영속 상태가 된다.
entityManager.persist(member);
- 주의: 영속 상태로 바뀔때 DB에 반영이 안됨. 트랜젝션이 커밋될때 DB 쿼리문이 날라가고, 반영이 된다.
영속 상태를 가짐으로써 얻을 수 있는 이점들
영속 상태는 캐싱 기능과 동일하다. 즉, 영속 컨텍스트는 1차 캐시역할을 한다.
1. persist 하는 순간 바로 db에 저장이 안되기 때문에, 이 시점에서 최적화를 할 수 있다.
-> 예를 들어, 20개의 데이터를 db에 저장할 때, 10개씩 2번 DB와 통신하는 식으로 저장할 수도 있다.
2. 영속 컨텍스트에 이미 있는 데이터들은 DB에서 가져올 필요없이, 영속 컨텍스트안에서 빠르게 가져올 수 있다.
3. 또한, 영속 컨텍스트에 엔티티를 먼저 보관하기 때문에, 영속 컨텍스트에서 가져오는 객체들은 동일성을 보장할 수 있다.
변경감지 - dirty Checking
영속 컨텍스트의 1차 캐시안에 엔티티들이 있다. 근데, 해당 1차 캐시에서 엔티티들의 처음상태(생성될 때의 상태X 가장 최근에 가져온 상태O)를 함께 저장한다. -> 가지고 있는 처음 상태와 최종 엔티티 상태를 비교하고, 변화된 부분은 update한다.
객체 중심 설계 🔛 데이터 중심 설계
Member | Team | ||
member_id | long | team_id | long |
team_id | long | member_id | long |
... | ... | ... | ... |
- 위 처럼 데이터 중심적으로 설계할 경우, Member-Team 간의 관계가 객체 지향적이지 않음.
즉, Team객체(Member 객체)에서 반대편의 객체를 조회할 경우, EntityManger의 도움(find)를 2번 받아야 함.
Member | Team | ||
member_id | long | team_id | long |
team | Team | Members | Array<Member> |
... | ... | ... | ... |
- 위 처럼, Member와 Team에서 서로 반대편의 객체를 가지고 있으면, 객체 ➡️ 객체로 조회 할 수 있다.
- 이러한 객체 지향적 설계는 연관관계 매핑을 이용...
객체 지향 단방향 매핑 예제...
'''Member'''
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
- Member와 팀은 다 : 1 관계. 즉, Team은 여러 Member를 가질 수 있다. ➡️ @ManyToOne
- Member가 가진 Team 객체는 DB상에서 TEAM_ID 값(Member의 FK값)과 매핑을 해야 한다. ➡️ @JoinColumn(name = "TEAM_ID")
JPA의 양방향 관계...
관계를 맺은 엔티티에 각각 단방향 관계를 맺어줌으로써, 양방향 관계가 된다.
연관관계의 주인
- 단방향일때는 주인을 지정할 필요가 없지만, 양방향일때는 관계의 주인을 지정해줘야 한다.
- 예를 들어, Team과 Member가 양방향 관계를 가지고 있을 때, "Team의 Member를 바꿀 것인가? 아니면 Member의 Team을 바꿀 것인가?"를 정해야 한다. 이때, 주체가 되는 Entity가 관계의 주인이 된다.
- 양방향 매핑 규칙
- Entity 중 하나를 주인으로 지정.
- 주인만이 외래 키를 관리
- 주인이 아닌 쪽은 읽기만 가능
- 양방향 관계를 맺은 두 Entity는 양 쪽에서 값을 세팅해주는게 좋다.
Team과 Member(주인)가 있을 경우, Member에서 Team을 지정해주고, 추가로 Team에서도 Member를 지정해주자.
이유는? : 개발할 때, 주인이 아닌 곳에 값을 지정해주지 않으면, 해당 Entity는 반대 쪽 Entity값이 null인 상태로 되기 때문이다. 만약 Team과 Member의 관계에서 Team에는 값을 지정해주지 않는다면, Team에서 member를 조회할 수 없다.
'''team에 member 지정 x '''
findTeam = em.find(Team.Class, team.getId());
List<Members> members = findTeam.getMembers();
//이때, members는 빈 배열. Team에는 member를 지정해주지 않았기 때문에,,,
'''만약, 위의 코드에서 team의 members를 조회하고 싶으면,
2번 라인의 em.find 위에 em.flush(); em.clear();를 추가해줘야한다.
왜? 위 코드에서 findTeam은 1차 캐시에서 가져온 Team객체 (아무런 반영이 안된 상태)
이를 해결하기 위해, 조회 전에 flush()(db에 반영), clear()(1차 캐시 비우기)를 함으로써,상태를 DB에 반영시키고,
반영이 끝난 team객체(DB 안에 team의 member 값이 반영이 끝난 시점임.)를 새로 DB에서 꺼내와서 조회하는
것이기 때문에 작동한다.'''
'''team에 member 지정 o '''
team.getMembers.add(member); //team의 Members 배열에 member 추가
findTeam = em.find(Team.Class, team.getId());
List<Members> members = findTeam.getMembers();
team.getMembers에 값이 지정됨(추가됨). -> team에서 Members 조회가능.
위의 규칙4 부분 코드에서 team.getMembers.add(member)가 번거롭다면,,,
- ✔️Member.setTeam 부분에 team.setMembers().add(this) 와 같이 한번에 만들어주자.
- 이런 연관관계 편의 메서드는 team에서도 만들 수 있고, member에서도 만들 수 있다. 어디에 만들건 상관은 없지만, 한쪽에서만 만들어주도록 하자. 양쪽에 만들어준다면, 나중에 문제가 생길 수도 있다. (무한루프)
연관관계의 주인을 지정할 때 헷갈린다면?
- 외래 키가 있는 곳을 주인으로 지정한다.(1:다 관계에서 '다'쪽이 관계의 주인이 된다.
- 이유는?
관계를 가진 두 Entity에서 외래키를 가진 Entity의 외래키만 바꿔주면 되기 때문에...
(비지니스 로직적으로 중요한 부분은 아니다.)
- 이유는?
- 연관관계의 주인을 정하는 기준
- 비지니스 로직을 기준으로 연관관계의 주인을 선택하면 안된다.
- 연관관계의 주인은 외래 키의 위치를 기준으로 정해야 한다.
중요 사항
- Controller에는 Entity를 절대 반환하지말 것. DTO로 변환 후 반환하자.
- 설계를 할때 일단 단방향 매핑으로 설계를 완료하자. 객체 입장에서 양방향 설계는 좋을게 없다. 단방향으로 설계를 마치고, 양방향은 필요할 때 추가하면 된다.
- 연관관계의 주인을 정하는 기준
- 비지니스 로직을 기준으로 연관관계의 주인을 선택하면 안된다.
- 연관관계의 주인은 외래 키의 위치를 기준으로 정해야 한다.