2023년 7월 6일

코딩 - Spring DATA JPA : 엔터티 매핑

1. 엔터티 (Entity) 클래스

1.1 객체와 물리적 테이블 매핑

객체와 물리적 테이블 매핑은 Spring Data JPA( 이하 JPA) 가 관리하는 엔터티 클래스를 물리적 테이블과 매핑하는 방법을 의미하며, 엔터티 클래스는 @Entity 어노테이션을 사용해서 물리적 테이블과 매핑할 수 있다.


디폴트 명명 규칙을 사용하지않고 테이블 이름을 직접 지정하려면 @Table 어노테이션을 사용한다. 논리적인 엔터티 클래스의 이름을 변경하고자 하는 경우 @Entity 의 name 속성을 사용하면 된다.

1.2 필드와 컬럼 매핑

엔터티 필드와 컬럼 매핑하는 방법은 아래과 같다.
  1. @Column 어노테이션을 사용.
  2. @Column 어노테이션의 name 속성을 사용하여 엔터티 필드와 매핑할 컬럼 이름을 지정.
  3. @Column 어노테이션의 nullable 속성을 사용하여 컬럼에 null 값을 허용할지 여부를 지정.
  4. @Column 어노테이션의 length 속성을 사용하여 컬럼의 길이를 지정.
  5. @Column 어노테이션의 precision 속성과 scale 속성을 사용하여 숫자 컬럼의 정밀도와 소수점 자릿수를 지정.
예를 들어 아래와 같이 @Column 어노테이션을 사용하연 엔터티 필드와 데이터베이스 컬럼이 매핑된다. 이렇게 정의된 엔터티는 JPA 를 사용하여 데이터베이스에 저장, 조회, 수정, 삭제를 할 수 있다. (➔ Spring Data JPA 에서는 Repository 를 통하여 처리된다.)


@Entity
@Table(name = "TB_USER")
public class LocalUser {
@Id // tell persistence provider 'id' is primary key
@Column(name = "USER_ID", nullable = false)
@GeneratedValue( // tell persistence provider that value of 'id' will be generated
strategy = GenerationType.IDENTITY // use RDBMS unique id generator
)
private long userId;

@Column(name = "USERNAME", nullable = false)
private String username;

@Column(name = "NAME")
private String name;

@Column(name = "CREATION_DATE", updatable = false)
private Date creationDate;

@Column(name = "MODIFIED_DATE")
private Date modifiedDate;
}



1.3 기본 키 매핑

JPA 에서 기본키 매핑은 ⑴ 직접할당 또는 ⑵ 자동생성 방법으로 할 수 있다. ⑴ 직접할당은 엔터티 필드에 @Id 어노테이션을 사용하고, 해당 필드에 기본 키 값을 직접 주입 또는 할당하는 방법이다. ⑵ 자동생성은 엔터티 필드에 @Id 어노테이션과 @GeneratedValue 어노테이션을 사용하고, @GeneratedValue 어노테이션의 strategy 속성을 사용하여 기본키 생성 전략을 지정하는 방법이다. 기본키 생성 전략은 아래와 같다.

  • IDENTITY - 데이터베이스에서 기본 키를 자동으로 생성.
  • SEQUENCE - 시퀀스를 사용하여 기본 키를 생성.
  • TABLE - 테이블을 사용하여 기본 키를 생성. (데이터베이스 기능을 사용하는 것을 권고)
  • AUTO - 데이터베이스에서 기본 키를 자동으로 생성하거나 시퀀스를 사용하여 기본 키를 생성.(가장 일반적인 전략)

기본 키를 생성하는 방법을 선택할 때는 데이터베이스의 특성과 요구 사항을 고려하여 선택하여야 한다. 개인적으로 Oracle 에서는 SEQUENCE 를 그외는 IDENTITY 전략을 사용하고 있다. 다음 표는 데이터베이스 별 자동증감과 스퀀스 지원 여부를 보여준다.


1.4 생명 주기 매핑

생명주기란 엔터티의 생성, 영속화, 수정, 삭제 과정을 관리하는 것을 말한다. JPA 엔터티의 생명주기에는 네 가지 상태가 있다.

  • 비영속 (new/transient) 상태: 엔티티가 생성되었지만 영속성 컨텍스트에 의해 관리되지 않는 상태.
  • 영속 (managed) 상태: 엔티티가 생성되었고 영속성 컨텍스트에 의해 관리되는 상태.
  • 준영속 (detached) 상태: 엔티티가 영속성 컨텍스트에서 분리된 상태.
  • 삭제 (removed) 상태: 엔티티가 삭제된 상태.

Spring Data JPA Repository 는 기본적인 엔터티의 생명 주기를 자동으로 관리한다.  별도의 생명주기 매핑이 필요한 경우 Repository 에서 

