Spring
Raw JPA 로 개발하기
sehunbang
2024. 3. 5. 15:02
도메인 모델과 테이블 설계

- User : 유저 정보로 채널과 관계만 양방향이고, 다른 도메인과는 단방향 관계를 가집니다.
- Channel : 대화 채널은 유저와 다대다 관계를 가진다.
- Thread : 채널내 대화 쓰레드로 Post 와 같이 댓글, 이모지, 멘션과 관계를 가진다.
- Comment : 쓰레드내 댓글로 쓰레드와 다대일 관계를 가지며 이모지, 멘션과도 관계를 가진다.
- Emotion : 쓰레드, 댓글내 이모지로 쓰레드, 댓글과 다대다 관계를 가집니다.
- Mention : 쓰레드, 댓글내 멘션으로 쓰레드, 댓글과 다대다 관계를 가집니다.
Raw JPA 테이블 매핑 기능 (복습)
@Entity
- 객체 관점에서의 이름
- 디폴트로 클래스명으로 설정됨
- 엔티티의 이름은 JQL에서 쓰임
- JQL : Entity 명으로 쿼리짤때 쓰이는 언어 (ex. JPQL, QueryDsl)
@Table
- RDB 의 테이블 이름
- @Entity의 이름이 테이블의 기본값.
- 주로 Entity 이름과 다르게 Table 명을 지정하고 싶을때 아래와 같이 사용
@Id
- 엔티티의 주키를 맵핑할 때 사용.
- 자바의 모든 primitive 타입과 그 랩퍼 타입을 사용할 수 있음
- Date랑 BigDecimal, BigInteger도 사용 가능.
- 복합키를 만드는 맵핑하는 방법도 있지만 그건 논외로..
@GeneratedValue
- 주키의 생성 방법을 맵핑하는 애노테이션
- 생성 전략과 생성기를 설정할 수 있다.
- 기본 전략은 AUTO: 사용하는 DB에 따라 적절한 전략 선택
- TABLE, SEQUENCE, IDENTITY 중 하나.
@Column
- unique
- nullable
- length
- columnDefinition
- ...
@Temporal
- 현재 JPA 2.1까지는 Date와 Calendar만 지원.
@Transient
- 컬럼으로 맵핑하고 싶지 않은 멤버 변수에 사용.
example :
@Entity(name = "parent")
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany
@JoinTable(
name = "parent_child",
joinColumns = @JoinColumn(name = "parent_id"),
inverseJoinColumns = @JoinColumn(name = "child_id")
)
private List<User> childList;
}
Raw JPA 필드 타입 매핑 기능
value 타입 종류
- 기본 타입
- **@Column**
- String, Date, Boolean, 과 같은 타입들에 공통으로 사이즈를 제한할 용도로 쓰인다.
- Class 에 @Entity 가 붙어있으면 자동으로 필드들에 @Column 이 붙음
- **@Enumerated**
- Enum 매핑용도로 쓰이며 실무에서는 @Enumerated(EnumType.*STRING*) 으로 사용권장
- Default 타입인 ORDINAL 은 0,1,2.. 값으로 들어가기 때문에 추후 순서가 바뀔 가능성있다.
- **@Column**
- Composite Value 타입
- **@Embeddable**
- **@Embedded**
- **@AttributeOverrides**
- **@AttributeOverride**
@Embeddable
public class Address {
private String street;
private String city;
private String state;
private String zipCode;
}
Raw JPA 매핑 기능 (복습)
@OneToOne
- 일대일 관계를 나타내는 매핑 정보
- 1:1 관계를 지정하기에 앞서 이것이 꼭 물리적으로 테이블이 분리되어야 하는지에 대해 생각해 봐야 합니다.
- 1:1 관계로 구성 한다는 것은 결국 하나의 목적에 부합되는 공통된 데이타를 관리한다고 볼 수 있으며 이것은 하나의 테이블에서 관리 할 수 있는 데이타일 가능성이 높다는 의미입니다.
- 즉, 의도적 중복이 아니라면 사용할일이 없다는 말
@Entity
public class Locker {
@Id
@GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
}
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
@OneToMany (양방양 없음 ManyToOne 으로 할순 있지만 의미 없음)
- 일대다 관계를 나타내는 매핑 정보
- @OneToMany가 단방향으로 쓰이면 문제가 발생할 수 있다.
- 속도를 위해 기본적으로 FetchType 설정이 LAZY 로 설정되어 있습니다.
- 속성
- mappedBy : 연관관계의 주인 필드를 선택한다.
- fetch : 글로벌 페치 전략 설정
- cascade : 영속성 전이 기능을 사용한다.
- targetEntity : 연관된 엔티티의 타입 정보를 설정한다.
@Entity(name = "parent")
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany
@JoinColumn(name = "parent_id")
private List<Child> childList;
}
@Entity(name = "child")
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "parent_id")
private Long parentId;
}
@ManyToOne
- 다대일 관계를 나타내는 매핑 정보
- 속성
- optional (default true) : false로 설정하면 연관된 엔티티가 반드시 있어야 함.
- fetch : 글로벌 패치 전략 설정
- ✋ 기본이 EGEAR 로 설정되어있으나 실무에서는 기본 LAZY로 설정하는것 추천!
- cascade : 영속성 전이 기능 사용
- targetEntity : 연관된 엔티티의 타입 정보 설정 (targetEntity = Member.class 식으로 사용)
@JoinColumn
- 외래 키 매핑 시 사용 (Join 을 요청하기 위한 매핑정보로 쓰인다.)
- @ManyToOne 어노테이션과 주로 함께 쓰인다. (조인대상 컬럼 지정기능을 안쓸거면 생략해도 됨)
- name 속성은 매핑할 외래키의 이름
- 어노테이션을 생략해도 외래 키가 생성됨.
- 생략 시 외래키의 이름이 기본 전략을 활용하여 생성된다.
- 속성
- name : 매핑할 외래 키의 이름
- referencedColumnName : 외래 키가 참조하는 대상 테이블의 컬럼명
- foreignKey : 외래 키 제약조건 지정 (테이블 생성 시에만 적용됨)
- unique/nullable/insertable/updateable/columnDefinition/table : @Column의 속성과 같음
@Entity(name = "child")
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
@ManyToMany (언제나 양방양) (실무에서는 OneToMany 로 Mapping table 만들어 사용)
- 다대다 관계를 나타내는 매핑 정보 (N:M)
- 다대다 설정을 하게되면 중간 매핑테이블(JoinTable)이 자동으로 생성된다.
- 중간 매핑 테이블은 JPA상에서 숨겨져서(Entity 정의 없이) 관리된다.
예시.
@Entity
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany
@JoinTable(
name = "parent_child",
joinColumns = @JoinColumn(name = "parent_id"),
inverseJoinColumns = @JoinColumn(name = "child_id")
)
private List<Parent> parents;
}
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany(mappedBy = "parents")
private List<Child> childs;
}
- ✋ 매핑 테이블 관리가 불가능하여서 실무에서는 잘 사용하지 않는 기능 입니다.
- 실무에서는 매핑 테이블을 아래와 같은 형태로 직접 정의합니다.
@Entity
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "child")
private List<ParentChild> parentChilds;
}
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "parent")
private List<ParentChild> parentChilds;
}
@Entity
public class ParentChild {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
@ManyToOne
@JoinColumn(name = "child_id")
private Child child;
}
연관관계 매핑 심화
복합키(Forien Key) 사용하기
- PK 1개 - 단일키
- FK 2개 - 복합키 (PK 없이)
- 복합키를 선언하는 방법은 2가지가 있습니다.
- @IdClass를 활용하는 복합키는 복합키를 사용할 엔티티 위에 @IdClass(식별자 클래스) 사용
- @IdClass 복합키 실습코드
- @IdClass를 활용하는 복합키는 복합키를 사용할 엔티티 위에 @IdClass(식별자 클래스) 사용
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserChannelId implements Serializable {
private Long user; // UserChannel 의 user 필드명과 동일해야함
private Long channel; // UserChannel 의 channel 필드명과 동일해야함
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserChannelId userChannelId = (UserChannelId) o;
return Objects.equals(getUser(), userChannelId.getUser()) && Objects.equals(getChannel(), userChannelId.getChannel());
}
@Override
public int hashCode() {
return Objects.hash(getUser(), getChannel());
}
}
@Entity
@IdClass(UserChannelId.class)
public class UserChannel {
@Id
@ManyToOne
@JoinColumn(name = "user_id")
User user;
@Id
@ManyToOne
@JoinColumn(name = "channel_id")
Channel channel;
}
- @EmbeddedId를 활용하는 복합키는 복합키 위에 @EmbeddedId 사용
- @EmbeddedId 복합키 실습코드
@Entity
public class UserChannel {
@EmbeddedId
private UserChannelId userChannelId;
@ManyToOne
@MapsId("user_id")
User user;
@ManyToOne
@MapsId("channel_id")
Channel channel;
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class UserChannelId implements Serializable {
@Column(name = "user_id")
private Long userId;
@Column(name = "channel_id")
private Long channelId;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserChannelId userChannelId = (UserChannelId) o;
return Objects.equals(getUser(), userChannelId.getUser()) && Objects.equals(getChannel(), userChannelId.getChannel());
}
@Override
public int hashCode() {
return Objects.hash(getUser(), getChannel());
}
Raw JPA 기타 기능 (Cascade, orphanRemoval, Fetch)
Cascade (영속성 전이)
- 사용 위치
- 연관관계의 주인 반대편 - 부모 엔티티(다대일에서 일)
- 즉, @OneToMany 가 있는 쪽 또는 @OneToOne 도 가능
- 예를들어, 게시글과 첨부파일이라면 일에 해당하는 게시글에 설정한다.
- 사용 조건
- 양쪽 엔티티의 라이프사이클이 동일하거나 비슷해야한다.
- 예를들어, 게시글이 삭제되면 첨부파일도 같이 삭제 되어야 한다.
- 대상 엔티티로의 영속성 전이는 현재 엔티티에서만 전이 되어야 한다. (다른곳에서 또 걸면 안됨)
- 예를들어, 첨부파일을 게시글이 아닌 다른곳에서 영속성 전이를 하면 안된다.
- 양쪽 엔티티의 라이프사이클이 동일하거나 비슷해야한다.
- 옵션 종류
- ALL : 전체 상태 전이
- PERSIST : 저장 상태 전이
- REMOVE : 삭제 상태 전이
- MERGE : 업데이트 상태 전이
- REFERESH : 갱신 상태 전이
- DETACH : 비영속성 상태 전이
/**
* Defines the set of cascadable operations that are propagated
* to the associated entity.
* The value<code>cascade=ALL</code>is equivalent to
*<code>cascade={PERSIST, MERGE, REMOVE, REFRESH, DETACH}</code>.
*
*@since1.0
*/
public enum CascadeType {
/** Cascade all operations */
ALL,
/** Cascade persist operation */
PERSIST,
/** Cascade merge operation */
MERGE,
/** Cascade remove operation */
REMOVE,
/** Cascade refresh operation */
REFRESH,
/**
* Cascade detach operation
*
*@since2.0
*
*/
DETACH
}
orphanRemoval (고아 객체 제거)
- 사용 위치
- @OneToMany 또는 @OneToOne 에서 사용 - 부모 엔티티
- 사용법
- Cascade.REMOVE 와 비슷한 용도로 삭제를 전파하는데 쓰인다.
- 부모 객체에서 리스트 요소삭제를 했을경우 해당 자식 객체는 매핑정보가 없어지므로 대신 삭제해준다.
- 요건 DB 에서는 절대 알 수 없는 행동이다. (부모가 자식의 손을 놓고 버리고 간 고아 객체)
Parent parent1 = em.find(Parent.class, parent.getId());
parent1.getChildList().remove(0); // delete 쿼리나간다.
그렇다면 Cascade.REMOVE 와 orphanRemoval 차이점은 무엇인가?
Cascade.REMOVE의 경우 일에 해당하는 부모 엔티티를 em.remove를 통해 직접 삭제할 때,그 아래에 있는 다에 해당하는 자식 엔티티들이 삭제되는 것입니다.
orphanRemoval=true는 위 케이스도 포함하며,일에 해당하는 부모 엔티티의 리스트에서 요소를 삭제하기만 해도 해당 다에 해당하는 자식 엔티티가 delete되는 기능까지 포함하고 있다고 이해하시면 됩니다.
즉, orphanRemoval=true 는 리스트 요소로써의 영속성 전이도 해준다는 뜻
- 옵션
- true
- false
✅ 영속성 전이 최강 조합 : orphanRemoval=true + Cascade.ALL
위 2개를 함께 설정하면 자식 엔티티의 라이프 사이클이 부모 엔티티와 동일해지며, 직접 자식 엔티티의 생명주기를 관리할 수 있게 되므로 자식 엔티티의 Repository 조차 없어도 된다. (따라서, 매핑 테이블에서 많이 쓰임)
Fetch (조회시점)
- 사용 위치
- Entity 에 FetchType 으로 설정할 수 있다.
- @ElementCollection, @ManyToMany, @OneToMany, @ManyToOne, @OneToOne
- Query 수행시 fetch Join 을 통해서 LAZY 인 경우도 즉시 불러올 수 있다.
- Entity 에 FetchType 으로 설정할 수 있다.
- 사용법
- 기본 LAZY를 설정한 뒤에 필요할때만 fetch Join 을 수행한다.
- 항상 같이 쓰이는 연관관계 일 경우만 EAGER 를 설정한다.
- 옵션(FetchType)
- EAGER : 즉시 로딩 (부모 조회 시 자식도 같이 조회)
- LAZY : 지연 로딩 (자식은 필요할때 따로 조회)