2022. 7. 2. 19:33ㆍLanguage`/JPA
JPA에서 가장 중요한 작업은 "Entity와 Table을 정확히 매핑"하는 것이다
엔티티 매핑 : @Entity
Entity는 정의한 domain에 붙여주는 것이다
@Entity를 붙인 domain은 JPA가 관리하는 domain이 된 것이다
- 연관관계 & column명 & ... 등은 추후에 설명
속성
@Entity에는 적용할 수 있는 속성이 "name" 1가지이다
name이란 JPA에서 사용할 Entity 이름을 지정한다
- default는 클래스 이름을 그대로 사용한다
다른 패키지에 이름이 동일한 Entity가 존재한다면 이 name을 활용해서 구분해줘야 한다
>> 나중에 JPQL에서 엔티티를 대상으로 쿼리를 생성하기 때문에 도메인끼리 겹치는 이름이 존재할 경우 name으로 반드시 구분해줘야 한다 (패키지간 동일 도메인 존재 가능성)
주의사항
1. 기본 생성자가 필수이다
- 파라미터가 없는 public/protected 생성자
2. {final 클래스, enum, interface, inner class}에는 @Entity를 적용할 수 없다
3. 저장할 필드에 final을 사용하면 안된다
테이블 매핑 : @Table
위에서 @Entity를 통해서 클래스를 지정했으면 이제 Entity와 매핑될 "DB상의 Table"을 지정해줘야 한다
속성
1. name
매핑할 테이블의 이름을 설정해주면 된다
@Table(name = "member")
지정하지 않으면 default는 "Entity 이름"을 사용하게 된다
>> 위의 예시의 경우 Member/member를 사용하게 된다
2. catalog
catalog 기능이 있는 DB에서 catalog를 매핑한다
3. schema
schema 기능이 있는 DB에서 schema를 매핑한다
4. uniqueConstraints (DDL)
DDL 생성시 Unique 제약조건을 만든다.
여기서 2개 이상의 복합 Unique 제약조건도 생성할 수 있다
- 이 기능은 hibernate가 제공해주는 "hibernate.hbm2ddl.auto"의 value를 {create / create-drop}으로 했을 경우에만 적용된다
@Table(uniqueConstraints = {
@UniqueConstraint( // username UNIQUE
name = "username_unique",
columnNames = "user_name"
),
@UniqueConstraint( // age UNIQUE
name = "age_unique",
columnNames = "age"
),
@UniqueConstraint( // (username + age) UNIQUE
name = "username_age_unique",
columnNames = {"user_name", "age"}
)
})
5. indexes
indexes는 index를 Table에 index를 생성하는 기능이다
기본키 매핑 : @Id & @GeneratedValue
JPA가 제공하는 DB PK 생성 전략은 2가지로 나눌 수 있다
- 직접 할당
- PK값을 application level에서 직접 할당하는 방식
- 자동 생성
- IDENTITY : PK value 생성을 DB에 위임하는 방식이다 (MySQL의 AUTO_INCREMENT)
- SEQUENCE : DB Sequence를 사용해서 PK를 할당한다
- TABLE : PK value 생성 테이블을 따로 구축해서 할당하는 방식 (이 테이블은 자동으로 생성된다)
자동 생성 전략이 다양한 이유는 DB Vendor마다 지원하는 방식이 다르기 때문이다
- Oracle은 SEQUENCE를 지원하지만, MySQL은 SEQUENCE를 지원하지 않는다
IDENTITY, SEQUENCE는 전적으로 DB에 의존해서 자동생성하는 방식이고, TABLE은 키 생성용 테이블을 별도로 만들고 마치 sequence처럼 사용하는 방식이다
@Id
@Id만 사용한다면 직접 할당으로 PK Value를 제공한다는 의미이다
여기서 자동 생성을 원한다면 @GeneratedValue로 설정해줘야 한다
먼저 @Id를 적용할 수 있는 "자바 타입"은 다음과 같다
- 자바 기본형
- short, int, long, ...
- 자바 Wrapper형
- String, Integer, Long, Short, ...
- java.util.Date
- java.sql.Date
- java.math.BigDecimal
- java.math.BigInteger
@Getter
@Setter
@Entity
@Table(name = "member")
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id
private Long id;
private String username;
private Integer age;
....
....
}
이렇게 PK 직접 할당 전략을 사용한다면 em.persist를 통해서 Entity를 영속화시키기 전에 직접 PK값을 할당해줘야 한다
Member member = new Member(1L, "홍길동", 20);
em.persist(member);
(1) @GeneratedValue(strategy = GenerationType.IDENTITY)
IDENTITY전략은 PK생성을 전적으로 DB에 위임하는 전략이다
주로 {MySQL, PostgreSQL, SQL Server, DB2, ..}에서 사용한다
- MySQL의 AUTO_INCREMENT 기능은 MySQL상에서 DB가 PK를 자동으로 생성해주도록 하는 키워드이다
Member member = new Member("홍길동", 20);
em.persist(member);
Member member2 = new Member("홍길동2", 30);
em.persist(member2);
이렇게 PK부분을 비워두고 persist를 하면 DB에서 알아서 PK를 전략에 따라 생성해준다
※ IDENTITY 전략과 영속성 컨텍스트 관리간의 관계
위에처럼 IDENTITY 전략은 DB에 값을 저자하고 나서야 PK값을 구할 수 있을 때 사용한다
근데 em.persist를 통해서 Entity를 관리하기 위해서는 @Id Value가 "필수적"이다
따라서 IDENTITY 전략을 사용할 경우 JPA는 JDBC3에 추가된 "Statement.getGeneratedKeys()"를 통해서 데이터를 저장함과 동시에 생성된 PK Value를 가져올 수 있다
이를 통해서 IDENTITY 전략을 사용할 경우 "예외적으로" persist를 하자마자 DB에 query가 나가게되고 이와 동시에 Statement.getGeneratedKeys()를 통해서 생성된 PK Value를 가져오는 것이다
- Hibernate는 Statement.getGeneratedKeys()를 사용해서 DB와 1번만 통신을 한다
Member member = new Member("홍길동", 20);
em.persist(member);
Member member2 = new Member("홍길동2", 30);
em.persist(member2);
System.out.println("== Commit 전 ==");
tx.commit();
System.out.println("== Commit 후 ==");
간단한 예제코드를 통해서 과연 persist를 통한 insert query가 언제 db로 날라가는지 확인해보자
Hibernate:
/* insert Strategy.domain.Member
*/ insert
into
member
(age, username)
values
(?, ?)
Hibernate:
/* insert Strategy.domain.Member
*/ insert
into
member
(age, username)
values
(?, ?)
== Commit 전 ==
== Commit 후 ==
결과를 통해서 IDENTITY 전략일 경우 commit 전 : persist 하자마자 db에 query가 나가는 것을 확인할 수 있다
(2) @GeneratedValue(strategy = GenerationType.SEQUENCE)
DB Sequence는 Unique한 value를 순서대로 생성하는 특별한 DB Object이다
SEQUENCE전략은 이 Sequence를 사용해서 PK를 생성해낸다
주로 {Oracle, PostgreSQL, DB2, H2, ..}에서 사용할 수 있다
먼저 Sequence를 사용하려면 시퀀스를 생성해야 한다
CREATE SEQUENCE MEMBER_SEQ START WITH 1 INCREMENT BY 1;
이거는 DB에서 직접 시퀀스를 생성하는 것이고 다음 코드는 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;
...
...
)
@SequenceGenerator를 통해서 Sequence전용 테이블을 생성하고, @GeneratedValue의 generator에 생성한 sequence 테이블 이름을 적용해주면 된다
IDENTITY 전략과 비슷해보이지만 "내부 동작 방식"은 전혀 다르다
SEQUENCE 전략은 em.persist()를 호출할 때 먼저 DB Sequence를 사용해서 PK를 조회한다
그리고 조회한 식별자를 Entity에 할당하고 나서 영속성 컨텍스트에 저장한다
이와 반대로 IDENTITY 전략은 먼저 Entity를 DB에 저장하고 나서, PK를 조회해서 Entity에 넣어주는 방식이다
▶ @SequenceGenerator 속성
속성 | 기능 | default value |
name | SequenceGenerator 이름 | default value가 없는 "필수" 속성이다 |
sequenceName | DB에 등록되어 있는(등록할) Sequence 이름 | hibernate_sequence |
initialValue | DDL 생성시에만 사용되는 속성이다 -> Sequence DDL을 생성할 때 초기 시작값 |
1 |
allocationSize | Sequence 1번 호출에 증가하는 수 -> 성능 최적화에 사용된다 |
50 |
catalog, schema | DB catalog, schema 이름 |
CREATE SEQUENCE [sequenceName] START WITH [initialValue] INCREMENT BY [allocationSize]
▶ SEQUENCE 전략 & 최적화
SEQUENCE 전략은 결국 DB Sequence를 통해서 PK를 persist하기 전에 가져오므로 조회하는 추가 작업이 필요하고 결론적으로 DB와 2번 통신한다
- PK를 구하기 위한 DB 통신
- SELECT MEMBER_SEQ.NEXTVAL FROM DUAL
- 조회한 sequence를 PK값으로 사용해서 DB에 저장
- INSERT INTO MEMBER VALUES(<조회한 PK값>, ....)
JPA는 Sequence에 접근하는 횟수를 줄이기 위한 "최적화" 목적으로 allocationSize를 사용한다
allocationSize에 설정한 값만큼 한번에 Sequence값을 증가시키고 나서 그만큼 메모리에 Sequence 값을 할당한다
- allocationSize가 20이라면 sequence를 한번에 20만큼 증가시키고 나서 1~20까지는 메모리에서 식별자를 할당한다
- 그러고나서 21이 되면 sequence를 또 20만큼 증가시키고 나서 21~40까지는 메모리에서 식별자를 할당한다
package Strategy.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
@Getter
@Setter
@Entity
@Table(name = "member")
@NoArgsConstructor
@AllArgsConstructor
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
sequenceName = "MEMBER_SEQ",
initialValue = 1,
allocationSize = 20
)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBER_SEQ_GENERATOR")
private Long id;
private String username;
private Integer age;
public Member(String username) {
this.username = username;
}
public Member(int age) {
this.age = age;
}
public Member(String username, int age) {
this.username = username;
this.age = age;
}
@Override
public String toString() {
return "Member{" +
"id=" + id +
", username='" + username + '\'' +
", age=" + age +
'}';
}
}
Member member = new Member("홍길동", 20);
em.persist(member);
Member member2 = new Member("홍길동2", 30);
em.persist(member2);
위의 코드를 실행하면 결과는 다음과 같다
Hibernate:
call next value for MEMBER_SEQ
Hibernate:
call next value for MEMBER_SEQ
Hibernate:
/* insert Strategy.domain.Member
*/ insert
into
member
(age, username, id)
values
(?, ?, ?)
Hibernate:
/* insert Strategy.domain.Member
*/ insert
into
member
(age, username, id)
values
(?, ?, ?)
일단 "홍길동"을 넣을때는 sequence의 시작이므로 1이 return되고 그 다음 "홍길동2"를 넣을때는 nextval인 2가 return된다
여기서 "홍길동 & 홍길동2"를 넣기 위해서 sequence를 1번 호출했으므로 1 ~ 20은 메모리상에서 할당되는 것이다
이제 또 persist를하면 다음과 같아진다
Hibernate:
call next value for MEMBER_SEQ
1 ~ 20을 메모리상에서 할당하였기 때문에 현재값은 21이였지만, persist를 통해서 nextvalue를 sequence로부터 호출하였기 때문에 22가 반환되고 "홍길동"은 22를, "홍길동2"는 23을 return받게 되는 것이다
만약 50개의 데이터를 저장한다고 했을 때 allocationSize=1이면 매번 sequence를 호출해야되고 그만큼 성능이 저하될 것이다
하지만 allocationSize=50이면 1번 호출함에 따라 1 ~ 50이 메모리상에 적재되고 따라서 Sequence를 호출하지 않아도 되기 때문에 DB상에 성능이 향상될 것이다
(3) @GeneratedValue(strategy = GenerationType.TABLE)
TABLE전략은 키 생성 전용 테이블을 만듦으로써 SEQUNCE전략을 흉내내는 방식이다
이 전략은 테이블을 사용하기 때문에 모든 DB에 적용할 수 있다
일단 TABLE전략을 사용하려면 먼저 키 생성 용도로 사용할 테이블을 만들어야 한다
CREATE TABLE MY_SEQUENCES(
sequence_name VARCHAR(255) NOT NULL,
next_val BIGINT,
PRIMARY KEY (sequence_name)
)
이처럼 테이블 자체를 생성할 수도 있지만 애노테이션으로 생성할 수도 있다
@TableGenerator(
name = "MEMBER_SEQ_GENERATOR",
table = "MY_SEQUENCES",
pkColumnName = "SEQUENCE_NAME",
valueColumnName = "NEXT_VALUE",
pkColumnValue = "MEMBER_SEQ",
initialValue = 0,
allocationSize = 1
)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "MEMBER_SEQ_GENERATOR")
private Long id;
...
...
}
▶ @TableGenerator 속성
속성 | 기능 | default value |
name | 식별자 생성기 이름 | default value는 없고 필수적으로 개발자가 정해야 한다 |
table | 키생성 테이블명 | hibernate_sequences |
pkColumnName | 시퀀스 컬럼명 | sequence_name |
valueColumnName | 시퀀스 값 컬럼명 | next_val |
pkColumnValue | 키로 사용할 값 이름 | Entity 이름 |
initialValue | 초기값 -> 마지막으로 생성된 값이 기준이다 |
0 |
allocationSize | 시퀀스 한번 호출에 증가하는 수 -> 성능 최적화에 사용된다 |
50 |
catalog, schema | DB catalog, schema 이름 | |
uniqueConstraints(DDL) | Unique 제약조건을 지정할 수 있다 |
여기서 {pkColumnName, valueColumnName, pkColumnvalue}가 헷갈리는데 이 특성은 다음과 같다
"pkColumnName" | "valueColumnName" |
{pkColumnValue1} | value |
{pkColumnValue2} | value |
.... | .... |
SEQUENCE_NAME | NEXT_VAL |
MEMBER_SEQ | 10 |
ART_SEQ | 30 |
ORDER_SEQ | 55 |
.... | .... |
Table 전략은 값을 조회할 때 select query를 사용하고, 해당되는 sequence의 value를 증가시키기 위해서 update query를 사용한다
(4) @GeneratedValue(strategy = GenerationType.AUTO)
AUTO전략은 DB Dialect에 따라 {IDENTITY, SEQUENCE, TABLE}중 자동으로 선택되는 방식이다
- oracle은 SEQUENCE, MySQL은 IDENTITY가 선택된다
@GeneratedValue의 strategy default는 AUTO이다
필드 & 컬럼 매핑
@Column
▶ name
<기능>
- 필드와 매핑할 Table의 column 이름
<default>
- "객체의 필드 이름"
@Column
private String column1;
@Column(name = "column_second")
private String column2;
이 경우 column1은 테이블상에 "column1"과 매핑되고, column2는 테이블상에 "column_second"와 매핑된다
▶ insertable
<기능>
- Table에 저장할 때 해당 필드를 저장할지 여부
<default>
- true
@Column
private String column1;
@Column(insertable = false)
private String column2;
column1은 테이블상에 insert할 수 있고(default), column2는 테이블상에 insert가 불가능하다
▶ updatable
<기능>
- Table에서 해당 필드를 update할 수 있는지 여부
<default>
- true
@Column
private String column1;
@Column(updatable = false)
private String column2;
column1은 테이블상에서 수정이 가능하고(default), column2는 한번 값을 insert하면 절대 update가 불가능하다
▶ unique (DDL)
<기능>
- 해당 필드를 Table상에서 unique한 컬럼으로 설정
- 오직 하나의 컬럼에 대해서 적용하고, 두개 이상의 컬럼에 적용하려면 @Table.uniqueConstraints를 사용해야 한다
- unique를 사용하면 unique 제약조건명은 random하게 생성된다
@Column
private int column1;
@Column(unique = true)
private int column2;
Hibernate:
create table member (
id bigint not null auto_increment,
column1 integer,
column2 integer,
primary key (id)
) engine=MyISAM
Hibernate:
alter table member
add constraint UK_fje8veko180ghs1bxqs95ggws unique (column2)
unique 제약조건이 설정된 column2는 alter table을 통해서 unique 제약조건이 걸리는 것을 확인할 수 있다
그리고 @Column에 unique를 설정하게되면 unique 제약조건명이 random하게 설정된다
@Table(
name = "member",
uniqueConstraints = {
@UniqueConstraint(
name = "Column2_Unique",
columnNames = "column2"
)
}
)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private int column1;
@Column
private int column2;
}
Hibernate:
create table member (
id bigint not null auto_increment,
column1 integer,
column2 integer,
primary key (id)
) engine=MyISAM
Hibernate:
alter table member
add constraint Column2_Unique unique (column2)
이렇게 @Table에다가 uniqueConstraints를 설정해주면 원하는 제약조건명으로 설정할 수 있다
▶ nullable (DDL)
<기능>
- null값 허용 여부 지정
<default>
- true
@Column
private int column1;
@Column(nullable = false)
private int column2;
Hibernate:
create table member (
id bigint not null auto_increment,
column1 integer,
column2 integer not null,
primary key (id)
) engine=MyISAM
column1은 NULL이 허용되고, column2는 NOT NULL이 설정된다
▶ columnDefinition (DDL)
<기능>
- DB Column 정보를 직접 부여할 수 있다
<default>
- {필드 타입 + DB Dialect} 정보를 통해서 적절한 컬럼 타입 생성
@Column(columnDefinition = "VARCHAR(50) COMMENT 'ABCDE'")
private String column1;
@Column(columnDefinition = "VARCHAR(100) UNIQUE NOT NULL CHECK(column2 LIKE '%김')")
private String column2;
Hibernate:
create table member (
id bigint not null auto_increment,
column1 VARCHAR(50) COMMENT 'ABCDE',
column2 VARCHAR(100) UNIQUE NOT NULL CHECK(column2 LIKE '%김'),
primary key (id)
) engine=MyISAM
▶ length (DDL)
<기능>
- 문자 길이 제약 조건 (String에만 사용할 수 있다)
<default>
- 255
@Column
private String column1;
@Column(length = 150)
private String column2;
Hibernate:
create table member (
id bigint not null auto_increment,
column1 varchar(255),
column2 varchar(150),
primary key (id)
) engine=MyISAM
▶ precision & scale (DDL)
<기능>
- BigDecimal Type에 사용할 수 있다
- precision은 소수점을 포함한 전체 자릿수
- scale은 소수의 자릿수 (double, float에는 적용 X)
<default>
- {precision = 19 & scale = 2}
<max>
- {precision=65, scale=30}
@Column(precision = 66, scale = 29)
private BigDecimal column;
// Too-big precision 66 specified for column 'column'. Maximum is 65.
@Column(precision = 65, scale = 31)
private BigDecimal column;
// Too big scale 31 specified for column 'column'. Maximum is 30.
@Column
private BigDecimal column1;
@Column(precision = 65, scale = 30)
private BigDecimal column2;
Hibernate:
create table member (
id bigint not null auto_increment,
column1 decimal(19,2),
column2 decimal(65,30),
primary key (id)
) engine=MyISAM
@Enumerated
자바의 enum 타입을 매핑할 때 사용한다
@Enumerated(EnumType.ORDINAL) : enum "순서"를 DB에 저장 (default)
@Enumerated(EnumType.STRING) : enum "이름"을 DB에 저장
실무에서는 반드시 EnumType.String을 사용해야 하고 이유는 다음과 같다
public enum RoleType {
USER, GUEST
}
@Enumerated(EnumType.ORDINAL)
private RoleType column1;
@Enumerated(EnumType.STRING)
private RoleType column2;
현재 이렇게 설계가 되어있다고 하자
Member memberA = new Member(RoleType.GUEST, RoleType.GUEST);
Member memberB = new Member(RoleType.USER, RoleType.USER);
em.persist(memberA);
em.persist(memberB);
Hibernate:
create table member (
id bigint not null auto_increment,
column1 integer,
column2 varchar(255),
primary key (id)
) engine=MyISAM
위의 코드를 실행시키게 되면 EnumType.ORDINAL은 integer로 생성이되고, EnumType.STRING은 varchar로 생성이 된다
그리고 DB에 저장된 데이터를 살펴보자
여기서 비즈니스적 요구사항으로 RoleType에 "ADMIN"을 추가해달라는 요구가 들어왔다고 하자
public enum RoleType {
ADMIN, USER, GUEST
}
Member memberC = new Member(RoleType.ADMIN, RoleType.ADMIN);
em.persist(memberC);
분명 요구사항이 들어오기 전까지는 {USER = 0, GUEST = 1}순서였지만, ADMIN이 들어오면서 {ADMIN = 0, USER = 1, GUEST = 2}가 되버렸다
여기서 이전에 넣었던 값을 생각해보면 id = 1인 instance는 RoleType.GUEST, id = 2인 instance는 RoleType.USER로 insert하였다
그런데 ADMIN이 들어오면서 이 순서가 완전히 변경되었고 따라서 column1만 있었다면 절대 어느 instance가 어느 RoleType인지 알 수가 없다
>> 따라서 RoleType.STRING으로 사용하자
@Temporal
사실 Temporal은 최신 Hibernate가 "LocalDate & LocalDateTime"을 지원하기 때문에 그냥 컬럼상에서도 LocalDate나 LocalDateTime으로 매핑해주면 된다.
따라서 @Temporal은 거의 사용할 일이 없다
@Temporal(TemporalType.DATE) : 날짜 & DB의 date와 매핑 (2022-07-02)
@Temporal(TemporalType.TIME) : 시간 & DB의 time과 매핑 (19:25:57 ~ (시:분:초))
@Temporal(TemporalType.TIMESTAMP) : 날짜와 시간 & DB의 datetime과 매핑 (2022-07-02 19:25:57)
@Temporal을 사용하면 default값이 없으므로 반드시 type을 지정해줘야 한다
@Lob
@Lob은 DB의 BLOB & CLOB과 매핑된다
CLOB : 매핑하는 필드 타입이 문자 (String, char [], java.sql.CLOB)
BLOB : 문자가 아닌 나머지 (byte [], java.sql.BLOB)
@Lob
private int column1;
@Lob
private byte [] column2;
@Lob
private String column3;
Hibernate:
create table member (
id bigint not null auto_increment,
column1 longblob not null,
column2 longblob,
column3 longtext,
primary key (id)
) engine=MyISAM
자바에서 int와 같은 primitive type은 null이 허용되지 않기 때문에 int로 매핑을 시키면 테이블상에서 자동으로 not null이 들어가게 된다
@Transient
@Transient가 걸려있는 필드는 컬럼과 매핑되지 않는다.
따라서 이거를 사용할 필드는 어떤 값을 "임시로 보관"하는 용도로 사용하면 된다
@Column
private String column1;
@Transient
private String column2;
Hibernate:
create table member (
id bigint not null auto_increment,
column1 varchar(255),
primary key (id)
) engine=MyISAM
@Access
@Access는 JPA가 Entity Data에 접근하는 방식을 지정한다
@Access(AccessType.FIELD) : 필드에 직접 접근하고, private이어도 접근할 수 있다
@Access(AccessType.PROPERTY) : "getter"를 통해서 필드에 접근한다
@Access(AccessType.FIELD)
private String column1;
@Access(AccessType.PROPERTY)
private String column2;