⑴ @PrePersist, @PostPersist, @PreUpdate, @PostUpdate, @PreRemove, @PostRemove 어노테이션을 사용하여 생명 주기를 관리한다.
  
  • @PrePersist 어노테이션을 사용하여 엔터티가 생성될 때 실행할 함수를 지정.
  • @PostPersist 어노테이션을 사용하여 엔터티가 생성된 후 실행할 함수를 지정.
  • @PreUpdate 어노테이션을 사용하여 엔터티가 수정될 때 실행할 함수를 지정.
  • @PostUpdate 어노테이션을 사용하여 엔터티가 수정된 후 실행할 함수를 지정.
  • @PreRemove 어노테이션을 사용하여 엔터티가 삭제될 때 실행할 함수를 지정.
  • @PostRemove 어노테이션을 사용하여 엔터티가 삭제된 후 실행할 함수를 지정.

⑵ @EntityListeners 어노테이션
을 사용하여 생명주기를 관리하는 클래스를 지정할 수 도 있다. 해당 클래스에서는 @PrePersist, @PostPersist, @PreUpdate, @PostUpdate, @PreRemove, @PostRemove 을 사용하여 생명주기를 관리한다.

1.5 타입 매핑

JPA 타입 매핑은 객체와 테이블 간의 관계를 매핑하는 방법이다.  객체와 테이블 간의 관계는 ⑴ 단방향 관계⑵ 양방향 관계로 구분되다. 단방향 관계는 한 쪽에서만 참조가 가능한 관계이다. 

예를 들어,  (단방향 관계)Member 엔터티가 Team 엔터티를 참조하는 경우 Member 엔티티는 Team 엔터티를 참조할 수 있지만, Team 엔터티는 Member 엔터티를 참조할 수 없다. (➔ 일대일 관계 예시로는 국가-수도, 사람-지문 등이 더 적합할 것 같다.)


위 예시를 양방향 관계로 만들어 보면 아래와 같다. Member 엔터티는 Team 엔터티를 참조하고, Team 엔터티는 Member 엔터티를 참조한다. 따라서 Member 엔터티는 Team 엔터티의 정보를 얻을 수 있고, Team 엔터티는 Member 엔터티의 정보를 얻을 수 있다.


양방향의 관계의 경우 엔터티 자체를 JSON 테이터로 변환하는 경우 엔터티들 간의 참조가 무한 반복되는 이슈를 주의해야 한다.

1.6 연관관계 매핑

연관관계 매핑은 객체와 테이블 간의 관계를 정의하는 것을 말한다. 연관관계 매핑을 통해 객체와 테이블 간의 관계를 명확하게 정의할 수 있다. 또한, 연관관계 매핑을 통해 객체와 테이블 간의 관계를 변경할 때 코드를 수정할 필요가 없다.

JPA는 @OneToOne, @OneToMany, @ManyToOne, @ManyToMany 어노테이션을 사용하여 연관관계 매핑을 할 수 있다.

  • @OneToOne 어노테이션은 일대일 관계를 매핑
  • @OneToMany 어노테이션은 일대다 관계를 매핑
  • @ManyToOne 어노테이션은 다대일 관계를 매핑
  • @ManyToMany 어노테이션은 다대다 관계를 매핑

1.6.1 일대일 (One to One)  관계

일대일(OneToOne) 관계는 두 엔터티가 서로 관련이 있고, 한 엔터티는 오직 하나의 다른 엔터티와만 관련이 있는 관계이다. 예를 들어, 학생 엔터티와 주민등록번호 엔터티는 일대일 관계일 수 있다. 한 학생은 오직 하나의 학번을 가질 수 있고, 한 학번은 오직 한 명의 학생만 가질 수 있다.

위의 1.5 예시 에서 Member 클래스의 Team 속성에 있는 @OneToOne 어노테이션은 Member 엔터티가 Team 엔터티와 일대일 관계를 맺고 있음을 나타낸다. 또한 @OneToOne 어노테이션은 Team 속성이 Team 엔티티의 teamId 속성에 대한 외래 키(FK) 임을 지정한다. 

@OneToOne 어노테이션은 mappedBy 속성을 사용하여 일대일  관계의 주체를 지정할 수 있다. mappedBy 속성 값은 일대일 관계의 대상이 참조하는 속성이다. 

@OneToOne (mappedBy = "team")
private Team team;

일대일(OneToOne) 관계를 정의하면 두 엔터티 간의 관계를 매핑하는 중간 테이블이 생성되지 않는다. 한 엔터티의 ID 가 다른 엔터티의 외래 키로 사용되기 때문이다. 이런 이유에서 일대일 관계를 사용하면 두 엔터티 간의 관계를 쉽게 정의하고 관리할 수 있다.

