@Entity가 붙은 클래스는 JPA가 관리한다.
엔티티라고 함
@Table : 엔티티와 매핑할 테이블을 지정
데이터베이스 테이블을 실행시점에 자동으로 생성하는 기능이 존재.
-> 실행할 경우 @Entity가 붙은 클래스와 매핑된 데이터베이스 테이블을 생성
@Entity
public class Member {
@Id
private Long id;
//컬럼매칭 - db 컬럼에는 'name'으로 생성됨
@Column(name = "name")
private String username;
private Integer age;
//enum 타입을 사용하고 싶은 경우 - STRING 타입을 사용하는 것이 좋음
@Enumerated(EnumType.STRING)
private RoleType roleType;
//Date 타입을 사용하고 싶은 경우
//LocalDate 타입을 사용할 경우, 이 어노테이션은 필요 없음
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
//매우 큰 값을 넣고 싶은 경우
@Lob
private String description;
//메모리 상에서만 사용하고, DB에 매핑하고 싶지 않은 경우
@Transient
int temp;
}
@Column의 세부 기능
@Id
private String id;
@Column(name = "name")
private String username;
직접 할당 방법
자동 생성(@GeneratedValue)
/*
기본키 생성을 데이터베이스에 위임
mysql의 경우 auto_increase와 같음
*/
@GeneratedValue(strategy = GenerationType.IDENTITY)
/*
SEQUENCE 오브젝트를 통해 값을 세팅
*/
@GeneratedValue(strategy = GenerationType.SEQUENCE)
/*
키생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략
*/
@GeneratedValue(strategy = GenerationType.TABLE)
권장하는 식별자 전략
JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL 실행
그러나 AUTO_ INCREMENT는 데이터베이스에 INSERT SQL을 실행
한 이후에 ID 값을 알 수 있음
따라서 예외적으로 IDENTITY 전략을 사용할 때만 em.persist(entity) 호출한 시점에서 데이터베이스에 insert 쿼리를 날린다.
[IDENTITY 전략의 동작 과정]
em.persist(entity) 호출
-> pk값이 null인 상태로 1차 캐시에 저장불가
-> 호출 시점한 시점에서 Insert 쿼리를 DB에 전달
-> DB에서 PK 값 생성
-> PK값을 조회해서(내부적으로 select 쿼리를 보냄) 영속성 컨텍스트의 1차 캐시에 저장,
PK 값이 적용된 영속 엔티티가 초기값일때 스냅샷으로 사용
객체의 참조와 테이블의 외래 키를 매핑하는 법
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
/*
일대 다 관계에서 누가 1이고 누가 다인지 알려줘야 함
이경우 팀은 하나이고 , 팀에 소속된 팀원이 다 이다.
@ManyToOne을 붙여 표시한다
*/
@ManyToOne
// 조인해야 하는 컬럼이 무엇인지 선언
@JoinColumn(name = "TEAM_ID")
private Team team;
…
실사용 예시
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);//알아서 Team의 pk값을 찾아 DB에 삽입함
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team); //단방향 연관관계 설정, 참조 저장
em.persist(member);
//조회
Member findMember = em.find(Member.class, member.getId());
//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<Member>();
…
}
mappedBy를 사용하는 이유
DB 테이블의 경우 한번의 join으로 사용하면 조인된 두 테이블의 데이터를 자유롭게 사용할 수 있다
객체의 경우 연관관계가 두번 필요하다 -> 따라서 누군가가 외래 키를 관리해야 한다
// 주인이 아니면 mappedBy를 선언하여 '주인'의 어떤 칼럼과 join할 것인지 알려야함
@OneToMany(mappedBy = "주인")
1.양방향 매핑시 연관관계의 주인에 값을 입력해야 한다.
순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야 한다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);
em.persist(member)
옳은 코드
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
//없어도 되긴함
team.getMembers().add(member);
//연관관계의 주인에 값 설정
member.setTeam(team);
em.persist(member);
'연관관계 편의매핑' 방식으로 Setter 메소드를 만들면
team.getMembers().add(member); 코드를 사용할 필요 없이
항상 양쪽에 값을 입력할 수 있다.(권장)
public void setTeam(Team team){
this.team=team;
team.getMember().add(this);
}
테이블
객체
연관관계의 주인
테이블은 외래 키 하나로 두 테이블이 연관관계를 맺음
객체 양방향 관계는 A->B, B->A 처럼 참조가 2군데 있으므로
둘중 테이블의 외래 키를 관리할 곳을 지정해야함
연관관계의 주인: 외래 키를 관리하는 참조
주인의 반대편: 외래 키에 영향을 주지 않음, 단순 조회만 가능
외래 키가 있는 쪽이 연관관계의 주인
일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인
연관관계 관리를 위해 추가로 UPDATE SQL 실행해야 하는 단점이 있으므로,
일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는 것이 좋다
주 테이블이나 대상 테이블 중에 외래키 선택 가능
주 테이블/대상 테이블 어디든 외래키 지정 가능
외래키에 데이터베이스 유니크 제약조건이 추가되어야 함
다대일 양방향 매핑 처럼 외래 키가 있는 곳이 연관관계의 주인
반대편은 mappedBy 적용해야 함
• 주 테이블에 외래 키
• 주 객체가 대상 객체의 참조를 가지는 것 처럼
주 테이블에 외래 키를 두고 대상 테이블을 찾음
• 객체지향 개발자 선호
• JPA 매핑 편리
• 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
• 단점: 값이 없으면 외래 키에 null 허용
• 대상 테이블에 외래 키
• 대상 테이블에 외래 키가 존재
• 전통적인 데이터베이스 개발자 선호
• 장점: 주 테이블과 대상 테이블을 일대일에서
일대다 관계로 변경할 때 테이블 구조 유지
• 단점: 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨
관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를
표현할 수 없음
객테의 상속과 구조와 DB의 수퍼타입 서브타입 관계를 매핑
@Inheritance(strategy=InheritanceType.JOINED)
장점
단점
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
장점
단점
사용을 추천하지 않는 전략
장점
단점
공통으로 사용할 매핑 정보가 있을 경우
오직 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공
@MappedSuperclass의 역할
em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
-> 이 코드는 바로 값을 조회하는 것이 아닌,
조회한 값이 사용이 되는 시점에서 DB에서 객체를 조회
특징
유용한 기능들
프록시 인스턴스의 초기화 여부 확인
PersistenceUnitUtil.isLoaded(Object entity)
프록시 클래스 확인 방법
entity.getClass().getName() 출력(..javasist.. or
HibernateProxy…)
프록시 강제 초기화
org.hibernate.Hibernate.initialize(entity);
// 맴버와 팀을 따로따로 조회하는 경우가 많을 떄
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
/*
프록시를 사용하여, 지연로딩을 선언
-> 이 값이 실제로 사용되는 시점에서 조회
*/
@ManyToOne(fetch = FetchType.LAZY) //**
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}
//맴버와 팀을 같이 조회하는 경우가 많을 때
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
/*
프록시를 사용하여, 즉시로딩을 선언
*/
@ManyToOne(fetch = FetchType.EAGER) //**
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}
실무에선 가급적 지연 로딩을 사용하는 것이 좋다.
/*
이런 식으로 연관된 엔티티를 em.persist()를 여러번 사용하여 저장하는 대신,
한번에 영속 상태로 변경가능
*/
@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)
부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
//cascade=CascadeType.REMOVE(또는 ALL) 처럼 사용할 수 있음
@OneToMany(mappedBy="parent", orphanRemoval = true)
스스로 생명주기를 관리하는 엔티티는 sm.persist()로 영속화, em.remove()로 제거할 수 있음
값 타입은 다음과 같이 분류된다.
기본값 타입
임베디드 타입(embedded type, 복합 값 타입)
엔티티 타입
값 타입
특징
생명주기를 엔티티에 의존
-> 회원을 삭제시 이름, 나이 필드도 함꼐 삭제
값 타입은 공유되서는 안된다
-> 회원 이름 변경시 다른 회원의 이름도 함께 변경되선 안됨
primitve 타입은 값을 공유하는 것이 아닌 복사하는 것
Wrapper 클래스는 값을 공유할 수 있으나 변경을 되지 않는다
장점
사용법
한 엔티티에서 같은 값 타입을 사용해야 하는 경우
@AttributeOverrides, @AttributeOverride를 사용해서 컬럼 명 속성을 재정의
// 임베디드 타입 사용
@Entity
public class Member {
@Id @GeneratedVAlue
private Long id;
private String name;
@Embedded
private Period workPeriod; // 근무 기간
@Embedded
private Address homeAddress; // 집 주소
}
// 기간 임베디드 타입
@Embeddable
public class Peroid {
@Temporal(TemporalType.DATE)
Date startDate;
@Temporal(TemporalType/Date)
Date endDate;
// ...
public boolean isWork (Date date) {
// .. 값 타입을 위한 메서드를 정의할 수 있다
}
}
임베디드 타입을 여러 엔티티에서 공유할 경유, 부작용이 발생할 수 있음
-> 값 변경시 특정 엔티티만 변경되는 것이 아닌, 여러 엔티티의 값이 동시에 변경될 수 있다.
따라서 값을 복사해서 사용하여야 한다.
//값을 복사해서 사용
Address a = new Address(“Old”);
Address b = a; //객체 타입은 참조를 전달
b. setCity(“New”)
그러나 컴파일러 레벨에서, 객체타입은 참조값을 직접 대입하는 것을 막을 방법이 없다.
-> 객체의 공유 참조는 피할 수 없다.
객체 타입을 수정할 수 없게 만드는 방법
인스턴스가 달라도 그 안의 값이 같으면 같은 것으로 봐야함
제약사항
따라서 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려하는 것이 좋다