객체와 테이블 매핑
엔티티 매핑 소개
- 객체와 테이블 매핑: @Entity, @Table
- 필드와 컬럼 매핑: @Column
- 기본 키 매핑: @Id
- 연관관계 매핑: @ManyToOne,@JoinColumn
@Entity
@Entity가 붙은 클래스는 JPA가 관리, 엔티티라 한다.
JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 필수
주의
- 기본 생성자 필수(파라미터가 없는 public 또는 protected 생성자)
- JPA는 리플렉션 같은 다양한 기술들을 써서 객체를 프록시 하는데 이런 것들을 위해 기본 생성자는 필수
- final 클래스, enum, interface, inner 클래스 사용 X
- 저장할 필드에 final 사용 X
- DB에 저장하고 싶은 필드에는 final 사용 안됨
@Entity 속성 정리
속성: name
- JPA에서 사용할 엔티티 이름을 지정한다.
- 기본값: 클래스 이름을 그대로 사용(예: Member)
- 같은 클래스 이름이 없으면 가급적 기본값을 사용한다.
@Table
@Table은 엔티티와 매핑할 테이블 지정
데이터베이스 스키마 자동 생성
jpa에서는 애플리케이션 로딩 시점에 db 테이블을 생성하는 기능도 지원한다.
테이블중심 -> 객체중심
데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL 생성한다.
이렇게 생성된 DDL은 개발 장비에서만 사용해야 한다.
생성된 DDL은 운영서버에서는 사용하지 않거나, 적절히 다듬은 후 사용해야 한다.
데이터베이스 스키마 자동 생성 - 속성
hibernate.hbm2ddl.auto
- create 옵션은 기존 테이블이 존재할 경우 삭제하고 새로운 테이블을 생성한다.
- create-drop 옵션은 테스트 케이스 같은 걸 실행시킬 때 마지막에 깔끔하게 다 날리고 싶을 때 주로 사용한다.
- udpate 옵션은 alter table을 하는 옵션이다.(추가하는 것만 가능하다. 컬럼을 삭제하는 것은 안 된다.)
create 옵션 실행 결과
Hibernate:
drop table if exists Member cascade
Hibernate:
create table Member (
id bigint not null,
name varchar(255),
primary key (id)
)
update 옵션 실행 결과
Hibernate:
alter table if exists Member
add column age varchar(255)
데이터베이스 스키마 자동 생성 - 주의
운영 장비에는 절대 create, create-drop, update 사용하면 안된다.
- 개발 초기 단계는 create 또는 update 스테이징과
- 테스트 서버는 update 또는 validate
- 운영 서버는 validate 또는 none
스키마 자동 생성 옵션을 사용하여 나온 DDL을 다듬어 운영 서버에 반영할 수 있다.
DDL 생성 기능
- 제약조건 추가: 회원 이름은 필수, 10자 초과X
- @Column(nullable = false, length = 10)
- 유니크 제약조건 추가
- @Table(uniqueConstraints = {@UniqueConstraint( name = "NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"} )})
@Column에서 unique 나 length를 넣어주는 것은 DDL을 생성할 때만 영향을 주고, JPA가 동작하는 것과는 연관이 없다.
JPA의 런타임 시 기능은 결국 DB와 연결하여 테이블에 Insert, Update, Delete 쿼리를 날리는 것과 관련이 있다.
그러한 점에서 봤을 때, @Table의 name 속성을 바꾸면 JPA는 해당 name에 있는 테이블명에 insert, update, delete 쿼리를 날리기 때문에 런타임 기능에 영향을 준다
반면에 @Column의 unique나 length 속성값들은 DDL 생성이 켜져 있을 때, 처음 애플리케이션 실행 시에만 DDL에서 작동할 뿐, JPA의 기능을 활용하는 런타임에서는 사용하지 않는다.
정리하면 DDL 생성 기능은 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다.
필드와 컬럼 매핑
위 내용을 설명하기 위해 아래 예시 추가
요구사항 추가
- 회원은 일반회원과 관리자로 구분해야 한다.
- 회원가입일과 수정일이 있어야 한다.
- 회원을 설명할 수 있는 필드가 있어야 한다. 이필드는 길이 제한이 없다.
@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() {
}
}
- Integer와 가장 적절한 숫자 타입이 선택된다.
- DB에 enum 타입 비슷한 것이 있는데 기본적으로 없다.
- 그래서 Java의 enum 타입을 DB에 저장하려면 @Enumerated을 사용하면 된다.
- 날짜 타입을 쓸려면 @Temporal을 사용하면 된다.
- DB는 보통 날짜, 시간, 날짜시간을 구분해서 사용한다.
- 그래서 (TemporalType.TIMESTAMP)처럼 어떤 거를 쓸지 맵핑 정보를 줘야 한다.
- DB에 varchar를 넘어서 굉장히 큰 컨텐츠를 쓰고 싶으면 @Lob을 사용하면 된다.
위 엔티티의 필드를 바탕으로 생성한 DDL 실행 결과
Hibernate:
create table Member (
age integer,
createdDate timestamp(6),
id bigint not null,
lastModifiedDate timestamp(6),
name varchar(255),
roleType varchar(255) check (roleType in ('USER','ADMIN')),
description clob,
primary key (id)
)
id는 BigInt 타입으로 만들어진다.
@Lob을 사용하는 필드 타입이 문자 타입이면 CLOB으로 생성된다.
enum 타입이 varchar(255)로 맵핑됐다.
매핑 어노테이션 정리
hibernate.hbm2ddl.auto
필드를 DB에 반영하고 싶지 않으면 @Transient를 사용한다.
@Transient
private int temp;
@Column
- insertable, updatable
- @Column(name = "name", insertable = true, updatable = true)
- 위와 같은 경우 삽입은 되지만 수정은 되지 않는다.
- nullable
- null 제약 조건 설정이다. 자주 사용한다.
- unique
- 유니크 제약 조건 설정이다.
- 잘 안 쓴다. 왜냐면 생성된 유니크 제약 조건 이름이 랜덤처럼 이상하게 나온다. 그래서 운영에서 사용 못 한다.
- 그래서 유니크 제약 조건은 @Column에 안 쓰이고 @Table에 쓰인다.
- @Table(uniqueConstraints = ) 이런 식으로@Table에 쓰일 경우 이름도 설정할 수 있다.
@Enumerated
자바 enum 타입을 매핑할 때 사용
자바 enum 타입을 DB에 저장할 때 두 가지를 사용할 수 있다.
ORIDINAL은 enum의 순서를 DB에 저장한다.
STRING은 enum의 이름을 그대로 DB에 저장한다.
ORIDINAL을 사용 시 DDL이 아래와 같이 출력된다.
roleType tinyint check (roleType between 0 and 1),
DB 저장 결과
주의! ORDINAL 사용X
만약 자바 애플리케이션에서 enum 타입이 추가되면 순서가 바뀌어버린다.
그래서 기존 DB에 저장되어 있는 enum 타입의 값과 섞일 수 있어 사용하면 안 된다.
public enum RoleType {
MEM, USER, ADMIN;
}
MEM enum 타입 추가 후 DB에 저장하면 2가 아닌 0이 저장되어 버린다.
@Enumerated(EnumType.STRING)
private RoleType roleType;
꼭 STRING 타입을 사용하자
@Temporal
날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용
참고: LocalDate, LocalDateTime을 사용할 때는 생략 가능(최신 하이버네이트 지원)
@Lob
데이터베이스 BLOB, CLOB 타입과 매핑
- @Lob에는 지정할 수 있는 속성이 없다.
- 매핑하는 필드 타입이 문자면 CLOB 매핑, 나머지는 BLOB 매핑
- CLOB: String, char[], java.sql.CLOB
- BLOB: byte[], java.sql. BLOB
@Transient
- 필드 매핑X
- 데이터베이스에 저장X, 조회X
- 주로 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용
@Transient
private Integer temp;
기본 키 매핑
기본 키 매핑 어노테이션
- @Id
- @GeneratedValue
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
기본 키 매핑 방법
- 직접 할당: @Id만 사용
- 직접 id 값을 할당하는 경우 @Id만 사용한다.
- 자동 생성(@GeneratedValue)
- IDENTITY: 데이터베이스에 위임, MYSQL
- SEQUENCE: 데이터베이스 시퀀스 오브젝트 사용, ORACLE
- @SequenceGenerator 필요
- TABLE: 키 생성용 테이블 사용, 모든 DB에서 사용
- @TableGenerator 필요
- AUTO: 방언에 따라 자동 지정, 기본값
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private String id;
AUTO 전략은 DB 방언에 맞춰서 자동으로 생성된다.
오라클이면 시퀀스가 생성되고 MySQL이면 MySQL에 맞춰서 생성된다.
IDENTITY 전략 - 특징
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
- 기본 키 생성을 데이터베이스에 위임
- 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용(예: MySQL의 AUTO_ INCREMENT)
- JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL 실행
- AUTO_ INCREMENT는 데이터베이스에 INSERT SQL을 실행한 이후에 ID 값을 알 수 있음
- IDENTITY 전략은 em.persist() 시점에 즉시 INSERT SQL 실행하고 DB에서 식별자를 조회
마지막 두 문장에 대해 좀 더 알아보자.
IDENTITY 전략은 id에 값을 넣고 DB로 insert 쿼리문을 보내면 안 된다.
id에 null 값이 들어간 상태로 insert 쿼리가 DB에 보내진다. DB에서는 이때 id에 값을 세팅해 준다.
문제 발생
id 값을 알 수 있는 시점이 언제냐면 DB에 insert 쿼리가 보내져서 id값이 저장이 되고 난 후에 알 수 있다.
문제는 영속성 컨텍스트에서 값이 관리되려면 PK값(id)이 있어야 한다.
JPA 입장에선 키(PK)가 없으니까 값을 넣을 수 있는 방법이 없다.
그래서 IDENTITY 전략에서만 예외적으로 em.persist()를 호출하자마자 DB에 insert 쿼리를 보낸다.
(원래는 커밋하는 시점에 insert 쿼리가 날아간다.)
Hibernate:
/* insert for jpa_basic.ex1_hello_jpa.hellojpa.Member */
insert into
Member (name, id)
values
(?, default)
테스트를 한번 해보자.
테스트 코드
Member member = new Member();
member.setUsername("A");
System.out.println("======================");
em.persist(member);
System.out.println("member.id = " + member.getId());
System.out.println("=======================");
tx.commit();
실행결과
======================
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.Member */insert
into
Member (name, id)
values
(?, default)
member.id = 1
=======================
보면 id 값이 1이라고 잘 출력된다.
em.persist() 시점에 일단 DB에 insert 쿼리를 보내면 DB에서는 id 값에 1을 할당하고 저장한다.
그러면 DB가 생성한 id 값을 읽어온다. 이때 select 쿼리가 없는 이유는 JDBC 드라이버에는 insert 쿼리를 보내고 바로 리턴을 받을 수 있게 내부적으로 코드가 짜여있다. 따라서 DB에 insert 하는 시점에 값을 바로 알 수 있다.
그래서 JPA가 해당 id 값을 가져와서 영속성 컨텍스트의 pk 값으로 쓸 수 있다.
따라서 IDENTITY 전략에서는 insert 문을 모아서 보내는 게 불가능하다.
SEQUENCE 전략 - 특징
@Entity
@SequenceGenerator(
name = “MEMBER_SEQ_GENERATOR",
sequenceName=“MEMBER_SEQ",//매핑할 데이터베이스 시퀀스 이름
initialValue = 1, allocationSize = 1)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBER_SEQ_GENERATOR")
private Long id;
...
}
- 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트(예: 오라클 시퀀스)
- 데이터베이스에 있는 sequence object를 통해서 값을 gnenerating 한다.
- sequence object를 통해서 값을 가져온 다음 id에 가져온 값을 세팅한다.
- 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용
실행결과
Hibernate:
drop table if exists Member cascade
Hibernate:
drop sequence if exists MEMBER_SEQ
Hibernate:
create sequence MEMBER_SEQ start with 1 increment by 1
Hibernate:
create table Member (
id bigint not null,
name varchar(255) not null,
primary key (id)
)
Hibernate:
select
next value for MEMBER_SEQ
Hibernate:
/* insert for jpa_basic.ex1_hello_jpa.hellojpa.Member */
insert into
Member (name, id)
values
(?, ?)
실행결과를 봐보자.
우선 create sequence MEMBER_SEQ start with 1 increment by 1 sequence를 통해 시퀀스를 만든 걸 볼 수 있다. 그리고 select next value for MEMBER_SEQ를 보면 생성한 sequence에서 값을 가져온 다음에 insert into 구문에서 values(?, ?) 첫 번째 ? 에 값이 들어간다.
SEQUENCE - @SequenceGenerator
주의: allocationSize 기본값 = 50
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;
- 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략
- 장점: 모든 데이터베이스에 적용 가능
- 단점: 성능 이슈(락에 걸릴 수 있음 이외 다른 성능 이슈도 존재)
어떤 DB는 auto_increment가 있고 어떤 DB는 sequence가 있다.
반면 테이블 전략은 모든 DB에 다 적용할 수 있다.
테이블을 하나 만들어서 거기서 키를 계속 만드는 방식이다.
실행결과
Hibernate:
create table Member (
id bigint not null,
name varchar(255) not null,
primary key (id)
)
Hibernate:
create table MY_SEQUENCES (
next_val bigint,
sequence_name varchar(255) not null,
primary key (sequence_name)
)
Hibernate:
insert into MY_SEQUENCES(sequence_name, next_val) values ('MEMBER_SEQ',0)
Hibernate:
select
tbl.next_val
from
MY_SEQUENCES tbl
where
tbl.sequence_name=? for update
Hibernate:
update
MY_SEQUENCES
set
next_val=?
where
next_val=?
and sequence_name=?
Hibernate:
/* insert for jpa_basic.ex1_hello_jpa.hellojpa.Member */
insert into
Member (name, id)
values
(?, ?)
실행결과를 보면 테이블이 2개 생성된 것을 볼 수 있다.
H2 콘솔을 보면 실제 2개의 테이블이 생성된 것을 볼 수 있고 MY_SEQUENCES를 조회하면 아래와 같이 값이 나온다.
값을 한번 더 넣으면 MY_SEQUENCES 테이블에 NEXT_VAL 값이 증가한 것을 볼 수 있다.
@TableGenerator - 속성
권장하는 식별자 전략
- 기본 키 제약 조건: null 아님, 유일, 변하면 안된다.
- 미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. 대리키(대체키)를 사용하자.
- 자연키: 비즈니스적으로 의미있는 키, 주민등록번호, 전화번호 이런 것들을 자연키라 한다.
- 예를 들어 주민등록번호도 기본 키로 적절하지 않다.
- 권장: Long형 + 대체키 + 키 생성전략 사용
정리
auto-increment나 아니면 sequence-object 둘 중에 하나를 쓰거나 아니면 UUID 같은 랜덤 값을 조합 방식을 사용하는 게 좋다.
실전 예제 - 1. 요구사항 분석과 기본 매핑
요구사항 분석
- 회원은 상품을 주문할 수 있다.
- 주문시 여러 종류의 상품을 선택 할 수 있다.
도메인 모델 분석
- 회원과 주문의 관계: 회원은 여러 번 주문할 수 있다. (일대다)
- 주문과 상품의 관계: 주문할때 여러 상품을 선택 할 수 있다. 반대로 같은 상품도 여러 번 주문될 수 있다.
- 주문상품이라는 모델을 만들어서 다대다 관계를 일다대, 다대일 관계로 풀어냄
테이블 설계
엔티티 설계와 매핑
데이터 중심 설계의 문제점
- 현재 방식은 객체 설계를 테이블 설계에 맞춘 방식
- 테이블의 외래키를 객체에 그대로 가져옴
- 객체 그래프 탐색이 불가능
- 참조가 없으므로 UML도 잘못됨
실제 코드 예시
Order order = em.find(Order.class, 1L);
Long memberId = order.getMemberId();
Member member = em.find(Member.class, memberId);
위 설계 방식은 객체를 관계형 DB 테이블에 맞춘 설계 방식이다.
그러다 보니 주문한 고객의 정보를 조회하고 싶은 경우 order에서 member를 참조해서 가져오는 게 아닌 order 자체에 memberId 값을 저장하여 memberId 값을 가지고 member를 조회해야 한다.
객체는 객체 그래프로 자유롭게 참조를 할 수 있어야 하는데 데이터 중심 설계 방식은 끊겨버리게 된다.
이런 방식의 설계는 객체지향스럽지 않다.
참고
김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본 편 강의를 보고 정리한 내용입니다.
'JPA > JPA 기본' 카테고리의 다른 글
6. 다양한 연관관계 매핑 (0) | 2024.08.10 |
---|---|
5. 연관관계 매핑 기초 (0) | 2024.08.10 |
3. 영속성 관리 - 내부 동작 방식 (0) | 2024.08.08 |
2. JPA 시작하기 (1) | 2024.08.08 |
1. JPA 소개 (0) | 2024.08.08 |