단방향 관계 @OneToOne 을 사용할 때는 몇 가지 유의해야 할 사항이 있다:

  • 일대일 관계는 실제 애플리케이션에서는 일반적이지 않다.
  • 일대일 관계는 특히 두 엔티티가 서로 독립적으로 업데이트되는 경우 관리하기 어려울 수 있다.
  • 일대일 관계는 두 엔티티가 동일한 정보를 저장하는 경우 데이터 중복으로 이어질 수 있다.
  • 일대일 관계 사용을 고려하고 있다면 장단점을 신중하게 검토하는 것이 중요하다. 

1.6.2 일대다 (One to Many)  관계

일대다(OneToMany) 관계는 한 엔터티가 여러 엔터티와 연관될 수 있는 관계이다. 예를 들어, 한 학생이 여러 과목을 수강할 수 있으므로 학생 엔터티와 과목 엔터티는 일대다 관계를 가질 수 있다. JPA에서 일대다 관계를 매핑하려면 @OneToMany 애노테이션을 사용한다. @OneToMany 애노테이션은 연관된 엔터티의 클래스 이름을 지정합니다. 예를 들어, 다음 코드는 학생 엔터티와 과목 엔터티의 일대다 관계를 매핑하고 있다.



import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import lombok.Data;

@Entity
@Table(name="TB_STUDENT")
@Data
public class Student {
@Id
@Column(name="STUDENT_ID")
@GeneratedValue( strategy = GenerationType.IDENTITY)
private Long studentId;

@Column(name="NAME")
private String name;

@OneToMany (mappedBy = "student")
private List<Subject> subjects;

}


import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import lombok.Data;

@Entity
@Table(name="TB_SUBJECT")
@Data
public class Subject {
@Id
@Column(name="SUBJECT_ID")
@GeneratedValue( strategy = GenerationType.IDENTITY)
private Long subjectId;

@Column(name="NAME")
private String name;

@ManyToOne
@JoinColumn( name = "STUDENT_ID")
private Student student;

}


1.5에서 사용된 예시( 두번째 ) 경우 역시 멤버 와 팀 관계는 One To Many 관계이다.

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name = "TB_MEMBER")
public class Member {
@Id
@Column(name = "MEMBER_ID")
@GeneratedValue
private Long memberId;

@Column(name = "NAME")
private String name;

@OneToMany (mappedBy = "team")
private Team team;

}


import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Entity;

@Entity
@Table(name = "TB_TEAM")
public class Team {

@Id
@Column(name = "TEAM_ID")
@GeneratedValue
private Long teamId;

@Column(name = "NAME")
private String name;

@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;

}


1.6.3 일대다 (Many to One)  관계

다대일 (Many to One) 관계는 두 엔터티가 서로 관련이 있고, 한 엔터티는 여러 개의 다른 엔터티와 관련이 있는 관계이다. 예를 들어, 학생 엔터티와 강좌 엔터티는 다대일 관계일 수 있다. 한 학생은 하나의 강좌를 수강할 수 있고, 한 강좌는 여러 명의 학생이 수강할 수 있다.

다대일 관계를 정의하려면 @ManyToOne 애노테이션을 사용한다. 예를 들어, 다음 코드는 학생 엔터티와 강좌 엔터티 간의 다대일 관계를 정의합니다.


import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import lombok.Data;

@Entity
@Table(name = "TB_STUDENT")
@Data
public class Student {

@Id
@Column(name = "STUDENT_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long studentId;

@Column(name = "NAME")
private String name;

@ManyToOne
@JoinColumn( name = "COURSE_ID")
private Course course;
}


import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import lombok.Data;

@Entity
@Table(name = "TB_COURSE")
@Data
public class Course {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "COURSE_ID")
private Long courseId;

@Column(name = "NAME")
private String name;

@ManyToMany(mappedBy = "courses")
private List<Student> students;

}

1.6.4 다대다 (Many to Many)  관계

다대다(ManyToMany) 관계는 두 개 이상의 엔티티가 서로 관련이 있는 관계이다. 예를 들어, 학생 엔터티와 강좌 엔터티는 다대다 관계일 수 있다. 한 학생은 여러 개의 강좌를 수강할 수 있고, 한 강좌는 여러 명의 학생이 수강할 수 있다.

다대다 관계를 정의하려면 @ManyToMany 애노테이션을 사용한다. 예를 들어, 다음 코드는 학생 엔터티와 강좌 엔터티 간의 다대다 관계를 정의한다.


import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import lombok.Data;

