JPA - Entity Mapping

jungseo·2023년 6월 22일
0

Spring

목록 보기
9/23

단일 엔티티 매핑

  1. 객체와 테이블 매핑
  2. 기본키 매핑
  3. 필드와 열 매핑

1. 객체와 테이블 매핑

@Entity // (name = "USERS) 이름 설정 가능 // 기본값 = 클래스 이름
@Table // (name = "USERS) 이름 설정 가능 // 사용하지 않으면 기본값 = 클래스 이름
@NoArgsConstructor // 기본 생성자 필수 // 없는 경우 에러 발생 가능
@Getter
public class Member { // 테이블 매핑
    @Id
    private Long memberId;

    public Member(Long memberId) {
        this.memberId = memberId;
    }
}
  • @Entity : JPA 관리 대상 Entity로 등록
  • @Table : 생략 가능하며 테이블 이름을 클래스 이름과 다르게 설정할때 사용 (클래스 명과 동일하게 사용하는 것 권장)

2. 기본키 매핑


1) 직접 할당

  • 엔티티 객체를 생성할 때 직접 기본키를 설정해주는 방식

Entity 클래스

@NoArgsConstructor
@Getter
@Entity
public class Member {
    @Id
    private Long memberId;

    public Member(Long memberId) {
        this.memberId = memberId;
    }
}

Config 클래스

@Configuration
public class JpaIdDirectMappingConfig {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaSingleMappingRunner(EntityManagerFactory emFactory){
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

        return args -> {
            tx.begin();
            em.persist(new Member(1L));
            tx.commit();
            Member member = em.find(Member.class, 1L);

            System.out.println("# memberId: " + member.getMemberId());
        };
    }
}
  • 로그
Hibernate: drop table if exists member CASCADE 
Hibernate: create table member (member_id bigint not null, primary key (member_id))
Hibernate: insert into member (member_id) values (?)
# memberId = 1

2) 자동 생성 - IDENTITY

  • 데이터베이스에서 기본키를 대신 생성해주는 방식
  • @GeneratedValue 애너테이션의 strategy 애트리뷰트 값을 GernerationType.IDENTITY로 지정
  • 기본키 없이 영속성 컨텍스트에 저장 후 데이터베이스에 저장할 때 키 생성

Entity 클래스

@NoArgsConstructor
@Getter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long memberId;

    public Member(Long memberId) {
        this.memberId = memberId;
    }
}

Config 클래스

@Configuration
public class JpaIdIdentityMappingConfig {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaSingleMappingRunner(EntityManagerFactory emFactory){
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

				return args -> {
            tx.begin();
            em.persist(new Member());
            tx.commit();
            Member member = em.find(Member.class, 1L);

            System.out.println("# memberId: " + member.getMemberId());
        };
    }
}
  • 로그
Hibernate: drop table if exists member CASCADE 
Hibernate: create table member (member_id bigint generated by default as identity, primary key (member_id))
Hibernate: insert into member (member_id) values (default)
# memberId = 1

3) 자동 생성 - SEQUENCE

  • 데이터베이스 시퀀스를 이용한 방식
  • @GeneratedValue(strategy = GenerationType.SEQUENCE)로 지정
  • 엔티티가 영속성 컨텍스트에 저장되기 전 데이터베이스가 시퀀스에서 기본키에 해당하는 값을 지정

Entity 클래스

@NoArgsConstructor
@Getter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long memberId;

    public Member(Long memberId) {
        this.memberId = memberId;
    }
}

Config 클래스

@Configuration
public class JpaIdIdSequenceMappingConfig {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaSingleMappingRunner(EntityManagerFactory emFactory){
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

        return args -> {
            tx.begin();
            em.persist(new Member());
            Member member = em.find(Member.class, 1L);
            System.out.println("# memberId: " + member.getMemberId());
            tx.commit();
        };
    }
}
  • 로그
Hibernate: drop table if exists member CASCADE 
Hibernate: drop sequence if exists hibernate_sequence

// 시퀀스 생성
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: create table member (member_id bigint not null, primary key (member_id))

// 시퀀스 값을 조회
Hibernate: call next value for hibernate_sequence
# memberId = 1
Hibernate: insert into member (member_id) values (?)

3. 필드와 열 매핑

1) Member 클래스

@NoArgsConstructor
@Getter
@Setter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long memberId;

    @Column(nullable = false, updatable = false, unique = true) // (1-1)
    private String email;

    @Column(length = 100, nullable = false) // (1-2)
    private String name;

    @Column(length = 13, nullable = false, unique = true)
    private String phone;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();   // (2)

    @Column(nullable = false, name = "LAST_MODIFIED_AT") // (1-3)
    private LocalDateTime modifiedAt = LocalDateTime.now();

    @Transient // (3)
    private String age;

	public Member(String email) {
        this.email = email;
    }

	public Member(String email, String name, String phone) {
        this.email = email;
        this.name = name;
        this.phone = phone;
    }
}

