mojo's Blog
엔티티 매핑 본문
객체와 테이블 매핑
- 객체와 테이블 매핑 : @Entity, @Table
- 필드와 칼럼 매핑 : @Column
- 기본 키 매핑 : @Id
- 연관관계 매핑 : @ManyToOnd, @JoinColumn
※ @Entity
@Entity 가 붙은 클래스는 JPA 가 관리하며 엔티티라고 부른다.
JPA 를 사용해서 테이블과 매핑할 클래스는 @Entity 를 꼭 붙여준다.
주의할 점
- 기본 생성자가 필수다. (parameter 가 없는 public 또는 protected 생성자)
- final 클래스, enum, interface, inner 클래스는 사용하면 안된다.
- 저장할 필드에 final 을 사용하면 안된다.
@Entity 의 name 속성
- JPA 에서 사용할 엔티티 이름을 지정한다.
- 기본값은 클래스 이름을 그대로 사용한다. (ex : Repository)
- 같은 클래스 이름이 없다면 가급적으로 기본값을 사용한다. (즉, name 을 쓸 일이 없음)
※ @Table
@Table 은 엔티티와 매핑할 테이블을 지정한다.
속성은 다음과 같다.
- name : 매핑할 테이블 이름으로 기본값은 엔티티 이름을 사용한다.
- catalog : 데이터베이스 catalog 매핑한다.
- schema : 데이터베이스 schema 매핑한다.
- uniqueConstraints : DDL 생성 시에 유니크 제약 조건을 생성한다.
@Entity
@Table(name = "이걸로 매핑해줘.")
public class Member {
...
}
위와 같이 매핑할 테이블 이름을 변경하면 적용되는 것을 확인할 수 있다.
데이터베이스 스키마 자동 생성
- DDL 을 애플리케이션 실행 시점에 자동으로 생성한다.
- 테이블 중심 -> 객체 중심
- 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL 을 생성한다.
- 이렇게 생성된 DDL 은 개발 장비에서만 사용할 수 있다.
- 생성된 DDL 은 운영서버에서는 사용하지 않거나, 적절히 다듬은 후 사용할 수 있다.
※ 데이터베이스 스키마 자동 생성 - 속성
hibernate.hbm2ddl.auto
- create : 기존 테이블을 삭제하고 다시 생성한다. (DROP + CREATE)
- create-drop : create 와 같으나 종료시점에 테이블을 DROP 해준다.
- update : 변경만 반영한다. (운영 DB에서는 사용 x)
- validate : 엔티티와 테이블이 정상 매핑되었는지를 확인한다.
- none : 사용하지 않는다.
위와 같이 create 로 옵션을 설정한 경우 테이블을 삭제하고 생성하는 것을 확인할 수 있다.
이번에는 클래스에 필드를 하나 추가하고 update 옵션을 줘서 실행해보도록 한다.
@Entity
public class Member {
@Id
private Long id;
private String name;
private int age;
...
}
정상적으로 테이블이 변경된 것을 확인할 수 있다.
그렇다면 필드를 다시 지우고 변경하게 되면 어떻게 될까?
업데이트 되지 않고 지웠던 필드가 그대로 남아있게 된다.
이번엔 더미 필드를 추가해서 validate 옵션을 설정하여 정상 매핑이 되었는지를 확인해본다.
@Entity
public class Member {
@Id
private Long id;
private String name;
private int hello;
...
}
hello 라는 필드가 테이블에 없으므로 에러를 뿜어내는 것을 확인할 수 있다.
※ 데이터베이스 스키마 자동 생성 - 주의
운영 장비에는 절대로 create, create-drop, update 를 사용하면 안된다!
개발 초기 단계에는 create 또는 update 를 사용한다.
테스트 서버에서는 update 또는 validate 를 사용한다.
스테이징, 운영 서버에서는 validate 또는 none 을 사용한다.
필드와 컬럼 매핑
※ 요구사항 추가
1. 회원은 일반 회원과 관리자로 구분할 수 있어야 한다.
2. 회원 가입일과 수정일이 있어야 한다.
3. 회원을 설명할 수 있는 필드가 있어야 하며, 이 필드는 길이 제한이 없다.
@Entity
public class 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 lastModifiedDate;
@Lob
private String description;
public Member() {}
}
public enum RoleType {
USER, ADMIN
}
- @Id : 기본키로 매핑한다.
- @Column(name = "name") : 테이블에서 해당 필드명을 name 으로 칼럼명을 지정한다.
- @Enumerated(EnumType.STRING) : enum 타입으로 USER, ADMIN 이 String 타입으로 설정된다.
- @Temporal(TemporalType.TIMESTAMP) : 해당 애너테이션은 3 가지 속성이 존재한다.
그 중에서 TIMESTAMP 는 날짜 + 시간 정보를 동시에 담을 수 있도록 한다.
나머지 두개는 DATE, TIME 이 존재한다.
- @Lob : varchar 을 넘어선 굉장히 큰 문자열을 넣고 싶을 때 사용된다.
위와 같이 클래스를 설정하고 테이블을 생성하면 다음과 같다.
만약 특정 필드는 테이블에 칼럼을 생성하지 않고 메모리에서만 사용하고 싶을 경우가 있다.
이때는 @Transient 애너테이션을 필드에 붙여서 특정 필드를 컬럼에 매핑하지 않도록 한다.
※ Column
- name : 필드와 매핑할 테이블의 칼럼 이름 (디폴트는 객체의 필드 이름)
- insertable, updateable : 등록, 변경 가능 여부 (디폴트는 TRUE)
- nullable(DDL) : null 값의 허용 여부를 설정한다.
false 설정시 DDL 생성 시에 not null 제약조건이 붙는다.
예를 들어서 특정 컬럼은 널값을 허용하고 싶지 않은 경우가 있다. (항상 값이 들어가 있는 상태)
이때는 아래와 같이 해주면 된다.
@Column(nullable = false)
private String username;
- unique(DDL) : @Table 의 uniqueConstraints 와 같지만 한 컬럼에 간단히 유니크 제약조건을 걸 때 사용한다.
보통은 테이블에서 걸어주는 경우가 많다고 한다.
- columnDefinition(DDL) : 데이터베이스 컬럼 정보를 직접 줄 수 있다.
예를 들어서 특정 컬럼에 "varchar(100) default 'EMPTY'" 와 같이 정보를 부여하고 싶을 때,
아래와 같이 작성하면 된다.
@Column(name = "name", columnDefinition = "varchar(100) default 'EMPTY'")
private String username;
- length(DDL) : 문자 길이 제약조건으로, String 타입에만 사용한다. (디볼트는 255)
- precision, scale(DDL) : BigDecimal 타입에서 사용한다.
precision 은 소수점을 포함한 전체 자릿수를, scale 은 소수의 자릿수다.
참고로 double, float 타입에는 적용되지 않는다.
아주 큰 숫자나 정밀한 소수를 다뤄야 할 때만 사용한다. (디폴트는 precision = 19, scale = 2)
※ Enumerated
자바 enum 타입을 매핑할 때 사용한다.주의할 점은 ORDINAL 을 사용하지 말자! (디폴트가 ORDINAL)
- EnumType.ORDINAL : enum 순서를 데이터베이스에 저장한다.
- EnumType.STRING : enum 이름을 데이터베이스에 저장한다.
만약 ORDINAL 로 지정해서 데이터를 저장하게 된다면 어떻게 될까?
ROLETYPE 이 숫자로 들어가는 현상이 발생한다.
즉, 코드는 잘 작성했지만 테이블에서 해당 칼럼의 값이 무엇을 의미하는지를 모른다는 것이다.
따라서 ORDINAL 을 STRING 으로 변경해서 실행해야 알아볼 수 있다는 장점이 있다.
※ Temporal
날짜 타입(java.util.Date, java.util.Calendar) 을 매핑할 때 사용한다.
참고로 LocalDate, LocalDateTime 을 사용할 때는 생략이 가능하다. (최신 하이버네이트 지원)
- TemporalType.DATE : 날짜, 데이터베이스 date 타입과 매핑 (ex : 2022-08-09)
- TemporalType.TIME : 시간, 데이터베이스 time 타입과 매핑 (ex : 18:46:05)
- TemporalType.TIMESTAMP : 둘 다 매핑 (ex : 2022-08-09 18:46:05)
※ Lob
데이터베이스 BLOB, CLOB 타입과 매핑한다.
@Lob 에는 지정할 수 있는 속성이 없다.
매핑하는 필드 타입이 문자일 경우 CLOB 매핑, 나머지는 BLOB 매핑이다.
- CLOB : String, char[], java.sql.CLOB
- BLOB : byte[], java.sql.BLOB
※ Transient
필드 매핑을 하지 않는다.
데이터베이스에 저장하지 않고 조회하지 않는다.
주로 메모리상에서만 임시로 어던 값을 보관하고 싶을 때 사용된다. (디버깅때 도움)
기본 키 매핑
※ 기본 키 매핑 애너테이션
- @Id : 직접 할당할 때 사용한다.
- @GeneratedValue : 자동 생성할 때 사용한다.
- IDENTITY : 데이터베이스에 위임한다. (MYSQL)
- SEQUENCE : 데이터베이스 시퀀스 오브젝트를 사용한다. (ORACLE, @SequenceGenerator 필요)
- TABLE : 키 생성용 테이블을 사용하며 모든 DB 에서 사용한다. (@TableGenerator 필요)
- AUTO : 방언에 따라 자동 지정한다.
※ IDENTITY 전략
기본 키 생성을 데이터베이스에 위임한다.
주로 MySQL, PostgreSQL, SQL Server, DB2 에서 사용된다. (ex : MYSQL 의 AUTO_INCREMENT)
JPA 는 보통 트랜잭션 커밋 시점에 INSERT SQL 을 실행한다.
AUTO_INCREMENT 는 데이터베이스에 INSERT SQL 을 실행한 이후에 ID 값을 알 수 있다.
IDENTITY 전략은 em.persist() 시점에 즉시 INSERT SQL 실행 후 DB 에서 식별자를 조회한다.
IDENTITY 코드는 아래와 같다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
}
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 {
Member member = new Member();
member.setUsername("java is good");
System.out.println("=== BEFORE ===");
em.persist(member);
System.out.println("member.id = " + member.getId());
System.out.println("=== AFTER ===");
tx.commit();
} catch(Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
persist 를 하면 1차 캐시에 존재하다가(영속 상태) 커밋을 날리면 1차 캐시에 존재하는 SQL 들을
반영하는것을 배웠다.
그런데 신기하게 IDENTITY 전략을 사용하면 커밋 전에 SQL 이 반영된다.
id 는 primary key 이기 때문에 null 이 들어가면 안되는데, 이를 방지하기 위해 JPA 는 Insert 를 바로
적용하고 난 후에 id 를 가져오게 되면 정상적으로 가져올 수 있다.
※ SEQUENCE 전략
데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트이다.
오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용한다.
@SequenceGenerator
- name : 식별자 생성기 이름으로 필수다.
- sequenceName : 데이터베이스에 등록되어 있는 시퀀스 이름이다. (디폴트는 hibernate_sequence)
- initialValue : DDL 생성 시에만 사용되며, 시퀀스 DDL 을 생성할 때 처음 1 시작하는 수를 지정한다.
- allocationSize : 시퀀스 한 번 호출에 증가하는 수이다. (성능 최적화에 사용됨)
데이터베이스 시퀀스 값이 하나씩 증가하도록 이 값을 반드시 1로 설정해야 한다. (디폴트는 50)
- catalog, schema : 데이터베이스 catalog, schema 이름이다.
코드는 아래와 같다.
@Entity
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
sequenceName = "MEMBER_SEQ", // 매핑할 데이터베이스 시퀀스 이름
initialValue = 1, allocationSize = 50)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
...
}
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 {
Member member1 = new Member();
member1.setUsername("A");
Member member2 = new Member();
member2.setUsername("B");
Member member3 = new Member();
member3.setUsername("C");
System.out.println("=== BEFORE ===");
em.persist(member1);
em.persist(member2);
em.persist(member3);
System.out.println("member1.id = " + member1.getId());
System.out.println("member2.id = " + member2.getId());
System.out.println("member3.id = " + member3.getId());
System.out.println("=== AFTER ===");
tx.commit();
} catch(Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
MEMBER_SEQ 을 1 부터 시작해서 50 씩 증가시킨다.
call next value for MEMBER_SEQ 을 두번 호출한 것을 볼 수 있다.
첫 번째로 DB 에서 "call next value for MEMBER_SEQ" 을 호출하면 시퀀스 값은 1이 된다.
(50 씩 증가하기 때문에 이전 시퀀스 값은 -49 임)
그 다음에 성능 최적화를 위해 한번 더 호출하게 되면 시퀀스 값은 51이 된다.
그렇다면 52개 의 데이터를 저장하게 될 경우 그 이상의 시퀀스를 확보해야 하기 때문에 시퀀스 값은
51 에서 101 로 증가하게 된다.
즉, member 의 id 가 1, 2, 3 이 나오게 된 방식은 시퀀스 값을 충분히 확보한 다음에 애플리케이션에서
직접 1, 2, 3 을 얻게 되는 방식이다.
※ TABLE 전략
키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략이다.
모든 데이터베이스에 적용이 가능하지만 성능이 약간 떨어지는 문제가 있다.
코드는 아래와 같다.
@Entity
@TableGenerator(
name = "MEMBER_SEQ_GENERATOR",
table = "MY_SEQUENCES",
pkColumnValue = "MEMBER_SEQ", allocationSize = 1)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
※ 권장하는 식별자 전략
기본 키 제약 조건은 null 이 아니면서 변하면 안된다.
미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. (대체키를 사용하기)
예를 들어 주민등록번호는 기본 키로 적절하지 않다.
Long 형 + 대체키 + 키 생성전략을 사용하는 것을 권장한다고 한다.
실전 예제 - 1. 요구사항 분석과 기본 매핑
※ 요구사항 분석
회원은 상품을 주문할 수 있다.
주문 시 여러 종류의 상품을 선택할 수 있다.
※ 기능 목록
- 회원 기능 : 회원 등록, 회원 조회
- 상품 기능 : 상품 등록, 상품 수정, 상품 조회
- 주문 기능 : 상품 주문, 주문 내역 조회, 주문 취소
※ 도메인 모델 분석
- 회원과 주문의 관계 : 회원은 여러 번 주문할 수 있다. (일대다)
- 주문과 상품의 관계 : 주문할 때 여러 상품을 선택할 수 있다.
반대로 같은 상품도 여러 번 주문될 수 있다.
주문상품 이라는 모델을 만들어서 다대다 관계를 일대다, 다대일 관계로 풀어낸다.
※ 테이블 설계
※ 엔티티 설계와 매핑
※ 데이터 중심 설계의 문제점
현재 방식은 객체 설계를 테이블 설계에 맞춘 방식이다.
테이블의 외래키를 객체에 그대로 가져올 수 있다.
그러나 객체 그래프 탐색이 불가능하며 참조가 없으므로 UML 도 잘못되었다.
'JPA' 카테고리의 다른 글
다양한 연관관계 매핑 (0) | 2022.08.10 |
---|---|
연관관계 매핑 기초 (0) | 2022.08.10 |
영속성 관리 - 내부 동작 방식 (0) | 2022.08.08 |
JPA 시작 (0) | 2022.08.01 |
JPA 이란 (0) | 2022.08.01 |