@Entity
@Table(name = "TB_STUDENT")
@Data
public class Student {

@Id
@Column(name = "STUDENT_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long studentId;

@Column(name = "NAME")
private String name;

@ManyToMany
@JoinTable(
name = "TB_STUDENT_COURSE",
joinColumns = @JoinColumn( name = "STUDENT_ID"),
inverseJoinColumns = @JoinColumn( name = "COURSE_ID")
)
private List<Course> courses;

}



import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import lombok.Data;

@Entity
@Table(name = "TB_COURSE")
@Data
public class Course {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "COURSE_ID")
private Long courseId;

@Column(name = "NAME")
private String name;

@ManyToMany(mappedBy = "courses")
private List<Student> students;

}


다대다 관계를 정의하면 두 엔터티 간의 관계를 매핑하는 중간 테이블이 생성된다. 예를 들어, 학생 엔터티와 강좌 엔터티 간의 관계를 매핑하는 중간 테이블에는 STUDENT_ID , COURSE_ID 두 개의 열이 생성된다. 


1.6.5 ElementCollection

ElementCollection 은  Collection 형식 데이터를 쉽게 매핑하기 위하여 지원된다. 사용자가 여러 개의 전화번호와 주소를 가질 수 있다고 가정하자. 이를 위하여 전화 번호와 주소를 저장하기 위한 별도의 테이블을 만들어야 한다.



user_phone_numbers 및 user_addresses 테이블에는 모두 users 테이블에 대한 외래 키(FK) user_id 가 포함되어 있다.

일대다(OneToMany) 관계를 사용하여 구현할 수 있다. 그러나 위의 스키마와 같은 기본적이고 임베드 가능한 유형(엔터티가 아닌 객체)의 경우 JPA 는 ElementCollection 형태의 간단한 솔루션을 제공한다. 

@ElementCollection 과 @CollectionTable 어노테이션을 사용하여 쉽게 구현할 수 있다. 

전화번호의 경우 Collection 의 기본 데이터 타입인 Set<String> 을 주소의 @Embeddable 어노테이션을 사용하여 임베드 가능한 유형으로 정의한다. 


import javax.persistence.Embeddable;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
@Data
@Embeddable
public class Address {

@NotNull
@Size(max = 100)
private String addressLine1;

@NotNull
@Size(max = 100)
private String addressLine2;

@NotNull
@Size(max = 100)
private String city;

@NotNull
@Size(max = 100)
private String state;

@NotNull
@Size(max = 100)
private String country;

@NotNull
@Size(max = 100)
private String zipCode;

}


@ElementCollection 어노테이션을 사용하여 ElementCollection 매핑을 정의한다.  이때 컬렉션의 모든 레코드는 별도의 테이블에 저장된다. 이 테이블에 대한 구성은 @CollectionTable 어노테이션을 사용하여 정의한다.

컬렉션의 모든 레코드를 저장하는 테이블의 이름과 기본 테이블을 참조하는 Join 컬럼을 지정하는 데 @CollectionTable 어노테이션을 사용한다. 

또한 ElementCollection 과 함께 Embeddable 유형을 사용하는 경우 @AttributeOverrides 및 @AttributeOverride 어노테이션을 사용하여 Embeddable 유형의 필드를 재정의/사용자 지정할 수 있다.


import java.util.HashSet;
import java.util.Set;

import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.validation.constraints.Email;

@Entity
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotNull
@Size(max = 100)
private String name;

@NotNull
@Email
@Size(max = 100)
@Column(unique = true)
private String email;

@ElementCollection
@CollectionTable(name = "user_phone_numbers", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "phone_number")
private Set<String> phoneNumbers = new HashSet<>();

@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "user_addresses", joinColumns = @JoinColumn(name = "user_id"))
@AttributeOverrides({
@AttributeOverride(name = "addressLine1", column = @Column(name = "house_number")),
@AttributeOverride(name = "addressLine2", column = @Column(name = "street"))
})
private Set<Address> addresses = new HashSet<>();

}


ElementCollection 은 아주 편리한 기능이지만 다음과 같은 주의 사항을 고려해야 한다.

  • ElementCollection은 값 타입의 컬렉션을 매핑하는 데 사용된다.
  • ElementCollection은 엔티티와 달리 식별자를 가질 수 없다.
  • ElementCollection은 엔티티와 달리 생명주기를 관리할 수 없다.
  • ElementCollection은 값 타입의 컬렉션을 매핑하는 데 유용한 방법이지만, 컬렉션의 크기가 크면 성능에 영향을 줄 수 있다.
  • ElementCollection은 컬렉션의 크기가 크면 컬렉션을 조회할 때 주의해야 한다. 

1.7 기타

  • Join 의 경우 디폴트로 Lazy 되어 있어 필요에 따라 fetch 옵션을 지정해야 한다.
  • DISTINCT 가 필요한 경우 JPQL 쿼리에서는 DISTINCT ( 컬럼 이름 ) 을 사용한다. 다만 이경우에는 order by 이슈가 존재한다. 엔터티를 인자로 하는 경우 이슈가 발생되지 않았다.

댓글 없음:

댓글 쓰기