(1) @Column : 필드와 열을 매핑해줌

  • 애트리뷰트
    • nullable
      • null 값 허용 여부
      • 디폴트 : true
    • updatable
      • 수정가능 여부
      • 디폴트 : true
    • unique
      • 유니크 제약 조건 설정
      • 디폴트 : false
    • length
      • 열에 저장할 수 있는 문자 길이 지정
      • 디폴트 : 255
    • name
      • 필드명과 다른 이름으로 열 생성 가능
      • 디폴트 : 필드명
  • 생략 시 디폴트 값으로 적용
    • int, long 같은 원시 타입일 경우 애너테이션을 생략했을 때 nullable = false
    • 애트리뷰트 없이 애너테이션만 추가했을 경우 nullable = true

(2) LocalDateTime

  • 시간 및 날짜를 매핑하기 위한 필드
  • java.util.Date, java.util.Calendar 타입으로 매핑하기 위해서는 @Temporal 애너테이션을 추가해야 하지만 LocalDateTime 타입일 경우, @Temporal 애너테이션은 생략 가능
  • 열의 TIMESTAMP 타입과 매핑

(3) @Transient

  • 테이블 열과 매핑하지 않겠다는 의미
  • 데이터베이스에 저장되지 않음
  • 임시 데이터를 메모리에서 사용하기 위한 용도로 사용

2) Order 클래스

@NoArgsConstructor
@Getter
@Setter
@Entity(name = "ORDERS")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long orderId;

    // (1)
    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus = OrderStatus.ORDER_REQUEST;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();

    @Column(nullable = false, name = "LAST_MODIFIED_AT")
    private LocalDateTime modifiedAt = LocalDateTime.now();

    public enum OrderStatus {
        ORDER_REQUEST(1, "주문 요청"),
        ORDER_CONFIRM(2, "주문 확정"),
        ORDER_COMPLETE(3, "주문 완료"),
        ORDER_CANCEL(4, "주문 취소");

        @Getter
        private int stepNumber;

        @Getter
        private String stepDescription;

        OrderStatus(int stepNumber, String stepDescription) {
            this.stepNumber = stepNumber;
            this.stepDescription = stepDescription;
        }
    }
}

(1) @Enumerated

  • enum 타입과 매핑할 때 사용
    • EnumType.ORDINAL : enum의 순서를 나타내는 숫자를 테이블에 저장
    • EnumType.STRING : enum의 이름을 테이블에 저장

엔티티 간의 연관 관계 매핑

1. 단방향 연관 관계

  • 한쪽 클래스만 다른 쪽 클래스의 참조 정보를 가지고 있는 관계
  • Member 클래스의 경우 Order 클래스 객체를 List로 참조
  • Order 클래스는 Member 클래스에 대한 참조 값이 없음

2. 양방향 연관 관계

  • 양쪽 클래스가 서로의 참조 정보를 가지고 있는 관계
  • Spring Date JDBC는 단방향 연관 관계만 지원
  • JPA는 양방향, 단방향 연관 관계 모두 지원

3. 1:N 단방향 연관 관계

  • 예시의 Member 클래스 기준

  • 1에 해당하는 클래스가 N에 해당하는 객체를 참조할 수 있는 관계

  • 1에 해당하는 클래스엔 N에 해당하는 객체 정보가 있지만 N에 해당하는 객체는 1 객체의 정보가 없음

  • 1:N 관계 중 N에 해당하는 테이블에서 1에 해당하는 테이블의 기본키를 외래키로 가져 1:N 단방향 연관 관계로 테이블 간의 관계를 정상적으로 표현 불가능

    • 불필요한 update 쿼리가 추가로 발생
    • 1:N 단방향 연관 관계만 매핑 후 Order 클래스의 정보를 테이블에 저장해도 외래키에 해당하는 memberId 값이 없는 채로 저장
    • N:1 단방향 매핑을 먼저 한 뒤 필요한 경우 1:N 단방향 매핑을 추가해 양방향 연관 관계로 만듦

4. N:1 단방향 연관 관계

  • 예시의 Order 클래스 기준

  • N에 해당하는 클래스가 1에 해당하는 객체를 참조할 수 있는 관계

  • ORDERS 테이블이 MEMBER 테이블의 member_id를 외래키로 가지듯이 Order 클래스가 Member 클래스 참조

@NoArgsConstructor
@Getter
@Setter
@Entity(name = "ORDERS")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long orderId;

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus = OrderStatus.ORDER_REQUEST;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();

    @Column(nullable = false, name = "LAST_MODIFIED_AT")
    private LocalDateTime modifiedAt = LocalDateTime.now();

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

    public void addMember(Member member) {
        this.member = member;
    }

    public enum OrderStatus {
        ORDER_REQUEST(1, "주문 요청"),
        ORDER_CONFIRM(2, "주문 확정"),
        ORDER_COMPLETE(3, "주문 완료"),
        ORDER_CANCEL(4, "주문 취소");

        @Getter
        private int stepNumber;

        @Getter
        private String stepDescription;

        OrderStatus(int stepNumber, String stepDescription) {
            this.stepNumber = stepNumber;
            this.stepDescription = stepDescription;
        }
    }
}
  • (1) @ManyToOne : N:1 관계 명시
  • (2) @JoinColumn : ORDERS 테이블에서 외래키에 해당하는 열 이름
    • MEMBER 테이블의 기본키 열이 "MEMBER_ID" 이기 때문에 동일하게 작성

5. N:1 매핑에 1:N 매핑 추가(양방향 매핑)

@NoArgsConstructor
@Getter
@Setter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long memberId;

    @Column(nullable = false, updatable = false, unique = true)
    private String email;

    @Column(length = 100, nullable = false)
    private String name;

    @Column(length = 13, nullable = false, unique = true)
    private String phone;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();

    @Column(nullable = false, name = "LAST_MODIFIED_AT")
    private LocalDateTime modifiedAt = LocalDateTime.now();

		// (1)
    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();

    public Member(String email) {
        this.email = email;
    }

    public Member(String email, String name, String phone) {
        this.email = email;
        this.name = name;
        this.phone = phone;
    }

    public void addOrder(Order order) {
        orders.add(order);
    }
}
  • (1) @OneToMany(mappedBy = "member")
    • 1:N 단방향 매핑의 경우 mappedBy가 필요 없음

    • mappedBy의 값은 관계를 소유하고 있는 필드를 지정

      • 두 객체들 간에 외래키의 역할을 하는 필드
      • 해당 필드는 N에 해당하는 클래스에 존재

6. N:N 연관 관계

  • N:N 관계의 경우 중간에 테이블을 추가해 두 개의 1:N 관계로 만듦
  • 두 개의 N:1 매핑 후 객체 그래프 탐색으로 원하는 객체를 조회할 수 없다면 일대다 양방향 매핑 추가
  • @ManyToMany를 사용할 경우 엔티티 관리가 복잡해져 중간 연결 엔티티를 사용하는 방법이 더 유연하고 확장성이 높음

객체 그래프 탐색 : 객체를 통해 다른 객체의 정보를 얻을 수 있는 것

  • OrderCoffee 클래스
@NoArgsConstructor
@Getter
@Setter
@Entity(name = "ORDERS_COFFEE")
public class OrderCoffee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long orderCoffeeId;

//	N:1 관계 추가
    @ManyToOne
    @JoinColumn(name = "ORDER_ID")
    private Order order;

    @ManyToOne
    @JoinColumn(name = "COFFEE_ID")
    private Coffee coffee;

    @Column(nullable = false)
    private int quantity;

    public void addCoffee(Coffee coffee) {
        this.coffee = coffee;
    }

    public void addOrder(Order order) {
        this.order = order;
    }
}
  • Order 클래스

//    1:N 관계 추가
    @OneToMany(mappedBy = "order")
    private List<OrderCoffee> orderCoffees = new ArrayList<>();
    
    public void addOrderCoffee(OrderCoffee orderCoffee) {
        orderCoffees.add(orderCoffee);
    }
  • Coffee 클래스
//    1:N 관계 추가
    @OneToMany(mappedBy = "coffee")
    private List<OrderCoffee> orderCoffees = new ArrayList<>();

    public void addOrderCoffee(OrderCoffee orderCoffee) {
        orderCoffees.add(orderCoffee);
    }

7. 1:1 연관 관계

  • @OneToOne 애너테이션을 사용하며 N:1 관계 매핑과 방법은 동일

  • Member 클래스와 1:1 매핑한 Stamp 클래스

@NoArgsConstructor
@Getter
@Setter
@Entity
public class Stamp {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long stampId;

    @Column(nullable = false)
    private int stampCount;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();

    @Column(nullable = false)
    private LocalDateTime modifiedAt = LocalDateTime.now();

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

    public void addMember(Member member) {
        this.member = member;
    }
}
  • Member 클래스
//    1:1 관계 매핑
    @OneToOne(mappedBy = "member")
    private Stamp stamp;

    public void addStamp(Stamp stamp) {
        this.stamp = stamp;
    }

0개의 댓글